From 475ac51cd89660acea27398a26744ddeed87a590 Mon Sep 17 00:00:00 2001 From: sunspirit99 Date: Wed, 19 Feb 2025 19:27:01 +0700 Subject: [PATCH] tmp --- pkg/liquidity-source/four-meme/abis.go | 188 +++++++++ .../four-meme/abis/TokenManager.json | 208 ++++++++++ .../four-meme/abis/TokenManager2.json | 376 ++++++++++++++++++ .../four-meme/abis/TokenManagerHelper3.json | 309 ++++++++++++++ pkg/liquidity-source/four-meme/config.go | 11 + pkg/liquidity-source/four-meme/constant.go | 34 ++ pkg/liquidity-source/four-meme/embed.go | 12 + .../four-meme/pool_list_updater.go | 269 +++++++++++++ .../four-meme/pool_simulator.go | 341 ++++++++++++++++ .../four-meme/pool_simulator_test.go | 236 +++++++++++ .../four-meme/pool_tracker.go | 220 ++++++++++ pkg/liquidity-source/four-meme/types.go | 74 ++++ pkg/msgpack/register_pool_types.gen.go | 2 + pkg/pooltypes/pooltypes.go | 3 + pkg/valueobject/exchange.go | 2 + 15 files changed, 2285 insertions(+) create mode 100644 pkg/liquidity-source/four-meme/abis.go create mode 100644 pkg/liquidity-source/four-meme/abis/TokenManager.json create mode 100644 pkg/liquidity-source/four-meme/abis/TokenManager2.json create mode 100644 pkg/liquidity-source/four-meme/abis/TokenManagerHelper3.json create mode 100644 pkg/liquidity-source/four-meme/config.go create mode 100644 pkg/liquidity-source/four-meme/constant.go create mode 100644 pkg/liquidity-source/four-meme/embed.go create mode 100644 pkg/liquidity-source/four-meme/pool_list_updater.go create mode 100644 pkg/liquidity-source/four-meme/pool_simulator.go create mode 100644 pkg/liquidity-source/four-meme/pool_simulator_test.go create mode 100644 pkg/liquidity-source/four-meme/pool_tracker.go create mode 100644 pkg/liquidity-source/four-meme/types.go diff --git a/pkg/liquidity-source/four-meme/abis.go b/pkg/liquidity-source/four-meme/abis.go new file mode 100644 index 000000000..8287f1dbf --- /dev/null +++ b/pkg/liquidity-source/four-meme/abis.go @@ -0,0 +1,188 @@ +package fourmeme + +import ( + "bytes" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/samber/lo" +) + +var ( + erc20ABI abi.ABI + tokenManagerABI abi.ABI + tokenManager2ABI abi.ABI + tokenManagerHelperABI abi.ABI +) + +func init() { + builder := []struct { + ABI *abi.ABI + data []byte + }{ + { + &tokenManagerABI, tokenManagerABIJson, + }, + { + &tokenManager2ABI, tokenManager2ABIJson, + }, + { + &tokenManagerHelperABI, tokenManagerHelperABIJson, + }, + } + + for _, b := range builder { + var err error + *b.ABI, err = abi.JSON(bytes.NewReader(b.data)) + if err != nil { + panic(err) + } + } + + // https://www.notion.so/kybernetwork/four-meme-reverse-engineer-19d26751887e80119fc9d113074b2365?pvs=4#19d26751887e80488cf8e96d0d41794c + + // method for query templates + tokenManager2ABI.Methods["_templates"] = abi.Method{ + Inputs: abi.Arguments{ + { + Name: "", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + }, + Outputs: abi.Arguments{ + { + Name: "", + Type: lo.Must(abi.NewType("address", "", nil)), + }, + { + Name: "", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "minTradeFee", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + }, + } + + // method for query tradingHalted + tokenManager2ABI.Methods["tradingHalted"] = abi.Method{ + ID: common.Hex2Bytes("088c5d0b"), + Inputs: abi.Arguments{}, + Outputs: abi.Arguments{ + { + Name: "", + Type: lo.Must(abi.NewType("bool", "", nil)), + }, + }, + } + + // method for query tokenTxFee + tokenManager2ABI.Methods["tokenTxFee"] = abi.Method{ + ID: common.Hex2Bytes("9f266331"), + Inputs: abi.Arguments{ + { + Name: "", + Type: lo.Must(abi.NewType("address", "", nil)), + }, + }, + Outputs: abi.Arguments{ + { + Name: "tokenTxFee", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + }, + } + + tokenManager2ABI.Methods["tokenData"] = abi.Method{ + ID: common.Hex2Bytes("e684626b"), + Inputs: abi.Arguments{ + { + Name: "", + Type: lo.Must(abi.NewType("address", "", nil)), + }, + }, + Outputs: abi.Arguments{ + { + Name: "token", + Type: lo.Must(abi.NewType("address", "", nil)), + }, + { + Name: "raisedToken", + Type: lo.Must(abi.NewType("address", "", nil)), + }, + { + Name: "templateId", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "field3", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "maxOffers", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "maxFunds", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "LaunchTime", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "Offers", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "Funds", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "Price", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "field10", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "field11", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + { + Name: "TradingDisabled", + Type: lo.Must(abi.NewType("uint256", "", nil)), + }, + }, + } +} diff --git a/pkg/liquidity-source/four-meme/abis/TokenManager.json b/pkg/liquidity-source/four-meme/abis/TokenManager.json new file mode 100644 index 000000000..765ed29cb --- /dev/null +++ b/pkg/liquidity-source/four-meme/abis/TokenManager.json @@ -0,0 +1,208 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "requestId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "launchTime", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "launchFee", + "type": "uint256" + } + ], + "name": "TokenCreate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "etherAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "TokenPurchase", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "etherAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "TokenSale", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "TradeStop", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxFunds", + "type": "uint256" + } + ], + "name": "purchaseToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "funds", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minAmount", + "type": "uint256" + } + ], + "name": "purchaseTokenAMAP", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "saleToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/pkg/liquidity-source/four-meme/abis/TokenManager2.json b/pkg/liquidity-source/four-meme/abis/TokenManager2.json new file mode 100644 index 000000000..66dca473a --- /dev/null +++ b/pkg/liquidity-source/four-meme/abis/TokenManager2.json @@ -0,0 +1,376 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "base", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "offers", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "quote", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "funds", + "type": "uint256" + } + ], + "name": "LiquidityAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "requestId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "launchTime", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "launchFee", + "type": "uint256" + } + ], + "name": "TokenCreate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "cost", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "offers", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "funds", + "type": "uint256" + } + ], + "name": "TokenPurchase", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "cost", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "offers", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "funds", + "type": "uint256" + } + ], + "name": "TokenSale", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "TradeStop", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxFunds", + "type": "uint256" + } + ], + "name": "buyToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxFunds", + "type": "uint256" + } + ], + "name": "buyToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "funds", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minAmount", + "type": "uint256" + } + ], + "name": "buyTokenAMAP", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "funds", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minAmount", + "type": "uint256" + } + ], + "name": "buyTokenAMAP", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "sellToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "_tokenCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "_tradingFeeRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "_tokens", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/pkg/liquidity-source/four-meme/abis/TokenManagerHelper3.json b/pkg/liquidity-source/four-meme/abis/TokenManagerHelper3.json new file mode 100644 index 000000000..64df77fb9 --- /dev/null +++ b/pkg/liquidity-source/four-meme/abis/TokenManagerHelper3.json @@ -0,0 +1,309 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "PANCAKE_FACTORY", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "PANCAKE_V3_FACTORY", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "TM", + "outputs": [ + { + "internalType": "contract ITokenManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "TM2", + "outputs": [ + { + "internalType": "contract ITokenManager2", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "TOKEN_MANAGER", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "TOKEN_MANAGER_2", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "WETH", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "maxRaising", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "offers", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserves", + "type": "uint256" + } + ], + "name": "calcInitialPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "getTokenInfo", + "outputs": [ + { + "internalType": "uint256", + "name": "version", + "type": "uint256" + }, + { + "internalType": "address", + "name": "tokenManager", + "type": "address" + }, + { + "internalType": "address", + "name": "quote", + "type": "address" + }, + { + "internalType": "uint256", + "name": "lastPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tradingFeeRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minTradingFee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "launchTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "offers", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxOffers", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "funds", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxFunds", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "liquidityAdded", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "funds", + "type": "uint256" + } + ], + "name": "tryBuy", + "outputs": [ + { + "internalType": "address", + "name": "tokenManager", + "type": "address" + }, + { + "internalType": "address", + "name": "quote", + "type": "address" + }, + { + "internalType": "uint256", + "name": "estimatedAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "estimatedCost", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "estimatedFee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountMsgValue", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountApproval", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountFunds", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "trySell", + "outputs": [ + { + "internalType": "address", + "name": "tokenManager", + "type": "address" + }, + { + "internalType": "address", + "name": "quote", + "type": "address" + }, + { + "internalType": "uint256", + "name": "funds", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/pkg/liquidity-source/four-meme/config.go b/pkg/liquidity-source/four-meme/config.go new file mode 100644 index 000000000..fda8968a0 --- /dev/null +++ b/pkg/liquidity-source/four-meme/config.go @@ -0,0 +1,11 @@ +package fourmeme + +import "github.com/KyberNetwork/kyberswap-dex-lib/pkg/valueobject" + +type Config struct { + ChainID valueobject.ChainID `json:"chainID"` + NewPoolLimit int `json:"newPoolLimit"` + TokenManagerV2 string `json:"tokenManagerV2"` + TokenManagerHelperV3 string `json:"tokenManagerHelperV3"` + DefaultQuoteToken string `json:"defaultQuoteToken"` +} diff --git a/pkg/liquidity-source/four-meme/constant.go b/pkg/liquidity-source/four-meme/constant.go new file mode 100644 index 000000000..b3c41a718 --- /dev/null +++ b/pkg/liquidity-source/four-meme/constant.go @@ -0,0 +1,34 @@ +package fourmeme + +import ( + "github.com/holiman/uint256" +) + +var ( + defaultGas = Gas{Swap: 250000} + + ZERO = uint256.NewInt(0) + PRECISION = uint256.NewInt(10000) + EXP18 = new(uint256.Int).Exp(uint256.NewInt(10), uint256.NewInt(18)) + EXP9 = new(uint256.Int).Exp(uint256.NewInt(10), uint256.NewInt(9)) +) + +const ( + DexType = "four-meme" + + erc20BalanceOfMethod = "balanceOf" + + pairTokenAMethod = "tokenA" + pairTokenBMethod = "tokenB" + pairGetReservesMethod = "getReserves" + pairKLastMethod = "kLast" + + tokenManager2TokenCountMethod = "_tokenCount" + tokenManager2TokensMethod = "_tokens" + + tokenManagerHelperGetTokenInfoMethod = "getTokenInfo" + + ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +) + +var () diff --git a/pkg/liquidity-source/four-meme/embed.go b/pkg/liquidity-source/four-meme/embed.go new file mode 100644 index 000000000..d966bf928 --- /dev/null +++ b/pkg/liquidity-source/four-meme/embed.go @@ -0,0 +1,12 @@ +package fourmeme + +import _ "embed" + +//go:embed abis/TokenManager.json +var tokenManagerABIJson []byte + +//go:embed abis/TokenManager2.json +var tokenManager2ABIJson []byte + +//go:embed abis/TokenManagerHelper3.json +var tokenManagerHelperABIJson []byte diff --git a/pkg/liquidity-source/four-meme/pool_list_updater.go b/pkg/liquidity-source/four-meme/pool_list_updater.go new file mode 100644 index 000000000..10e89e33d --- /dev/null +++ b/pkg/liquidity-source/four-meme/pool_list_updater.go @@ -0,0 +1,269 @@ +package fourmeme + +import ( + "context" + "math/big" + "strings" + "time" + + "github.com/KyberNetwork/ethrpc" + "github.com/KyberNetwork/logger" + "github.com/ethereum/go-ethereum/common" + "github.com/goccy/go-json" + + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/entity" + poollist "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool/list" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/util" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/valueobject" +) + +type ( + PoolsListUpdater struct { + config *Config + ethrpcClient *ethrpc.Client + } + + PoolsListUpdaterMetadata struct { + Offset int `json:"offset"` + } +) + +var _ = poollist.RegisterFactoryCE(DexType, NewPoolsListUpdater) + +func NewPoolsListUpdater( + cfg *Config, + ethrpcClient *ethrpc.Client, +) *PoolsListUpdater { + return &PoolsListUpdater{ + config: cfg, + ethrpcClient: ethrpcClient, + } +} + +func (u *PoolsListUpdater) GetNewPools(ctx context.Context, metadataBytes []byte) ([]entity.Pool, []byte, error) { + var ( + dexID = DexType + startTime = time.Now() + ) + + logger.WithFields(logger.Fields{"exchange": dexID}).Info("Started getting new pools") + + ctx = util.NewContextWithTimestamp(ctx) + + allPairsLength, err := u.getAllPairsLength(ctx) + if err != nil { + logger. + WithFields(logger.Fields{"dex_id": dexID}). + Error("getAllPairsLength failed") + + return nil, metadataBytes, err + } + + offset, err := u.getOffset(metadataBytes) + if err != nil { + logger. + WithFields(logger.Fields{"dex_id": dexID, "err": err}). + Warn("getOffset failed") + } + + batchSize := u.getBatchSize(allPairsLength, u.config.NewPoolLimit, offset) + + tokenList, tokenInfoList, err := u.listPairs(ctx, offset, batchSize) + if err != nil { + logger. + WithFields(logger.Fields{"dex_id": dexID, "err": err}). + Error("listPairAddresses failed") + + return nil, metadataBytes, err + } + + pools, err := u.initPools(tokenList, tokenInfoList) + if err != nil { + logger. + WithFields(logger.Fields{"dex_id": dexID, "err": err}). + Error("initPools failed") + + return nil, metadataBytes, err + } + + newMetadataBytes, err := u.newMetadata(offset + batchSize) + if err != nil { + logger. + WithFields(logger.Fields{"dex_id": dexID, "err": err}). + Error("newMetadata failed") + + return nil, metadataBytes, err + } + + logger. + WithFields( + logger.Fields{ + "dex_id": dexID, + "valid_pools": len(pools), + "offset": offset, + "duration_ms": time.Since(startTime).Milliseconds(), + }, + ). + Info("Finished getting new pools") + + return pools, newMetadataBytes, nil +} + +// getAllPairsLength gets number of pairs from the factory contracts +func (u *PoolsListUpdater) getAllPairsLength(ctx context.Context) (int, error) { + var allPairsLength *big.Int + + getAllPairsLengthRequest := u.ethrpcClient.NewRequest().SetContext(ctx) + + getAllPairsLengthRequest.AddCall(ðrpc.Call{ + ABI: tokenManager2ABI, + Target: u.config.TokenManagerV2, + Method: tokenManager2TokenCountMethod, + Params: nil, + }, []interface{}{&allPairsLength}) + + if _, err := getAllPairsLengthRequest.Call(); err != nil { + return 0, err + } + + return int(allPairsLength.Int64()), nil +} + +// getOffset gets index of the last pair that is fetched +func (u *PoolsListUpdater) getOffset(metadataBytes []byte) (int, error) { + if len(metadataBytes) == 0 { + return 0, nil + } + + var metadata PoolsListUpdaterMetadata + if err := json.Unmarshal(metadataBytes, &metadata); err != nil { + return 0, err + } + + return metadata.Offset, nil +} + +// listPairAddresses lists address of pairs from offset +func (u *PoolsListUpdater) listPairs(ctx context.Context, offset int, batchSize int) ([]common.Address, []TokenInfo, error) { + listTokens := make([]common.Address, batchSize) + + req := u.ethrpcClient.NewRequest().SetContext(ctx) + for i := 0; i < batchSize; i++ { + index := big.NewInt(int64(offset + i)) + + req.AddCall(ðrpc.Call{ + ABI: tokenManager2ABI, + Target: u.config.TokenManagerV2, + Method: tokenManager2TokensMethod, + Params: []interface{}{index}, + }, []interface{}{&listTokens[i]}) + } + _, err := req.Aggregate() + if err != nil { + return nil, nil, err + } + + listQuotes := make([]TokenInfo, len(listTokens)) + + req = u.ethrpcClient.NewRequest().SetContext(ctx) + for i := range listTokens { + req.AddCall(ðrpc.Call{ + ABI: tokenManagerHelperABI, + Target: u.config.TokenManagerHelperV3, + Method: tokenManagerHelperGetTokenInfoMethod, + Params: []interface{}{listTokens[i]}, + }, []interface{}{&listQuotes[i]}) + } + + _, err = req.Aggregate() + if err != nil { + return nil, nil, err + } + + return listTokens, listQuotes, nil +} + +// initPools fetches token data and initializes pools +func (u *PoolsListUpdater) initPools(tokenList []common.Address, tokenInfoList []TokenInfo) ([]entity.Pool, error) { + pools := make([]entity.Pool, 0, len(tokenList)) + + for i := range tokenList { + token := tokenList[i].Hex() + if strings.EqualFold(token, ZERO_ADDRESS) || tokenInfoList[i].LiquidityAdded { + continue + } + + raisedToken := tokenInfoList[i].Quote.Hex() + if strings.EqualFold(raisedToken, ZERO_ADDRESS) { + raisedToken = u.config.DefaultQuoteToken + } + + extra, err := json.Marshal(&Extra{ + TradingFeeRate: tokenInfoList[i].TradingFeeRate, + }) + if err != nil { + return nil, err + } + + var newPool = entity.Pool{ + Address: strings.ToLower(tokenList[i].Hex()), + Exchange: string(valueobject.ExchangeFourMeme), + Type: DexType, + Timestamp: time.Now().Unix(), + Reserves: []string{"0", "0"}, + Tokens: []*entity.PoolToken{ + { + Address: strings.ToLower(raisedToken), + Swappable: true, + }, + { + Address: strings.ToLower(token), + Swappable: true, + }, + }, + // StaticExtra: string(staticExtra), + Extra: string(extra), + } + + pools = append(pools, newPool) + } + + return pools, nil +} + +func (u *PoolsListUpdater) newMetadata(newOffset int) ([]byte, error) { + metadata := PoolsListUpdaterMetadata{ + Offset: newOffset, + } + + metadataBytes, err := json.Marshal(metadata) + if err != nil { + return nil, err + } + + return metadataBytes, nil +} + +// getBatchSize +// @params length number of pairs (factory tracked) +// @params limit number of pairs to be fetched in one run +// @params offset index of the last pair has been fetched +// @returns batchSize +func (u *PoolsListUpdater) getBatchSize(length int, limit int, offset int) int { + if offset == length { + return 0 + } + + if offset+limit >= length { + if offset > length { + logger.WithFields(logger.Fields{ + "dex": DexType, + "offset": offset, + "length": length, + }).Warn("[getBatchSize] offset is greater than length") + } + return max(length-offset, 0) + } + + return limit +} diff --git a/pkg/liquidity-source/four-meme/pool_simulator.go b/pkg/liquidity-source/four-meme/pool_simulator.go new file mode 100644 index 000000000..5b48cac5f --- /dev/null +++ b/pkg/liquidity-source/four-meme/pool_simulator.go @@ -0,0 +1,341 @@ +package fourmeme + +import ( + "errors" + "fmt" + "math/big" + "time" + + "github.com/KyberNetwork/blockchain-toolkit/number" + "github.com/goccy/go-json" + "github.com/holiman/uint256" + "github.com/samber/lo" + + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/entity" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + utils "github.com/KyberNetwork/kyberswap-dex-lib/pkg/util/bignumber" +) + +var ( + ErrInvalidToken = errors.New("invalid token") + ErrInvalidReserve = errors.New("invalid reserve") + ErrInvalidAmountIn = errors.New("invalid amount in") + ErrInsufficientInputAmount = errors.New("INSUFFICIENT_INPUT_AMOUNT") + ErrInvalidAmountOut = errors.New("invalid amount out") + ErrTokenTxFee = errors.New("tokenTxFee must be empty to swap") + ErrTradingHalted = errors.New("trading is halted") + ErrTradingDisabled = errors.New("trading is disabled") + ErrTokenNotLaunched = errors.New("token has not been launched yet") + ErrFundsTooLow = errors.New("funds too low") + ErrInvalidTradeAmount = errors.New("error: Amount mod gwei != 0") + ErrSpendingFundsTooMuch = errors.New("spending too much funds") + ErrSmallOrderSize = errors.New("order size is too small") + ErrPriceTooLow = errors.New("price is too low") +) + +type PoolSimulator struct { + pool.Pool + gas Gas + + tradingHalted bool + tradingDisabled bool + launchTime int64 + minTradeFee *uint256.Int + tradingFeeRate *uint256.Int + tokenTxFee *uint256.Int + + f3 *uint256.Int + f10 *uint256.Int + f11 *uint256.Int +} + +var _ = pool.RegisterFactory0(DexType, NewPoolSimulator) + +func NewPoolSimulator(entityPool entity.Pool) (*PoolSimulator, error) { + var extra Extra + err := json.Unmarshal([]byte(entityPool.Extra), &extra) + if err != nil { + return nil, err + } + + var staticExtra StaticExtra + err = json.Unmarshal([]byte(entityPool.StaticExtra), &staticExtra) + if err != nil { + return nil, err + } + + p := &PoolSimulator{ + Pool: pool.Pool{Info: pool.PoolInfo{ + Address: entityPool.Address, + ReserveUsd: entityPool.ReserveUsd, + Exchange: entityPool.Exchange, + Type: entityPool.Type, + Tokens: lo.Map(entityPool.Tokens, func(item *entity.PoolToken, index int) string { return item.Address }), + Reserves: lo.Map(entityPool.Reserves, func(item string, index int) *big.Int { return utils.NewBig(item) }), + BlockNumber: entityPool.BlockNumber, + }}, + + gas: defaultGas, + } + + return p, nil +} + +func (s *PoolSimulator) CalcAmountOut(param pool.CalcAmountOutParams) (*pool.CalcAmountOutResult, error) { + var ( + tokenAmountIn = param.TokenAmountIn + tokenOut = param.TokenOut + ) + + indexIn, indexOut := s.GetTokenIndex(tokenAmountIn.Token), s.GetTokenIndex(tokenOut) + if indexIn < 0 || indexOut < 0 { + return nil, ErrInvalidToken + } + + amountIn, overflow := uint256.FromBig(tokenAmountIn.Amount) + if overflow { + return nil, ErrInvalidAmountIn + } + + if amountIn.Sign() <= 0 { + return nil, ErrInsufficientInputAmount + } + + var ( + amountOut *uint256.Int + err error + ) + + // is Buy + if indexIn == 0 { + offers, overflow := uint256.FromBig(s.Pool.Info.Reserves[1]) + if overflow { + return nil, ErrInvalidReserve + } + + amountOut, err = s.buyTokenExactIn(amountIn, offers) + } else { + amountOut, err = s.sellToken(amountIn) + } + if err != nil { + return nil, err + } + + return &pool.CalcAmountOutResult{ + TokenAmountOut: &pool.TokenAmount{Token: s.Pool.Info.Tokens[indexOut], Amount: amountOut.ToBig()}, + Fee: &pool.TokenAmount{Token: s.Pool.Info.Tokens[indexIn], Amount: ZERO.ToBig()}, + Gas: s.gas.Swap, + SwapInfo: SwapInfo{ + TradedAmount: amountIn, + }, + }, nil + +} + +func (s *PoolSimulator) CalcAmountIn(param pool.CalcAmountInParams) (*pool.CalcAmountInResult, error) { + var ( + tokenAmountOut = param.TokenAmountOut + tokenIn = param.TokenIn + ) + + indexIn, indexOut := s.GetTokenIndex(tokenIn), s.GetTokenIndex(tokenAmountOut.Token) + if indexIn < 0 || indexOut < 0 { + return nil, ErrInvalidToken + } + + if indexOut == 0 { + return nil, errors.New("this DEX doesn't support sell exact out") + } + + amountOut, overflow := uint256.FromBig(tokenAmountOut.Amount) + if overflow { + return nil, ErrInvalidAmountOut + } + + if amountOut.Cmp(number.Zero) <= 0 { + return nil, ErrInsufficientInputAmount + } + + offers, overflow := uint256.FromBig(s.Pool.Info.Reserves[1]) + if overflow { + return nil, ErrInvalidReserve + } + + amountIn, err := s.buyTokenExactOut(offers, amountOut) + if err != nil { + return nil, err + } + + return &pool.CalcAmountInResult{ + TokenAmountIn: &pool.TokenAmount{Token: s.Pool.Info.Tokens[indexIn], Amount: amountIn.ToBig()}, + Fee: &pool.TokenAmount{Token: s.Pool.Info.Tokens[indexOut], Amount: ZERO.ToBig()}, + Gas: s.gas.Swap, + SwapInfo: SwapInfo{ + TradedAmount: amountIn, + }, + }, nil + +} + +func (s *PoolSimulator) UpdateBalance(params pool.UpdateBalanceParams) { + if swapInfo, ok := params.SwapInfo.(SwapInfo); ok { + if s.GetTokenIndex(params.TokenAmountIn.Token) == 0 { + s.f11 = new(uint256.Int).Sub(s.f11, swapInfo.TradedAmount) + } else { + s.f11 = new(uint256.Int).Add(s.f11, swapInfo.TradedAmount) + } + } +} + +func (s *PoolSimulator) GetMetaInfo(_ string, _ string) interface{} { + return PoolMeta{ + BlockNumber: s.Pool.Info.BlockNumber, + } +} + +func (s *PoolSimulator) buyTokenExactOut(offers, buyAmount *uint256.Int) (*uint256.Int, error) { + return s.buyToken(ZERO, buyAmount, offers) +} + +func (s *PoolSimulator) buyTokenExactIn(funds, offers *uint256.Int) (*uint256.Int, error) { + return s.buyToken(funds, ZERO, offers) +} + +func (s *PoolSimulator) sellToken(sellAmount *uint256.Int) (amountOut *uint256.Int, err error) { + defer func() { + if r := recover(); r != nil { + if recoveredError, ok := r.(error); ok { + err = recoveredError + } else { + err = fmt.Errorf("unexpected panic: %v", r) + } + } + }() + + if s.tradingHalted { + return nil, ErrTradingHalted + } + + if time.Now().Unix() < s.launchTime { + return nil, ErrTokenNotLaunched + } + + if s.tradingDisabled { + return nil, ErrTradingDisabled + } + + if new(uint256.Int).Mod(sellAmount, EXP9).Sign() != 0 { + return nil, ErrInvalidTradeAmount + } + + if _, overflow := new(uint256.Int).AddOverflow(s.f11, sellAmount); overflow { + return nil, number.ErrOverflow + } + + fund := calcSellCost(s.f10, s.f11, sellAmount) + + tradingFee := calcTradingFee(fund, s.tradingFeeRate) + if fund.Cmp(tradingFee) <= 0 { + return nil, ErrSmallOrderSize + } + + if s.f3.Sign() != 0 && fund.Lt(s.f3) { + return nil, ErrPriceTooLow + } + + return new(uint256.Int).Sub(fund, tradingFee), nil +} + +func calcSellCost(varg0, varg1, varg2 *uint256.Int) *uint256.Int { + v0 := number.SafeMul(varg0, EXP18) + v1 := number.SafeDiv(v0, varg1) + v2 := number.SafeAdd(varg1, varg2) + v2 = number.SafeDiv(v0, v2) + + return number.SafeSub(v1, v2) +} + +func (s *PoolSimulator) buyToken(funds, offers, buyAmount *uint256.Int) (amountOut *uint256.Int, err error) { + defer func() { + if r := recover(); r != nil { + if recoveredError, ok := r.(error); ok { + err = recoveredError + } else { + err = fmt.Errorf("unexpected panic: %v", r) + } + } + }() + + if s.tradingHalted { + return nil, ErrTradingHalted + } + + if s.tokenTxFee.Sign() > 0 { + return nil, ErrTokenTxFee + } + + if time.Now().Unix() < s.launchTime { + return nil, ErrTokenNotLaunched + } + + if s.tradingDisabled { + return nil, ErrTradingDisabled + } + + var tradingFee *uint256.Int + if buyAmount.Sign() != 0 { + if funds.Sign() > 0 { + if funds.Cmp(s.minTradeFee) <= 0 { + return nil, ErrFundsTooLow + } + + fundsAfterFee := number.SafeAdd(s.tradingFeeRate, PRECISION) + fundsAfterFee = number.SafeDiv(new(uint256.Int).Mul(funds, PRECISION), fundsAfterFee) + + tradingFee = number.SafeSub(funds, fundsAfterFee) + if tradingFee.Lt(s.minTradeFee) { + fundsAfterFee = number.SafeSub(funds, s.minTradeFee) + } + + buyAmount = calcBuyAmount(s.f10, s.f11, fundsAfterFee) + } + } + + if new(uint256.Int).Mod(buyAmount, EXP9).Sign() != 0 { + return nil, ErrInvalidTradeAmount + } + + if buyAmount.Gt(offers) { + buyAmount = new(uint256.Int).Set(offers) + } + + cost := calcBuyCost(s.f10, s.f11, buyAmount) + + tradingFee = calcTradingFee(cost, s.tradingFeeRate) + + return amountOut.Add(tradingFee, cost), nil +} + +func calcBuyCost(varg0, varg1, varg2 *uint256.Int) *uint256.Int { + v0 := number.SafeMul(varg0, EXP18) + v1 := number.SafeDiv(v0, varg1) + v2 := number.SafeSub(varg1, varg2) + v2 = number.SafeDiv(v0, v2) + + return number.SafeSub(v2, v1) +} + +func calcTradingFee(tradeLiquidity, lastLiquidityTradedEMA *uint256.Int) *uint256.Int { + return number.SafeDiv(number.SafeMul(tradeLiquidity, lastLiquidityTradedEMA), PRECISION) +} + +func calcBuyAmount(amount, maxFunds, fundsAfterFee *uint256.Int) *uint256.Int { + v0 := number.SafeMul(amount, EXP18) + v1 := number.SafeDiv(v0, maxFunds) + v1 = number.SafeAdd(v1, fundsAfterFee) + v0 = number.SafeDiv(v0, v1) + + buyAmount := number.SafeSub(maxFunds, v0) + + return number.SafeSub(buyAmount, new(uint256.Int).Mod(buyAmount, EXP9)) +} diff --git a/pkg/liquidity-source/four-meme/pool_simulator_test.go b/pkg/liquidity-source/four-meme/pool_simulator_test.go new file mode 100644 index 000000000..7a5080bb8 --- /dev/null +++ b/pkg/liquidity-source/four-meme/pool_simulator_test.go @@ -0,0 +1,236 @@ +package fourmeme + +import ( + "math/big" + "testing" + + "github.com/goccy/go-json" + "github.com/holiman/uint256" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/entity" + poolpkg "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/valueobject" +) + +func createTestPoolSimulator() *PoolSimulator { + extra := Extra{ + GradThreshold: big.NewInt(0), + BuyTax: big.NewInt(5), // 5% buy tax + SellTax: big.NewInt(10), // 10% sell tax + KLast: big.NewInt(1500000), + ReserveA: big.NewInt(1500), + ReserveB: big.NewInt(1000), + } + extraBytes, _ := json.Marshal(extra) + + staticExtra := StaticExtra{ + BondingAddress: "0xF66DeA7b3e897cD44A5a231c61B6B4423d613259", + } + staticExtraBytes, _ := json.Marshal(staticExtra) + + entityPool := entity.Pool{ + Address: "0xc321c3a7f730608b51e4747b72aeb18e0a3d32c4", + Exchange: string(valueobject.ExchangeVirtualFun), + Type: DexType, + Tokens: []*entity.PoolToken{{Address: "TokenA"}, {Address: "TokenB"}}, + Reserves: []string{"1000", "1000"}, + Extra: string(extraBytes), + StaticExtra: string(staticExtraBytes), + } + + poolSimulator, err := NewPoolSimulator(entityPool) + if err != nil { + panic(err) + } + + return poolSimulator +} + +// func TestNewPoolSimulator(t *testing.T) { +// poolSimulator := createTestPoolSimulator() + +// assert.NotNil(t, poolSimulator) +// assert.Equal(t, "0xc321c3a7f730608b51e4747b72aeb18e0a3d32c4", poolSimulator.Pool.Info.Address) +// assert.Equal(t, uint256.NewInt(5), poolSimulator.buyTax) +// assert.Equal(t, uint256.NewInt(10), poolSimulator.sellTax) +// assert.Equal(t, uint256.NewInt(1500000), poolSimulator.kLast) +// assert.Equal(t, uint256.NewInt(1500), poolSimulator.reserveA) +// assert.Equal(t, uint256.NewInt(1000), poolSimulator.reserveB) +// assert.Equal(t, "0xF66DeA7b3e897cD44A5a231c61B6B4423d613259", poolSimulator.bondingAddress) + +// } + +func TestCalcAmountOut_SellToken(t *testing.T) { + poolSimulator := createTestPoolSimulator() + + amountIn, _ := uint256.FromBig(big.NewInt(187)) + params := poolpkg.CalcAmountOutParams{ + TokenAmountIn: poolpkg.TokenAmount{ + Token: "TokenA", + Amount: amountIn.ToBig(), + }, + TokenOut: "TokenB", + } + + result, err := poolSimulator.CalcAmountOut(params) + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "TokenB", result.TokenAmountOut.Token) + assert.Equal(t, big.NewInt(100), result.TokenAmountOut.Amount) +} + +func TestCalcAmountOut_BuyToken(t *testing.T) { + poolSimulator := createTestPoolSimulator() + + amountIn, _ := uint256.FromBig(big.NewInt(100)) + params := poolpkg.CalcAmountOutParams{ + TokenAmountIn: poolpkg.TokenAmount{ + Token: "TokenB", + Amount: amountIn.ToBig(), + }, + TokenOut: "TokenA", + } + + result, err := poolSimulator.CalcAmountOut(params) + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "TokenA", result.TokenAmountOut.Token) + assert.Equal(t, big.NewInt(131), result.TokenAmountOut.Amount) +} + +func TestCalcAmountIn_SellExactOut(t *testing.T) { + poolSimulator := createTestPoolSimulator() + + amountOut, _ := uint256.FromBig(big.NewInt(100)) + params := poolpkg.CalcAmountInParams{ + TokenAmountOut: poolpkg.TokenAmount{ + Token: "TokenB", + Amount: amountOut.ToBig(), + }, + TokenIn: "TokenA", + } + + result, err := poolSimulator.CalcAmountIn(params) + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "TokenA", result.TokenAmountIn.Token) + assert.Equal(t, big.NewInt(187), result.TokenAmountIn.Amount) + + // Swap back the calculated input amount and verify the output + _params := poolpkg.CalcAmountOutParams{ + TokenAmountIn: poolpkg.TokenAmount{ + Token: "TokenA", + Amount: result.TokenAmountIn.Amount, + }, + TokenOut: "TokenB", + } + + _result, err := poolSimulator.CalcAmountOut(_params) + require.NoError(t, err) + assert.NotNil(t, _result) + assert.Equal(t, "TokenB", _result.TokenAmountOut.Token) + assert.Equal(t, amountOut.ToBig(), _result.TokenAmountOut.Amount) +} + +func TestCalcAmountIn_BuyExactOut(t *testing.T) { + poolSimulator := createTestPoolSimulator() + + amountOut, _ := uint256.FromBig(big.NewInt(100)) + params := poolpkg.CalcAmountInParams{ + TokenAmountOut: poolpkg.TokenAmount{ + Token: "TokenA", + Amount: amountOut.ToBig(), + }, + TokenIn: "TokenB", + } + + result, err := poolSimulator.CalcAmountIn(params) + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "TokenB", result.TokenAmountIn.Token) + assert.Equal(t, big.NewInt(74), result.TokenAmountIn.Amount) + + // Swap back the calculated input amount and verify the output + _params := poolpkg.CalcAmountOutParams{ + TokenAmountIn: poolpkg.TokenAmount{ + Token: "TokenB", + Amount: result.TokenAmountIn.Amount, + }, + TokenOut: "TokenA", + } + + _result, err := poolSimulator.CalcAmountOut(_params) + require.NoError(t, err) + assert.NotNil(t, _result) + assert.Equal(t, "TokenA", _result.TokenAmountOut.Token) + assert.Equal(t, amountOut.ToBig(), _result.TokenAmountOut.Amount) +} + +// func TestUpdateBalance(t *testing.T) { +// poolSimulator := createTestPoolSimulator() + +// newReserveA := uint256.NewInt(1200) +// newReserveB := uint256.NewInt(800) +// swapInfo := SwapInfo{ +// IsBuy: true, +// NewReserveA: newReserveA, +// NewReserveB: newReserveB, +// NewBalanceA: newReserveA, +// NewBalanceB: newReserveB, +// } + +// params := poolpkg.UpdateBalanceParams{ +// SwapInfo: swapInfo, +// } + +// poolSimulator.UpdateBalance(params) + +// assert.Equal(t, newReserveA.ToBig(), poolSimulator.Pool.Info.Reserves[0]) +// assert.Equal(t, newReserveB.ToBig(), poolSimulator.Pool.Info.Reserves[1]) +// assert.Equal(t, newReserveA, poolSimulator.reserveA) +// assert.Equal(t, newReserveB, poolSimulator.reserveB) +// } + +func TestErrorCases(t *testing.T) { + poolSimulator := createTestPoolSimulator() + + t.Run("Insufficient Input Amount", func(t *testing.T) { + params := poolpkg.CalcAmountOutParams{ + TokenAmountIn: poolpkg.TokenAmount{ + Token: "TokenA", + Amount: big.NewInt(0), + }, + TokenOut: "TokenB", + } + + _, err := poolSimulator.CalcAmountOut(params) + assert.Error(t, err) + // assert.Equal(t, ErrInsufficientInputAmount, err) + }) + + t.Run("Insufficient Output Amount", func(t *testing.T) { + params := poolpkg.CalcAmountInParams{ + TokenAmountOut: poolpkg.TokenAmount{ + Token: "TokenA", + Amount: big.NewInt(10000), // exceeds reserveOut + }, + TokenIn: "TokenB", + } + + _, err := poolSimulator.CalcAmountIn(params) + assert.Error(t, err) + // assert.Equal(t, ErrInsufficientOutputAmount, err) + }) +} + +func TestGetMetaInfo(t *testing.T) { + poolSimulator := createTestPoolSimulator() + + metaInfo := poolSimulator.GetMetaInfo("", "") + poolMeta, ok := metaInfo.(PoolMeta) + + assert.True(t, ok) + assert.Equal(t, uint64(0), poolMeta.BlockNumber) +} diff --git a/pkg/liquidity-source/four-meme/pool_tracker.go b/pkg/liquidity-source/four-meme/pool_tracker.go new file mode 100644 index 000000000..1cca9a43e --- /dev/null +++ b/pkg/liquidity-source/four-meme/pool_tracker.go @@ -0,0 +1,220 @@ +package fourmeme + +import ( + "context" + "math/big" + "time" + + "github.com/KyberNetwork/ethrpc" + "github.com/KyberNetwork/logger" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient/gethclient" + "github.com/goccy/go-json" + + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/entity" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + pooltrack "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool/tracker" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/util/bignumber" +) + +type PoolTracker struct { + config *Config + ethrpcClient *ethrpc.Client +} + +var _ = pooltrack.RegisterFactoryCE(DexType, NewPoolTracker) + +func NewPoolTracker( + config *Config, + ethrpcClient *ethrpc.Client, +) (*PoolTracker, error) { + return &PoolTracker{ + config: config, + ethrpcClient: ethrpcClient, + }, nil +} + +func (d *PoolTracker) GetNewPoolState( + ctx context.Context, + p entity.Pool, + params pool.GetNewPoolStateParams, +) (entity.Pool, error) { + return d.getNewPoolState(ctx, p, params, nil) +} + +func (d *PoolTracker) GetNewPoolStateWithOverrides( + ctx context.Context, + p entity.Pool, + params pool.GetNewPoolStateWithOverridesParams, +) (entity.Pool, error) { + return d.getNewPoolState(ctx, p, pool.GetNewPoolStateParams{Logs: params.Logs}, params.Overrides) +} + +func (d *PoolTracker) getNewPoolState( + ctx context.Context, + p entity.Pool, + _ pool.GetNewPoolStateParams, + overrides map[common.Address]gethclient.OverrideAccount, +) (entity.Pool, error) { + if !p.Tokens[0].Swappable && !p.Tokens[1].Swappable { + return p, nil + } + + logger.WithFields(logger.Fields{"pool_id": p.Address}).Info("Started getting new pool state") + + tokenReserves, pairReserves, canPoolTradable, gradThreshold, blockNumber, err := d.getBondingData(ctx, p.Address, p.Tokens, overrides) + if err != nil { + return p, err + } + + if p.BlockNumber > blockNumber.Uint64() { + return p, nil + } + + // Disable pool : need a solution to clear these pools + if !canPoolTradable { + p.Tokens[0].Swappable = false + p.Tokens[1].Swappable = false + p.Reserves[0] = "0" + p.Reserves[1] = "0" + + return p, nil + } + + newReserves := make(entity.PoolReserves, 0, len(tokenReserves)) + for _, reserve := range tokenReserves { + if reserve == nil { + newReserves = append(newReserves, "0") + } else { + newReserves = append(newReserves, reserve.String()) + } + } + + buyTax, sellTax, kLast, err := d.getTax(ctx, p.Address, blockNumber) + if err != nil { + return p, err + } + + var extra = Extra{ + GradThreshold: gradThreshold, + SellTax: sellTax, + BuyTax: buyTax, + ReserveA: pairReserves[0], + ReserveB: pairReserves[1], + KLast: kLast, + } + + newExtra, err := json.Marshal(&extra) + if err != nil { + return p, err + } + + p.Reserves = newReserves + p.BlockNumber = blockNumber.Uint64() + p.Timestamp = time.Now().Unix() + p.Extra = string(newExtra) + + return p, nil +} + +func (d *PoolTracker) getBondingData( + ctx context.Context, + poolAddress string, + tokens []*entity.PoolToken, + overrides map[common.Address]gethclient.OverrideAccount, +) ([]*big.Int, [2]*big.Int, bool, *big.Int, *big.Int, error) { + var ( + tokenReserves = make([]*big.Int, len(tokens)) + pairReserves [2]*big.Int + gradThreshold *big.Int + tradable = true + ) + + req := d.ethrpcClient.NewRequest().SetContext(ctx) + if overrides != nil { + req.SetOverrides(overrides) + } + + // Fetch individual token balances for the pool + for i, token := range tokens { + req.AddCall(ðrpc.Call{ + ABI: erc20ABI, + Target: token.Address, + Method: erc20BalanceOfMethod, + Params: []interface{}{common.HexToAddress(poolAddress)}, + }, []interface{}{&tokenReserves[i]}) + } + + // Fetch pair reserves used for AMM calculations + req.AddCall(ðrpc.Call{ + ABI: tokenManager2ABI, + Target: poolAddress, + Method: pairGetReservesMethod, + Params: nil, + }, []interface{}{&pairReserves}) + + req.AddCall(ðrpc.Call{ + ABI: tokenManager2ABI, + Target: "", + Method: "", + Params: nil, + }, []interface{}{&gradThreshold}) + + // Call to detect if pool can tradable ? Tradable if there is an error + req.AddCall(ðrpc.Call{ + ABI: tokenManager2ABI, + Target: "", + Method: "", + Params: []interface{}{common.HexToAddress(tokens[0].Address), []common.Address{}}, + }, []interface{}{&struct{}{}}) + + resp, err := req.TryBlockAndAggregate() + if err != nil { + return nil, [2]*big.Int{}, tradable, nil, nil, err + } + + // Check the last call result + if resp.Result[len(resp.Result)-1] { + tradable = false + } + + return tokenReserves, pairReserves, tradable, gradThreshold, resp.BlockNumber, nil +} + +func (d *PoolTracker) getTax(ctx context.Context, poolAddress string, blocknumber *big.Int) (*big.Int, *big.Int, *big.Int, error) { + var ( + buyTax, sellTax = bignumber.ZeroBI, bignumber.ZeroBI + kLast = bignumber.ZeroBI + ) + + req := d.ethrpcClient.NewRequest().SetContext(ctx) + + if blocknumber != nil { + req.SetBlockNumber(blocknumber) + } + + req.AddCall(ðrpc.Call{ + ABI: tokenManager2ABI, + Target: "", + Method: "", + Params: nil, + }, []interface{}{&buyTax}) + req.AddCall(ðrpc.Call{ + ABI: tokenManager2ABI, + Target: "", + Method: "", + Params: nil, + }, []interface{}{&sellTax}) + req.AddCall(ðrpc.Call{ + ABI: tokenManager2ABI, + Target: poolAddress, + Method: pairKLastMethod, + Params: nil, + }, []interface{}{&kLast}) + + if _, err := req.Aggregate(); err != nil { + return nil, nil, nil, err + } + + return buyTax, sellTax, kLast, nil +} diff --git a/pkg/liquidity-source/four-meme/types.go b/pkg/liquidity-source/four-meme/types.go new file mode 100644 index 000000000..1f2ae4ac2 --- /dev/null +++ b/pkg/liquidity-source/four-meme/types.go @@ -0,0 +1,74 @@ +package fourmeme + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/holiman/uint256" +) + +type TokenInfo struct { + Version *big.Int `json:"version"` // The TokenManager version. If version returns 1, you should call V1 TokenManager methods for trading. If version returns 2, call V2 + TokenManager common.Address `json:"tokenManager"` // The address of the token manager which manages your token. We recommend using this address to call the TokenManager-related interfaces and parameters, replacing the hardcoded TokenManager addresses + Quote common.Address `json:"quote"` // The address of the quote token of your token. If quote returns address 0, it means the token is traded by BNB. otherwise traded by BEP20 + LastPrice *big.Int `json:"lastPrice"` // The last price of your token + TradingFeeRate *big.Int `json:"tradingFeeRate"` // The trading fee rate of your token. The actual usage of the fee rate should be the return value divided by 10,000 + MinTradingFee *big.Int `json:"minTradingFee"` // The amount of minimum trading fee + LaunchTime *big.Int `json:"launchTime"` // Launch time of the token + Offers *big.Int `json:"offers"` // Amount of tokens that are not sold + MaxOffers *big.Int `json:"maxOffers"` // Maximum amount of tokens that could be sold before creating Pancake pair + Funds *big.Int `json:"funds"` // Amount of paid BNB or BEP20 received + MaxFunds *big.Int `json:"maxFunds"` // Maximum amount of paid BNB or BEP20 that could be received + LiquidityAdded bool `json:"liquidityAdded"` // True if the Pancake pair has been created +} + +type TokenData struct { + Token common.Address `json:"token"` + RaisedToken common.Address `json:"tokenManager"` + TemplateId *big.Int `json:"templateId"` + Field3 *big.Int `json:"field3"` + MaxOffers *big.Int `json:"maxOffers"` + MaxFunds *big.Int `json:"maxFunds"` + LaunchTime *big.Int `json:"launchTime"` + Offers *big.Int `json:"offers"` + Funds *big.Int `json:"funds"` + Price *big.Int `json:"price"` + Field10 *big.Int `json:"field10"` + Field11 *big.Int `json:"field11"` + TradingDisabled *big.Int `json:"tradingDisabled"` +} + +type FeeInfo struct { + TokenTxFee *big.Int + _ *big.Int + _ *big.Int + _ *big.Int + _ *big.Int +} + +type StaticExtra struct { + BondingAddress string `json:"bondingAddress"` +} + +type Extra struct { + GradThreshold *big.Int `json:"gradThreshold"` + KLast *big.Int `json:"kLast"` + BuyTax *big.Int `json:"buyTax"` + SellTax *big.Int `json:"sellTax"` + ReserveA *big.Int `json:"reserveA"` + ReserveB *big.Int `json:"reserveB"` + TradingFeeRate *big.Int `json:"tradingFeeRate"` + LaunchTime *big.Int `json:"launchTime"` +} + +type SwapInfo struct { + TradedAmount *uint256.Int `json:"-"` +} + +type PoolMeta struct { + BlockNumber uint64 `json:"blockNumber"` +} + +type Gas struct { + Swap int64 +} diff --git a/pkg/msgpack/register_pool_types.gen.go b/pkg/msgpack/register_pool_types.gen.go index 2cc7bb810..58cf70adf 100644 --- a/pkg/msgpack/register_pool_types.gen.go +++ b/pkg/msgpack/register_pool_types.gen.go @@ -40,6 +40,7 @@ import ( pkg_liquiditysource_etherfi_weeth "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/etherfi/weeth" pkg_liquiditysource_fluid_dext1 "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/fluid/dex-t1" pkg_liquiditysource_fluid_vaultt1 "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/fluid/vault-t1" + pkg_liquiditysource_fourmeme "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/four-meme" pkg_liquiditysource_frax_sfrxeth "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/frax/sfrxeth" pkg_liquiditysource_frax_sfrxethconvertor "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/frax/sfrxeth-convertor" pkg_liquiditysource_genericsimplerate "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/generic-simple-rate" @@ -171,6 +172,7 @@ func init() { msgpack.RegisterConcreteType(&pkg_liquiditysource_etherfi_weeth.PoolSimulator{}) msgpack.RegisterConcreteType(&pkg_liquiditysource_fluid_dext1.PoolSimulator{}) msgpack.RegisterConcreteType(&pkg_liquiditysource_fluid_vaultt1.PoolSimulator{}) + msgpack.RegisterConcreteType(&pkg_liquiditysource_fourmeme.PoolSimulator{}) msgpack.RegisterConcreteType(&pkg_liquiditysource_frax_sfrxeth.PoolSimulator{}) msgpack.RegisterConcreteType(&pkg_liquiditysource_frax_sfrxethconvertor.PoolSimulator{}) msgpack.RegisterConcreteType(&pkg_liquiditysource_genericsimplerate.PoolSimulator{}) diff --git a/pkg/pooltypes/pooltypes.go b/pkg/pooltypes/pooltypes.go index d9670fa16..353e43b2f 100644 --- a/pkg/pooltypes/pooltypes.go +++ b/pkg/pooltypes/pooltypes.go @@ -36,6 +36,7 @@ import ( "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/etherfi/weeth" fluidDexT1 "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/fluid/dex-t1" fluidVaultT1 "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/fluid/vault-t1" + fourmeme "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/four-meme" "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/frax/sfrxeth" sfrxethconvertor "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/frax/sfrxeth-convertor" genericsimplerate "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/generic-simple-rate" @@ -281,6 +282,7 @@ type Types struct { UniswapV4 string OvernightUsdp string SkyPSM string + FourMeme string } var ( @@ -431,5 +433,6 @@ var ( UniswapV4: uniswapv4.DexType, OvernightUsdp: overnightusdp.DexType, SkyPSM: skypsm.DexType, + FourMeme: fourmeme.DexType, } ) diff --git a/pkg/valueobject/exchange.go b/pkg/valueobject/exchange.go index 266b06013..45fa5948e 100644 --- a/pkg/valueobject/exchange.go +++ b/pkg/valueobject/exchange.go @@ -423,6 +423,7 @@ var ( ExchangeOvernightUsdp Exchange = "overnight-usdp" ExchangeSavingsUSDS Exchange = "savings-usds" ExchangeSkyPSM Exchange = "sky-psm" + ExchangeFourMeme Exchange = "four-meme" ) var AMMSourceSet = map[Exchange]struct{}{ @@ -773,6 +774,7 @@ var AMMSourceSet = map[Exchange]struct{}{ ExchangeHoney: {}, ExchangeSavingsUSDS: {}, ExchangeSkyPSM: {}, + ExchangeFourMeme: {}, } func IsAMMSource(exchange Exchange) bool {