From 6261f8955fd6891d7602e0611d2b77f158d1627a Mon Sep 17 00:00:00 2001 From: bergben Date: Tue, 29 Oct 2024 05:01:52 +0200 Subject: [PATCH] Integrate Fluid Dex (#524) * feat(fluid): implement dexT1 pools list updater * feat(fluid): implement dexT1 pool tracker * feat(fluid): add dex math * feat(fluid): dex integration pool simulator, fixes etc. * feat(fluid): finish implementing dex logic * tests(fluid): fix vault-t1 tests * refactor(fluid): improve token comparison logic * fix(ci): run go generate * fix(fluid): fix reserves sum calculation * Update pkg/liquidity-source/fluid/dex-t1/pool_list_updater.go Co-authored-by: SunSpirit <48086732+sunspirit99@users.noreply.github.com> Signed-off-by: bergben * Update pkg/liquidity-source/fluid/dex-t1/pool_tracker.go Co-authored-by: SunSpirit <48086732+sunspirit99@users.noreply.github.com> Signed-off-by: bergben * feat(fluid): remove duplicate log * feat(fluid): add nil check on reserves * refactor(fluid): replace big.newInt(0) with bignumber.ZeroBI * refactor(fluid): make fee percent precision a constant * refactor(fluid): remove unused struct Pool * fix(fluid): compare reserves against zero correctly * refactor(fluid): make panic in math.go an error instead * refactor(fluid): make dexAmountsDecimals a constant * refactor(fluid): make 1e18 a constant * fix(valueobject): fix merge compilation issue * refactor(fluid): move logic from math.go into pool_simulator.go * feat(fluid): finalize dex protocol integration * feat(fluid-dex): change swap gas cost to 150k * feat(fluid): finalize gas cost * docs(fluid): fix comment WETH * Update pkg/liquidity-source/fluid/dex-t1/pool_list_updater.go Signed-off-by: SunSpirit <48086732+sunspirit99@users.noreply.github.com> * Update pkg/liquidity-source/fluid/dex-t1/pool_list_updater.go Signed-off-by: SunSpirit <48086732+sunspirit99@users.noreply.github.com> * Update pkg/liquidity-source/fluid/dex-t1/pool_list_updater.go Signed-off-by: SunSpirit <48086732+sunspirit99@users.noreply.github.com> * Update pkg/liquidity-source/fluid/dex-t1/constant.go Signed-off-by: SunSpirit <48086732+sunspirit99@users.noreply.github.com> * Update pkg/valueobject/exchange.go Signed-off-by: SunSpirit <48086732+sunspirit99@users.noreply.github.com> * Update pkg/pooltypes/pooltypes.go Signed-off-by: SunSpirit <48086732+sunspirit99@users.noreply.github.com> * fix(fluidVaultT1): fix failing test because of hasNative in static extra * feat(fluid): finalize fetching reserves from updated resolver * refactor(poolTypes): fix formatting * feat(fluid-dex): add checks for insufficient liquidity * feat(fluid-dex): remove reserves check in pool_list_updater * feat(fluid-dex): remove reserves check in pool_tracker * feat(fluid-dex): fix native token use in pool * feat(fluid-dex): optimize only fetch new pools * feat(fluid-dex): optimize swapRouting methods * Update pkg/liquidity-source/fluid/dex-t1/pool_list_updater.go Co-authored-by: Phu Ngo <12547020+NgoKimPhu@users.noreply.github.com> Signed-off-by: bergben * Update pkg/liquidity-source/fluid/dex-t1/pool_simulator.go Signed-off-by: SunSpirit <48086732+sunspirit99@users.noreply.github.com> --------- Signed-off-by: bergben Signed-off-by: SunSpirit <48086732+sunspirit99@users.noreply.github.com> Co-authored-by: SunSpirit <48086732+sunspirit99@users.noreply.github.com> Co-authored-by: Phu Ngo <12547020+NgoKimPhu@users.noreply.github.com> --- pkg/liquidity-source/fluid/dex-t1/abis.go | 30 + .../fluid/dex-t1/abis/ERC20.json | 222 ++++ .../dex-t1/abis/dexReservesResolver.json | 999 ++++++++++++++++++ pkg/liquidity-source/fluid/dex-t1/config.go | 9 + pkg/liquidity-source/fluid/dex-t1/constant.go | 27 + pkg/liquidity-source/fluid/dex-t1/embed.go | 9 + .../fluid/dex-t1/pool_list_updater.go | 188 ++++ .../fluid/dex-t1/pool_list_updater_test.go | 102 ++ .../fluid/dex-t1/pool_simulator.go | 527 +++++++++ .../fluid/dex-t1/pool_simulator_test.go | 456 ++++++++ .../fluid/dex-t1/pool_tracker.go | 83 ++ .../fluid/dex-t1/pool_tracker_test.go | 121 +++ pkg/liquidity-source/fluid/dex-t1/types.go | 50 + .../fluid/vault-t1/pool_list_updater_test.go | 29 +- .../fluid/vault-t1/pool_tracker_test.go | 11 +- pkg/msgpack/register_pool_types.go | 2 + pkg/pooltypes/pooltypes.go | 7 +- pkg/valueobject/exchange.go | 6 +- 18 files changed, 2859 insertions(+), 19 deletions(-) create mode 100644 pkg/liquidity-source/fluid/dex-t1/abis.go create mode 100644 pkg/liquidity-source/fluid/dex-t1/abis/ERC20.json create mode 100644 pkg/liquidity-source/fluid/dex-t1/abis/dexReservesResolver.json create mode 100644 pkg/liquidity-source/fluid/dex-t1/config.go create mode 100644 pkg/liquidity-source/fluid/dex-t1/constant.go create mode 100644 pkg/liquidity-source/fluid/dex-t1/embed.go create mode 100644 pkg/liquidity-source/fluid/dex-t1/pool_list_updater.go create mode 100644 pkg/liquidity-source/fluid/dex-t1/pool_list_updater_test.go create mode 100644 pkg/liquidity-source/fluid/dex-t1/pool_simulator.go create mode 100644 pkg/liquidity-source/fluid/dex-t1/pool_simulator_test.go create mode 100644 pkg/liquidity-source/fluid/dex-t1/pool_tracker.go create mode 100644 pkg/liquidity-source/fluid/dex-t1/pool_tracker_test.go create mode 100644 pkg/liquidity-source/fluid/dex-t1/types.go diff --git a/pkg/liquidity-source/fluid/dex-t1/abis.go b/pkg/liquidity-source/fluid/dex-t1/abis.go new file mode 100644 index 000000000..1fe6a01cb --- /dev/null +++ b/pkg/liquidity-source/fluid/dex-t1/abis.go @@ -0,0 +1,30 @@ +package dexT1 + +import ( + "bytes" + + "github.com/ethereum/go-ethereum/accounts/abi" +) + +var ( + dexReservesResolverABI abi.ABI + erc20 abi.ABI +) + +func init() { + builder := []struct { + ABI *abi.ABI + data []byte + }{ + {&dexReservesResolverABI, dexReservesResolverJSON}, + {&erc20, erc20JSON}, + } + + for _, b := range builder { + var err error + *b.ABI, err = abi.JSON(bytes.NewReader(b.data)) + if err != nil { + panic(err) + } + } +} diff --git a/pkg/liquidity-source/fluid/dex-t1/abis/ERC20.json b/pkg/liquidity-source/fluid/dex-t1/abis/ERC20.json new file mode 100644 index 000000000..405d6b364 --- /dev/null +++ b/pkg/liquidity-source/fluid/dex-t1/abis/ERC20.json @@ -0,0 +1,222 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + } +] diff --git a/pkg/liquidity-source/fluid/dex-t1/abis/dexReservesResolver.json b/pkg/liquidity-source/fluid/dex-t1/abis/dexReservesResolver.json new file mode 100644 index 000000000..44a0251af --- /dev/null +++ b/pkg/liquidity-source/fluid/dex-t1/abis/dexReservesResolver.json @@ -0,0 +1,999 @@ +[ + { + "inputs": [ + { "internalType": "address", "name": "factory_", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "FACTORY", + "outputs": [ + { + "internalType": "contract IFluidDexFactory", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "dex_", "type": "address" }, + { "internalType": "bool", "name": "swap0to1_", "type": "bool" }, + { "internalType": "uint256", "name": "amountIn_", "type": "uint256" }, + { "internalType": "uint256", "name": "amountOutMin_", "type": "uint256" } + ], + "name": "estimateSwapIn", + "outputs": [ + { "internalType": "uint256", "name": "amountOut_", "type": "uint256" } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "dex_", "type": "address" }, + { "internalType": "bool", "name": "swap0to1_", "type": "bool" }, + { "internalType": "uint256", "name": "amountOut_", "type": "uint256" }, + { "internalType": "uint256", "name": "amountInMax_", "type": "uint256" } + ], + "name": "estimateSwapOut", + "outputs": [ + { "internalType": "uint256", "name": "amountIn_", "type": "uint256" } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "getAllPoolAddresses", + "outputs": [ + { "internalType": "address[]", "name": "pools_", "type": "address[]" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAllPools", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "pool", "type": "address" }, + { "internalType": "address", "name": "token0", "type": "address" }, + { "internalType": "address", "name": "token1", "type": "address" }, + { "internalType": "uint256", "name": "fee", "type": "uint256" } + ], + "internalType": "struct Structs.Pool[]", + "name": "pools_", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAllPoolsReserves", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "pool", "type": "address" }, + { "internalType": "address", "name": "token0", "type": "address" }, + { "internalType": "address", "name": "token1", "type": "address" }, + { "internalType": "uint256", "name": "fee", "type": "uint256" }, + { + "components": [ + { + "internalType": "uint256", + "name": "token0RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0ImaginaryReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1ImaginaryReserves", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.CollateralReserves", + "name": "collateralReserves", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "token0Debt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1Debt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0ImaginaryReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1ImaginaryReserves", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.DebtReserves", + "name": "debtReserves", + "type": "tuple" + } + ], + "internalType": "struct Structs.PoolWithReserves[]", + "name": "poolsReserves_", + "type": "tuple[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getAllPoolsReservesAdjusted", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "pool", "type": "address" }, + { "internalType": "address", "name": "token0", "type": "address" }, + { "internalType": "address", "name": "token1", "type": "address" }, + { "internalType": "uint256", "name": "fee", "type": "uint256" }, + { + "components": [ + { + "internalType": "uint256", + "name": "token0RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0ImaginaryReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1ImaginaryReserves", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.CollateralReserves", + "name": "collateralReserves", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "token0Debt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1Debt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0ImaginaryReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1ImaginaryReserves", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.DebtReserves", + "name": "debtReserves", + "type": "tuple" + } + ], + "internalType": "struct Structs.PoolWithReserves[]", + "name": "poolsReserves_", + "type": "tuple[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "dex_", "type": "address" } + ], + "name": "getDexCollateralReserves", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "token0RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0ImaginaryReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1ImaginaryReserves", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.CollateralReserves", + "name": "reserves_", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "dex_", "type": "address" } + ], + "name": "getDexCollateralReservesAdjusted", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "token0RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0ImaginaryReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1ImaginaryReserves", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.CollateralReserves", + "name": "reserves_", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "dex_", "type": "address" } + ], + "name": "getDexDebtReserves", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "token0Debt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1Debt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0ImaginaryReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1ImaginaryReserves", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.DebtReserves", + "name": "reserves_", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "dex_", "type": "address" } + ], + "name": "getDexDebtReservesAdjusted", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "token0Debt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1Debt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0ImaginaryReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1ImaginaryReserves", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.DebtReserves", + "name": "reserves_", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "dex_", "type": "address" } + ], + "name": "getDexPricesAndExchangePrices", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "lastStoredPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "centerPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "upperRange", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lowerRange", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "geometricMean", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "supplyToken0ExchangePrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "borrowToken0ExchangePrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "supplyToken1ExchangePrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "borrowToken1ExchangePrice", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.PricesAndExchangePrice", + "name": "pex_", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "poolId_", "type": "uint256" } + ], + "name": "getPool", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "pool", "type": "address" }, + { "internalType": "address", "name": "token0", "type": "address" }, + { "internalType": "address", "name": "token1", "type": "address" }, + { "internalType": "uint256", "name": "fee", "type": "uint256" } + ], + "internalType": "struct Structs.Pool", + "name": "pool_", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "poolId_", "type": "uint256" } + ], + "name": "getPoolAddress", + "outputs": [ + { "internalType": "address", "name": "pool_", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pool_", "type": "address" } + ], + "name": "getPoolConstantsView", + "outputs": [ + { + "components": [ + { "internalType": "uint256", "name": "dexId", "type": "uint256" }, + { "internalType": "address", "name": "liquidity", "type": "address" }, + { "internalType": "address", "name": "factory", "type": "address" }, + { + "components": [ + { "internalType": "address", "name": "shift", "type": "address" }, + { "internalType": "address", "name": "admin", "type": "address" }, + { + "internalType": "address", + "name": "colOperations", + "type": "address" + }, + { + "internalType": "address", + "name": "debtOperations", + "type": "address" + }, + { + "internalType": "address", + "name": "perfectOperationsAndOracle", + "type": "address" + } + ], + "internalType": "struct IFluidDexT1.Implementations", + "name": "implementations", + "type": "tuple" + }, + { + "internalType": "address", + "name": "deployerContract", + "type": "address" + }, + { "internalType": "address", "name": "token0", "type": "address" }, + { "internalType": "address", "name": "token1", "type": "address" }, + { + "internalType": "bytes32", + "name": "supplyToken0Slot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "borrowToken0Slot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "supplyToken1Slot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "borrowToken1Slot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "exchangePriceToken0Slot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "exchangePriceToken1Slot", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "oracleMapping", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.ConstantViews", + "name": "constantsView_", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pool_", "type": "address" } + ], + "name": "getPoolConstantsView2", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "token0NumeratorPrecision", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0DenominatorPrecision", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1NumeratorPrecision", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1DenominatorPrecision", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.ConstantViews2", + "name": "constantsView2_", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pool_", "type": "address" } + ], + "name": "getPoolFee", + "outputs": [ + { "internalType": "uint256", "name": "fee_", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pool_", "type": "address" } + ], + "name": "getPoolReserves", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "pool", "type": "address" }, + { "internalType": "address", "name": "token0", "type": "address" }, + { "internalType": "address", "name": "token1", "type": "address" }, + { "internalType": "uint256", "name": "fee", "type": "uint256" }, + { + "components": [ + { + "internalType": "uint256", + "name": "token0RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0ImaginaryReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1ImaginaryReserves", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.CollateralReserves", + "name": "collateralReserves", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "token0Debt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1Debt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0ImaginaryReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1ImaginaryReserves", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.DebtReserves", + "name": "debtReserves", + "type": "tuple" + } + ], + "internalType": "struct Structs.PoolWithReserves", + "name": "poolReserves_", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pool_", "type": "address" } + ], + "name": "getPoolReservesAdjusted", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "pool", "type": "address" }, + { "internalType": "address", "name": "token0", "type": "address" }, + { "internalType": "address", "name": "token1", "type": "address" }, + { "internalType": "uint256", "name": "fee", "type": "uint256" }, + { + "components": [ + { + "internalType": "uint256", + "name": "token0RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0ImaginaryReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1ImaginaryReserves", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.CollateralReserves", + "name": "collateralReserves", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "token0Debt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1Debt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0ImaginaryReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1ImaginaryReserves", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.DebtReserves", + "name": "debtReserves", + "type": "tuple" + } + ], + "internalType": "struct Structs.PoolWithReserves", + "name": "poolReserves_", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pool_", "type": "address" } + ], + "name": "getPoolTokens", + "outputs": [ + { "internalType": "address", "name": "token0_", "type": "address" }, + { "internalType": "address", "name": "token1_", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address[]", "name": "pools_", "type": "address[]" } + ], + "name": "getPoolsReserves", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "pool", "type": "address" }, + { "internalType": "address", "name": "token0", "type": "address" }, + { "internalType": "address", "name": "token1", "type": "address" }, + { "internalType": "uint256", "name": "fee", "type": "uint256" }, + { + "components": [ + { + "internalType": "uint256", + "name": "token0RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0ImaginaryReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1ImaginaryReserves", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.CollateralReserves", + "name": "collateralReserves", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "token0Debt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1Debt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0ImaginaryReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1ImaginaryReserves", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.DebtReserves", + "name": "debtReserves", + "type": "tuple" + } + ], + "internalType": "struct Structs.PoolWithReserves[]", + "name": "poolsReserves_", + "type": "tuple[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address[]", "name": "pools_", "type": "address[]" } + ], + "name": "getPoolsReservesAdjusted", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "pool", "type": "address" }, + { "internalType": "address", "name": "token0", "type": "address" }, + { "internalType": "address", "name": "token1", "type": "address" }, + { "internalType": "uint256", "name": "fee", "type": "uint256" }, + { + "components": [ + { + "internalType": "uint256", + "name": "token0RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0ImaginaryReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1ImaginaryReserves", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.CollateralReserves", + "name": "collateralReserves", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "token0Debt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1Debt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1RealReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token0ImaginaryReserves", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "token1ImaginaryReserves", + "type": "uint256" + } + ], + "internalType": "struct IFluidDexT1.DebtReserves", + "name": "debtReserves", + "type": "tuple" + } + ], + "internalType": "struct Structs.PoolWithReserves[]", + "name": "poolsReserves_", + "type": "tuple[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getTotalPools", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + } +] diff --git a/pkg/liquidity-source/fluid/dex-t1/config.go b/pkg/liquidity-source/fluid/dex-t1/config.go new file mode 100644 index 000000000..e4f158861 --- /dev/null +++ b/pkg/liquidity-source/fluid/dex-t1/config.go @@ -0,0 +1,9 @@ +package dexT1 + +import "github.com/KyberNetwork/kyberswap-dex-lib/pkg/valueobject" + +type Config struct { + DexID string `json:"dexID"` + ChainID valueobject.ChainID `json:"chainID"` + DexReservesResolver string `json:"dexReservesResolver"` +} diff --git a/pkg/liquidity-source/fluid/dex-t1/constant.go b/pkg/liquidity-source/fluid/dex-t1/constant.go new file mode 100644 index 000000000..78983eeab --- /dev/null +++ b/pkg/liquidity-source/fluid/dex-t1/constant.go @@ -0,0 +1,27 @@ +package dexT1 + +import "math/big" + +const ( + DexType = "fluid-dex-t1" +) + +const ( + // DexReservesResolver methods + DRRMethodGetAllPoolsReservesAdjusted = "getAllPoolsReservesAdjusted" + DRRMethodGetPoolReservesAdjusted = "getPoolReservesAdjusted" + + // ERC20 Token methods + TokenMethodDecimals = "decimals" +) + +const ( + String1e18 = "1000000000000000000" + + DexAmountsDecimals int64 = 12 + + FeePercentPrecision int64 = 1e4 + Fee100PercentPrecision int64 = 1e6 +) + +var bI1e18, _ = new(big.Int).SetString(String1e18, 10) // 1e18 diff --git a/pkg/liquidity-source/fluid/dex-t1/embed.go b/pkg/liquidity-source/fluid/dex-t1/embed.go new file mode 100644 index 000000000..1a469bf78 --- /dev/null +++ b/pkg/liquidity-source/fluid/dex-t1/embed.go @@ -0,0 +1,9 @@ +package dexT1 + +import _ "embed" + +//go:embed abis/dexReservesResolver.json +var dexReservesResolverJSON []byte + +//go:embed abis/ERC20.json +var erc20JSON []byte diff --git a/pkg/liquidity-source/fluid/dex-t1/pool_list_updater.go b/pkg/liquidity-source/fluid/dex-t1/pool_list_updater.go new file mode 100644 index 000000000..a792e497c --- /dev/null +++ b/pkg/liquidity-source/fluid/dex-t1/pool_list_updater.go @@ -0,0 +1,188 @@ +package dexT1 + +import ( + "context" + "encoding/json" + "math/big" + "strings" + + "github.com/KyberNetwork/ethrpc" + "github.com/KyberNetwork/logger" + "github.com/ethereum/go-ethereum/common" + + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/entity" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/valueobject" +) + +type PoolsListUpdater struct { + config Config + ethrpcClient *ethrpc.Client +} + +type Metadata struct { + LastSyncPoolsLength int `json:"lastSyncPoolsLength"` +} + +func NewPoolsListUpdater(config *Config, ethrpcClient *ethrpc.Client) *PoolsListUpdater { + return &PoolsListUpdater{ + config: *config, + ethrpcClient: ethrpcClient, + } +} + +func (u *PoolsListUpdater) GetNewPools(ctx context.Context, metadataBytes []byte) ([]entity.Pool, []byte, error) { + logger.WithFields(logger.Fields{ + "dexType": DexType, + }).Infof("Start updating pools list ...") + defer func() { + logger.WithFields(logger.Fields{ + "dexType": DexType, + }).Infof("Finish updating pools list.") + }() + + allPools, err := u.getAllPools(ctx) + if err != nil { + return nil, nil, err + } + + newMetadataBytes, err := json.Marshal(Metadata{ + LastSyncPoolsLength: len(allPools), + }) + if err != nil { + return nil, nil, err + } + + var metadata Metadata + if len(metadataBytes) > 0 { + if err := json.Unmarshal(metadataBytes, &metadata); err != nil { + return nil, nil, err + } + } + + if metadata.LastSyncPoolsLength > 0 { + // only handle new pools after last synced index + allPools = allPools[metadata.LastSyncPoolsLength:] + } + + pools := make([]entity.Pool, 0) + + for _, curPool := range allPools { + + token0Decimals, token1Decimals, err := u.readTokensDecimals(ctx, curPool.Token0Address, curPool.Token1Address) + if err != nil { + return nil, nil, err + } + + staticExtraBytes, err := json.Marshal(&StaticExtra{ + DexReservesResolver: u.config.DexReservesResolver, + HasNative: strings.EqualFold(curPool.Token0Address.Hex(), valueobject.EtherAddress) || + strings.EqualFold(curPool.Token1Address.Hex(), valueobject.EtherAddress), + }) + if err != nil { + return nil, nil, err + } + + extra := PoolExtra{ + CollateralReserves: curPool.CollateralReserves, + DebtReserves: curPool.DebtReserves, + } + + extraBytes, err := json.Marshal(extra) + if err != nil { + logger.WithFields(logger.Fields{"dexType": DexType, "error": err}).Error("Error marshaling extra data") + return nil, nil, err + } + + pool := entity.Pool{ + Address: curPool.PoolAddress.String(), + Exchange: string(valueobject.ExchangeFluidDexT1), + Type: DexType, + Reserves: entity.PoolReserves{ + new(big.Int).Add(curPool.CollateralReserves.Token0RealReserves, curPool.DebtReserves.Token0RealReserves).String(), + new(big.Int).Add(curPool.CollateralReserves.Token1RealReserves, curPool.DebtReserves.Token1RealReserves).String(), + }, + Tokens: []*entity.PoolToken{ + { + Address: valueobject.WrapETHLower(curPool.Token0Address.Hex(), u.config.ChainID), + Weight: 1, + Swappable: true, + Decimals: token0Decimals, + }, + { + Address: valueobject.WrapETHLower(curPool.Token1Address.Hex(), u.config.ChainID), + Weight: 1, + Swappable: true, + Decimals: token1Decimals, + }, + }, + SwapFee: float64(curPool.Fee.Int64()) / float64(FeePercentPrecision), + Extra: string(extraBytes), + StaticExtra: string(staticExtraBytes), + } + + pools = append(pools, pool) + } + + return pools, newMetadataBytes, nil +} + +func (u *PoolsListUpdater) getAllPools(ctx context.Context) ([]PoolWithReserves, error) { + var pools []PoolWithReserves + + req := u.ethrpcClient.R().SetContext(ctx) + + req.AddCall(ðrpc.Call{ + ABI: dexReservesResolverABI, + Target: u.config.DexReservesResolver, + Method: DRRMethodGetAllPoolsReservesAdjusted, + }, []interface{}{&pools}) + + if _, err := req.Aggregate(); err != nil { + logger.WithFields(logger.Fields{ + "dexType": DexType, + "error": err, + }).Error("Failed to get all pools reserves") + return nil, err + } + + return pools, nil +} + +func (u *PoolsListUpdater) readTokensDecimals(ctx context.Context, token0 common.Address, token1 common.Address) (uint8, uint8, error) { + var decimals0, decimals1 uint8 + + req := u.ethrpcClient.R().SetContext(ctx) + + if strings.EqualFold(valueobject.EtherAddress, token0.String()) { + decimals0 = 18 + } else { + req.AddCall(ðrpc.Call{ + ABI: erc20, + Target: token0.String(), + Method: TokenMethodDecimals, + Params: nil, + }, []interface{}{&decimals0}) + } + + if strings.EqualFold(valueobject.EtherAddress, token1.String()) { + decimals1 = 18 + } else { + req.AddCall(ðrpc.Call{ + ABI: erc20, + Target: token1.String(), + Method: TokenMethodDecimals, + Params: nil, + }, []interface{}{&decimals1}) + } + + _, err := req.Aggregate() + if err != nil { + logger.WithFields(logger.Fields{ + "dexType": DexType, + "error": err, + }).Error("can not read token info") + return 0, 0, err + } + + return decimals0, decimals1, nil +} diff --git a/pkg/liquidity-source/fluid/dex-t1/pool_list_updater_test.go b/pkg/liquidity-source/fluid/dex-t1/pool_list_updater_test.go new file mode 100644 index 000000000..25f052e2f --- /dev/null +++ b/pkg/liquidity-source/fluid/dex-t1/pool_list_updater_test.go @@ -0,0 +1,102 @@ +package dexT1 + +import ( + "context" + "encoding/json" + "math/big" + "os" + "strings" + "testing" + + "github.com/KyberNetwork/ethrpc" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/entity" + "github.com/KyberNetwork/logger" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestPoolListUpdater(t *testing.T) { + _ = logger.SetLogLevel("debug") + + if os.Getenv("CI") != "" { + t.Skip() + } + + var ( + pools []entity.Pool + metadataBytes, _ = json.Marshal(map[string]interface{}{}) + err error + + config = Config{ + DexReservesResolver: "0xE8a07a32489BD9d5a00f01A55749Cf5cB854Fd13", + } + ) + + // Setup RPC server + rpcClient := ethrpc.New("https://ethereum.kyberengineering.io") + rpcClient.SetMulticallContract(common.HexToAddress("0x5ba1e12693dc8f9c48aad8770482f4739beed696")) + + pu := NewPoolsListUpdater(&config, rpcClient) + require.NotNil(t, pu) + + pools, _, err = pu.GetNewPools(context.Background(), metadataBytes) + require.NoError(t, err) + require.True(t, len(pools) >= 1) + + staticExtraBytes, _ := json.Marshal(&StaticExtra{ + DexReservesResolver: config.DexReservesResolver, + HasNative: true, + }) + + expectedPool0 := entity.Pool{ + Address: "0x0B1a513ee24972DAEf112bC777a5610d4325C9e7", + Exchange: "fluid-dex-t1", + Type: "fluid-dex-t1", + Reserves: pools[0].Reserves, + Tokens: []*entity.PoolToken{ + { + Address: strings.ToLower("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"), + Weight: 1, + Swappable: true, + Decimals: 18, + }, + { + Address: "", + Weight: 1, + Swappable: true, + Decimals: 18, + }, + }, + SwapFee: 0.01, + + Extra: pools[0].Extra, + StaticExtra: string(staticExtraBytes), + } + + require.Equal(t, expectedPool0, pools[0]) + + var extra PoolExtra + err = json.Unmarshal([]byte(pools[0].Extra), &extra) + require.NoError(t, err) + + require.NotEqual(t, "0", pools[0].Reserves[0], "Reserve should not be zero") + require.NotEqual(t, "0", pools[0].Reserves[1], "Reserve should not be zero") + + require.True(t, extra.CollateralReserves.Token0RealReserves.Cmp(big.NewInt(0)) > 0) + require.True(t, extra.CollateralReserves.Token1RealReserves.Cmp(big.NewInt(0)) > 0) + require.True(t, extra.CollateralReserves.Token0ImaginaryReserves.Cmp(big.NewInt(0)) > 0) + require.True(t, extra.CollateralReserves.Token1ImaginaryReserves.Cmp(big.NewInt(0)) > 0) + require.True(t, extra.DebtReserves.Token0Debt.Cmp(big.NewInt(0)) > 0) + require.True(t, extra.DebtReserves.Token1Debt.Cmp(big.NewInt(0)) > 0) + require.True(t, extra.DebtReserves.Token0RealReserves.Cmp(big.NewInt(0)) > 0) + require.True(t, extra.DebtReserves.Token1RealReserves.Cmp(big.NewInt(0)) > 0) + require.True(t, extra.DebtReserves.Token0ImaginaryReserves.Cmp(big.NewInt(0)) > 0) + require.True(t, extra.DebtReserves.Token1ImaginaryReserves.Cmp(big.NewInt(0)) > 0) + + // Log all pools + // for i, pool := range pools { + // jsonEncoded, _ := json.MarshalIndent(pool, "", " ") + // t.Logf("Pool %d: %s\n", i, string(jsonEncoded)) + // } + +} diff --git a/pkg/liquidity-source/fluid/dex-t1/pool_simulator.go b/pkg/liquidity-source/fluid/dex-t1/pool_simulator.go new file mode 100644 index 000000000..86aad2a52 --- /dev/null +++ b/pkg/liquidity-source/fluid/dex-t1/pool_simulator.go @@ -0,0 +1,527 @@ +package dexT1 + +import ( + "encoding/json" + "errors" + "math/big" + + "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/util/bignumber" + "github.com/samber/lo" +) + +var ( + ErrInvalidAmountIn = errors.New("invalid amountIn") + ErrInvalidAmountOut = errors.New("invalid amount out") +) + +type PoolSimulator struct { + poolpkg.Pool + + DexReservesResolver string + CollateralReserves CollateralReserves + DebtReserves DebtReserves + + Token0Decimals uint8 + Token1Decimals uint8 +} + +var ( + // Uniswap takes total gas of 125k = 21k base gas & 104k swap (this is when user has token balance) + // Fluid takes total gas of 175k = 21k base gas & 154k swap (this is when user has token balance), + // with ETH swaps costing less (because no WETH conversion) + defaultGas = Gas{Swap: 260000} +) + +func NewPoolSimulator(entityPool entity.Pool) (*PoolSimulator, error) { + var extra PoolExtra + if err := json.Unmarshal([]byte(entityPool.Extra), &extra); err != nil { + return nil, err + } + + var staticExtra StaticExtra + if err := json.Unmarshal([]byte(entityPool.StaticExtra), &staticExtra); err != nil { + return nil, err + } + + fee := new(big.Int) + fee.SetInt64(int64(entityPool.SwapFee * float64(FeePercentPrecision))) + + return &PoolSimulator{ + Pool: poolpkg.Pool{Info: poolpkg.PoolInfo{ + Address: entityPool.Address, + 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 bignumber.NewBig(item) }), + BlockNumber: entityPool.BlockNumber, + SwapFee: fee, + }}, + CollateralReserves: extra.CollateralReserves, + DebtReserves: extra.DebtReserves, + Token0Decimals: entityPool.Tokens[0].Decimals, + Token1Decimals: entityPool.Tokens[1].Decimals, + DexReservesResolver: staticExtra.DexReservesResolver, + }, nil +} + +func (s *PoolSimulator) CalcAmountOut(param poolpkg.CalcAmountOutParams) (*poolpkg.CalcAmountOutResult, error) { + if param.TokenAmountIn.Amount.Cmp(bignumber.ZeroBI) <= 0 { + return nil, ErrInvalidAmountIn + } + + swap0To1 := param.TokenAmountIn.Token == s.Info.Tokens[0] + + var tokenInDecimals, tokenOutDecimals uint8 + if swap0To1 { + tokenInDecimals = s.Token0Decimals + tokenOutDecimals = s.Token1Decimals + } else { + tokenOutDecimals = s.Token0Decimals + tokenInDecimals = s.Token1Decimals + } + + // fee is applied on token in + fee := new(big.Int).Mul(param.TokenAmountIn.Amount, s.Pool.Info.SwapFee) + fee = new(big.Int).Div(fee, big.NewInt(Fee100PercentPrecision)) + + amountInAfterFee := new(big.Int).Sub(param.TokenAmountIn.Amount, fee) + + _, tokenAmountOut, err := swapIn(swap0To1, amountInAfterFee, s.CollateralReserves, s.DebtReserves, int64(tokenInDecimals), int64(tokenOutDecimals)) + if err != nil { + return nil, err + } + + return &poolpkg.CalcAmountOutResult{ + TokenAmountOut: &poolpkg.TokenAmount{Token: param.TokenOut, Amount: tokenAmountOut}, + Fee: &poolpkg.TokenAmount{Token: param.TokenAmountIn.Token, Amount: fee}, + Gas: defaultGas.Swap, + SwapInfo: StaticExtra{ + DexReservesResolver: s.DexReservesResolver, + }, + }, nil +} + +func (s *PoolSimulator) CalcAmountIn(param poolpkg.CalcAmountInParams) (*poolpkg.CalcAmountInResult, error) { + if param.TokenAmountOut.Amount.Cmp(bignumber.ZeroBI) <= 0 { + return nil, ErrInvalidAmountOut + } + + swap0To1 := param.TokenAmountOut.Token == s.Info.Tokens[1] + + var tokenInDecimals, tokenOutDecimals uint8 + if swap0To1 { + tokenInDecimals = s.Token0Decimals + tokenOutDecimals = s.Token1Decimals + } else { + tokenOutDecimals = s.Token0Decimals + tokenInDecimals = s.Token1Decimals + } + + tokenAmountIn, _, err := swapOut(swap0To1, param.TokenAmountOut.Amount, s.CollateralReserves, s.DebtReserves, int64(tokenInDecimals), int64(tokenOutDecimals)) + if err != nil { + return nil, err + } + + // fee is applied on token in + fee := new(big.Int).Mul(tokenAmountIn, s.Pool.Info.SwapFee) + fee = new(big.Int).Div(fee, big.NewInt(Fee100PercentPrecision)) + + amountInAfterFee := new(big.Int).Add(tokenAmountIn, fee) + + return &poolpkg.CalcAmountInResult{ + TokenAmountIn: &poolpkg.TokenAmount{Token: param.TokenIn, Amount: amountInAfterFee}, + Fee: &poolpkg.TokenAmount{Token: param.TokenIn, Amount: fee}, + Gas: defaultGas.Swap, + SwapInfo: StaticExtra{ + DexReservesResolver: s.DexReservesResolver, + }, + }, nil +} + +func (t *PoolSimulator) UpdateBalance(params poolpkg.UpdateBalanceParams) { + input, output := params.TokenAmountIn, params.TokenAmountOut + var inputAmount = input.Amount + var outputAmount = output.Amount + + for i := range t.Info.Tokens { + if t.Info.Tokens[i] == input.Token { + t.Info.Reserves[i] = new(big.Int).Add(t.Info.Reserves[i], inputAmount) + } + if t.Info.Tokens[i] == output.Token { + t.Info.Reserves[i] = new(big.Int).Sub(t.Info.Reserves[i], outputAmount) + } + } +} + +func (s *PoolSimulator) GetMetaInfo(_ string, _ string) interface{} { + return PoolMeta{ + BlockNumber: s.Pool.Info.BlockNumber, + } +} + +// ------------------------------------------------------------------------------------------------ +// @dev the logic in the methods below mirrors the original Solidity code used in Dex, see +// https://github.com/Instadapp/fluid-contracts-public/tree/main/contracts/protocols/dex/poolT1 +// ------------------------------------------------------------------------------------------------ + +/** + * Given an input amount of asset and pair reserves, returns the maximum output amount of the other asset. + * @param {number} amountIn - The amount of input asset. + * @param {number} iReserveIn - Imaginary token reserve with input amount. + * @param {number} iReserveOut - Imaginary token reserve of output amount. + * @returns {number} - The maximum output amount of the other asset. + */ +func getAmountOut(amountIn *big.Int, iReserveIn *big.Int, iReserveOut *big.Int) *big.Int { + // Both numerator and denominator are scaled to 1e6 to factor in fee scaling. + numerator := new(big.Int).Mul(amountIn, iReserveOut) + denominator := new(big.Int).Add(iReserveIn, amountIn) + + // Using the swap formula: (AmountIn * iReserveY) / (iReserveX + AmountIn) + return new(big.Int).Div(numerator, denominator) +} + +/** + * Given an output amount of asset and pair reserves, returns the input amount of the other asset + * @param {number} amountOut - Desired output amount of the asset. + * @param {number} iReserveIn - Imaginary token reserve of input amount. + * @param {number} iReserveOut - Imaginary token reserve of output amount. + * @returns {number} - The input amount of the other asset. + */ +func getAmountIn(amountOut *big.Int, iReserveIn *big.Int, iReserveOut *big.Int) *big.Int { + // Both numerator and denominator are scaled to 1e6 to factor in fee scaling. + numerator := new(big.Int).Mul(amountOut, iReserveIn) + denominator := new(big.Int).Sub(iReserveOut, amountOut) + + // Using the swap formula: (AmountOut * iReserveX) / (iReserveY - AmountOut) + return new(big.Int).Div(numerator, denominator) +} + +/** + * Calculates how much of a swap should go through the collateral pool. + * @param {number} t - Total amount in. + * @param {number} x - Imaginary reserves of token out of collateral. + * @param {number} y - Imaginary reserves of token in of collateral. + * @param {number} x2 - Imaginary reserves of token out of debt. + * @param {number} y2 - Imaginary reserves of token in of debt. + * @returns {number} a - How much swap should go through collateral pool. Remaining will go from debt. + * @note If a < 0 then entire trade route through debt pool and debt pool arbitrage with col pool. + * @note If a > t then entire trade route through col pool and col pool arbitrage with debt pool. + * @note If a > 0 & a < t then swap will route through both pools. + */ +func swapRoutingIn(t *big.Int, x *big.Int, y *big.Int, x2 *big.Int, y2 *big.Int) *big.Int { + var xyRoot, x2y2Root big.Int + xyRoot.Mul(x, y).Mul(&xyRoot, bI1e18).Sqrt(&xyRoot) + x2y2Root.Mul(x2, y2).Mul(&x2y2Root, bI1e18).Sqrt(&x2y2Root) + + var tmp big.Int + numerator := new(big.Int) + numerator.Mul(y2, &xyRoot).Add(numerator, tmp.Mul(t, &xyRoot)).Sub(numerator, tmp.Mul(y, &x2y2Root)) + denominator := tmp.Add(&xyRoot, &x2y2Root) + return numerator.Div(numerator, denominator) +} + +/** + * Calculates how much of a swap should go through the collateral pool for output amount. + * @param {number} t - Total amount out. + * @param {number} x - Imaginary reserves of token in of collateral. + * @param {number} y - Imaginary reserves of token out of collateral. + * @param {number} x2 - Imaginary reserves of token in of debt. + * @param {number} y2 - Imaginary reserves of token out of debt. + * @returns {number} a - How much swap should go through collateral pool. Remaining will go from debt. + * @note If a < 0 then entire trade route through debt pool and debt pool arbitrage with col pool. + * @note If a > t then entire trade route through col pool and col pool arbitrage with debt pool. + * @note If a > 0 & a < t then swap will route through both pools. + */ +func swapRoutingOut(t *big.Int, x *big.Int, y *big.Int, x2 *big.Int, y2 *big.Int) *big.Int { + var xyRoot, x2y2Root big.Int + xyRoot.Mul(x, y).Mul(&xyRoot, bI1e18).Sqrt(&xyRoot) + x2y2Root.Mul(x2, y2).Mul(&x2y2Root, bI1e18).Sqrt(&x2y2Root) + + var tmp big.Int + numerator := new(big.Int) + numerator.Mul(t, &xyRoot).Add(numerator, tmp.Mul(y, &x2y2Root)).Sub(numerator, tmp.Mul(y2, &xyRoot)) + denominator := tmp.Add(&xyRoot, &x2y2Root) + return numerator.Div(numerator, denominator) +} + +/** + * Calculates the output amount for a given input amount in a swap operation. + * @param {boolean} swap0To1 - Direction of the swap. True if swapping token0 for token1, false otherwise. + * @param {number} amountToSwap - The amount of input token to be swapped scaled to 1e12. + * @param {Object} colReserves - The reserves of the collateral pool scaled to 1e12. + * @param {number} colReserves.token0RealReserves - Real reserves of token0 in the collateral pool. + * @param {number} colReserves.token1RealReserves - Real reserves of token1 in the collateral pool. + * @param {number} colReserves.token0ImaginaryReserves - Imaginary reserves of token0 in the collateral pool. + * @param {number} colReserves.token1ImaginaryReserves - Imaginary reserves of token1 in the collateral pool. + * @param {Object} debtReserves - The reserves of the debt pool scaled to 1e12. + * @param {number} debtReserves.token0RealReserves - Real reserves of token0 in the debt pool. + * @param {number} debtReserves.token1RealReserves - Real reserves of token1 in the debt pool. + * @param {number} debtReserves.token0ImaginaryReserves - Imaginary reserves of token0 in the debt pool. + * @param {number} debtReserves.token1ImaginaryReserves - Imaginary reserves of token1 in the debt pool. + * @returns {Object} An object containing the input amount and the calculated output amount. + * @returns {number} amountIn - The input amount. + * @returns {number} amountOut - The calculated output amount. + * @returns {error} - An error object if the operation fails. + */ +func swapInAdjusted(swap0To1 bool, amountToSwap *big.Int, colReserves CollateralReserves, debtReserves DebtReserves) (*big.Int, *big.Int, error) { + var ( + colIReserveIn, colIReserveOut, debtIReserveIn, debtIReserveOut *big.Int + colReserveOut, debtReserveOut *big.Int + ) + + if swap0To1 { + colReserveOut = colReserves.Token1RealReserves + colIReserveIn = colReserves.Token0ImaginaryReserves + colIReserveOut = colReserves.Token1ImaginaryReserves + debtReserveOut = debtReserves.Token1RealReserves + debtIReserveIn = debtReserves.Token0ImaginaryReserves + debtIReserveOut = debtReserves.Token1ImaginaryReserves + } else { + colReserveOut = colReserves.Token0RealReserves + colIReserveIn = colReserves.Token1ImaginaryReserves + colIReserveOut = colReserves.Token0ImaginaryReserves + debtReserveOut = debtReserves.Token0RealReserves + debtIReserveIn = debtReserves.Token1ImaginaryReserves + debtIReserveOut = debtReserves.Token0ImaginaryReserves + } + + // Check if all reserves of collateral pool are greater than 0 + colPoolEnabled := colReserves.Token0RealReserves.Cmp(bignumber.ZeroBI) > 0 && + colReserves.Token1RealReserves.Cmp(bignumber.ZeroBI) > 0 && + colReserves.Token0ImaginaryReserves.Cmp(bignumber.ZeroBI) > 0 && + colReserves.Token1ImaginaryReserves.Cmp(bignumber.ZeroBI) > 0 + + // Check if all reserves of debt pool are greater than 0 + debtPoolEnabled := debtReserves.Token0RealReserves.Cmp(bignumber.ZeroBI) > 0 && + debtReserves.Token1RealReserves.Cmp(bignumber.ZeroBI) > 0 && + debtReserves.Token0ImaginaryReserves.Cmp(bignumber.ZeroBI) > 0 && + debtReserves.Token1ImaginaryReserves.Cmp(bignumber.ZeroBI) > 0 + + var a *big.Int + if colPoolEnabled && debtPoolEnabled { + a = swapRoutingIn(amountToSwap, colIReserveOut, colIReserveIn, debtIReserveOut, debtIReserveIn) + } else if debtPoolEnabled { + a = big.NewInt(-1) // Route from debt pool + } else if colPoolEnabled { + a = new(big.Int).Add(amountToSwap, big.NewInt(1)) // Route from collateral pool + } else { + return nil, nil, errors.New("no pools are enabled") + } + + var amountOutCollateral, amountOutDebt *big.Int = bignumber.ZeroBI, bignumber.ZeroBI + if a.Cmp(bignumber.ZeroBI) <= 0 { + // Entire trade routes through debt pool + amountOutDebt = getAmountOut(amountToSwap, debtIReserveIn, debtIReserveOut) + } else if a.Cmp(amountToSwap) >= 0 { + // Entire trade routes through collateral pool + amountOutCollateral = getAmountOut(amountToSwap, colIReserveIn, colIReserveOut) + } else { + // Trade routes through both pools + amountOutCollateral = getAmountOut(a, colIReserveIn, colIReserveOut) + amountOutDebt = getAmountOut(new(big.Int).Sub(amountToSwap, a), debtIReserveIn, debtIReserveOut) + } + + if amountOutDebt.Cmp(debtReserveOut) > 0 { + return nil, nil, errors.New("insufficient liquidity") + } + + if amountOutCollateral.Cmp(colReserveOut) > 0 { + return nil, nil, errors.New("insufficient liquidity") + } + + return amountToSwap, new(big.Int).Add(amountOutCollateral, amountOutDebt), nil +} + +/** + * Calculates the output amount for a given input amount in a swap operation. + * @param {boolean} swap0To1 - Direction of the swap. True if swapping token0 for token1, false otherwise. + * @param {number} amountToSwap - The amount of input token to be swapped. + * @param {Object} colReserves - The reserves of the collateral pool. + * @param {number} colReserves.token0RealReserves - Real reserves of token0 in the collateral pool. + * @param {number} colReserves.token1RealReserves - Real reserves of token1 in the collateral pool. + * @param {number} colReserves.token0ImaginaryReserves - Imaginary reserves of token0 in the collateral pool. + * @param {number} colReserves.token1ImaginaryReserves - Imaginary reserves of token1 in the collateral pool. + * @param {Object} debtReserves - The reserves of the debt pool. + * @param {number} debtReserves.token0RealReserves - Real reserves of token0 in the debt pool. + * @param {number} debtReserves.token1RealReserves - Real reserves of token1 in the debt pool. + * @param {number} debtReserves.token0ImaginaryReserves - Imaginary reserves of token0 in the debt pool. + * @param {number} debtReserves.token1ImaginaryReserves - Imaginary reserves of token1 in the debt pool. + * @param {number} inDecimals - The number of decimals for the input token. + * @param {number} outDecimals - The number of decimals for the output token. + * @returns {number} amountIn - The input amount. + * @returns {number} amountOut - The calculated output amount scaled to token decimals + * @returns {error} - An error object if the operation fails. + */ +func swapIn( + swap0To1 bool, + amountIn *big.Int, + colReserves CollateralReserves, + debtReserves DebtReserves, + inDecimals int64, + outDecimals int64, +) (*big.Int, *big.Int, error) { + var amountInAdjusted *big.Int + + if inDecimals > DexAmountsDecimals { + amountInAdjusted = new(big.Int).Div(amountIn, new(big.Int).Exp(big.NewInt(10), big.NewInt(inDecimals-DexAmountsDecimals), nil)) + } else { + amountInAdjusted = new(big.Int).Mul(amountIn, new(big.Int).Exp(big.NewInt(10), big.NewInt(DexAmountsDecimals-inDecimals), nil)) + } + + _, amountOut, err := swapInAdjusted(swap0To1, amountInAdjusted, colReserves, debtReserves) + + if err != nil { + return nil, nil, err + } + + if outDecimals > DexAmountsDecimals { + amountOut = new(big.Int).Mul(amountOut, new(big.Int).Exp(big.NewInt(10), big.NewInt(outDecimals-DexAmountsDecimals), nil)) + } else { + amountOut = new(big.Int).Div(amountOut, new(big.Int).Exp(big.NewInt(10), big.NewInt(DexAmountsDecimals-outDecimals), nil)) + } + + return amountIn, amountOut, nil +} + +/** + * Calculates the input amount for a given output amount in a swap operation. + * @param {boolean} swap0to1 - Direction of the swap. True if swapping token0 for token1, false otherwise. + * @param {number} amountOut - The amount of output token to be swapped scaled to 1e12. + * @param {Object} colReserves - The reserves of the collateral pool scaled to 1e12. + * @param {number} colReserves.token0RealReserves - Real reserves of token0 in the collateral pool. + * @param {number} colReserves.token1RealReserves - Real reserves of token1 in the collateral pool. + * @param {number} colReserves.token0ImaginaryReserves - Imaginary reserves of token0 in the collateral pool. + * @param {number} colReserves.token1ImaginaryReserves - Imaginary reserves of token1 in the collateral pool. + * @param {Object} debtReserves - The reserves of the debt pool scaled to 1e12. + * @param {number} debtReserves.token0RealReserves - Real reserves of token0 in the debt pool. + * @param {number} debtReserves.token1RealReserves - Real reserves of token1 in the debt pool. + * @param {number} debtReserves.token0ImaginaryReserves - Imaginary reserves of token0 in the debt pool. + * @param {number} debtReserves.token1ImaginaryReserves - Imaginary reserves of token1 in the debt pool. + * @returns {number} amountIn - The calculated input amount required for the swap. + * @returns {number} amountOut - The specified output amount of the swap. + * @returns {error} - An error object if the operation fails. + */ +func swapOutAdjusted(swap0To1 bool, amountOut *big.Int, colReserves CollateralReserves, debtReserves DebtReserves) (*big.Int, *big.Int, error) { + var ( + colIReserveIn, colIReserveOut, debtIReserveIn, debtIReserveOut *big.Int + colReserveOut, debtReserveOut *big.Int + ) + + if swap0To1 { + colReserveOut = colReserves.Token1RealReserves + colIReserveIn = colReserves.Token0ImaginaryReserves + colIReserveOut = colReserves.Token1ImaginaryReserves + debtReserveOut = debtReserves.Token1RealReserves + debtIReserveIn = debtReserves.Token0ImaginaryReserves + debtIReserveOut = debtReserves.Token1ImaginaryReserves + } else { + colReserveOut = colReserves.Token0RealReserves + colIReserveIn = colReserves.Token1ImaginaryReserves + colIReserveOut = colReserves.Token0ImaginaryReserves + debtReserveOut = debtReserves.Token0RealReserves + debtIReserveIn = debtReserves.Token1ImaginaryReserves + debtIReserveOut = debtReserves.Token0ImaginaryReserves + } + + // Check if all reserves of collateral pool are greater than 0 + colPoolEnabled := colReserves.Token0RealReserves.Cmp(bignumber.ZeroBI) > 0 && + colReserves.Token1RealReserves.Cmp(bignumber.ZeroBI) > 0 && + colReserves.Token0ImaginaryReserves.Cmp(bignumber.ZeroBI) > 0 && + colReserves.Token1ImaginaryReserves.Cmp(bignumber.ZeroBI) > 0 + + // Check if all reserves of debt pool are greater than 0 + debtPoolEnabled := debtReserves.Token0RealReserves.Cmp(bignumber.ZeroBI) > 0 && + debtReserves.Token1RealReserves.Cmp(bignumber.ZeroBI) > 0 && + debtReserves.Token0ImaginaryReserves.Cmp(bignumber.ZeroBI) > 0 && + debtReserves.Token1ImaginaryReserves.Cmp(bignumber.ZeroBI) > 0 + + var a *big.Int + if colPoolEnabled && debtPoolEnabled { + a = swapRoutingOut(amountOut, colIReserveIn, colIReserveOut, debtIReserveIn, debtIReserveOut) + } else if debtPoolEnabled { + a = big.NewInt(-1) // Route from debt pool + } else if colPoolEnabled { + a = new(big.Int).Add(amountOut, big.NewInt(1)) // Route from collateral pool + } else { + return nil, nil, errors.New("no pools are enabled") + } + + var amountInCollateral, amountInDebt *big.Int = bignumber.ZeroBI, bignumber.ZeroBI + if a.Cmp(bignumber.ZeroBI) <= 0 { + // Entire trade routes through debt pool + amountInDebt = getAmountIn(amountOut, debtIReserveIn, debtIReserveOut) + + if amountOut.Cmp(debtReserveOut) > 0 { + return nil, nil, errors.New("insufficient liquidity") + } + } else if a.Cmp(amountOut) >= 0 { + // Entire trade routes through collateral pool + amountInCollateral = getAmountIn(amountOut, colIReserveIn, colIReserveOut) + if amountOut.Cmp(colReserveOut) > 0 { + return nil, nil, errors.New("insufficient liquidity") + } + } else { + // Trade routes through both pools + amountInCollateral = getAmountIn(a, colIReserveIn, colIReserveOut) + amountInDebt = getAmountIn(new(big.Int).Sub(amountOut, a), debtIReserveIn, debtIReserveOut) + + if new(big.Int).Sub(amountOut, a).Cmp(debtReserveOut) > 0 || a.Cmp(debtReserveOut) > 0 { + return nil, nil, errors.New("insufficient liquidity") + } + } + + return new(big.Int).Add(amountInCollateral, amountInDebt), amountOut, nil +} + +/** + * Calculates the input amount for a given output amount in a swap operation. + * @param {boolean} swap0to1 - Direction of the swap. True if swapping token0 for token1, false otherwise. + * @param {number} amountOut - The amount of output token to be swapped. + * @param {Object} colReserves - The reserves of the collateral pool. + * @param {number} colReserves.token0RealReserves - Real reserves of token0 in the collateral pool. + * @param {number} colReserves.token1RealReserves - Real reserves of token1 in the collateral pool. + * @param {number} colReserves.token0ImaginaryReserves - Imaginary reserves of token0 in the collateral pool. + * @param {number} colReserves.token1ImaginaryReserves - Imaginary reserves of token1 in the collateral pool. + * @param {Object} debtReserves - The reserves of the debt pool. + * @param {number} debtReserves.token0RealReserves - Real reserves of token0 in the debt pool. + * @param {number} debtReserves.token1RealReserves - Real reserves of token1 in the debt pool. + * @param {number} debtReserves.token0ImaginaryReserves - Imaginary reserves of token0 in the debt pool. + * @param {number} debtReserves.token1ImaginaryReserves - Imaginary reserves of token1 in the debt pool. + * @param {number} inDecimals - The number of decimals for the input token. + * @param {number} outDecimals - The number of decimals for the output token. + * @returns {number} amountIn - The calculated input amount required for the swap scaled to token decimals. + * @returns {number} amountOut - The specified output amount of the swap. + * @returns {error} - An error object if the operation fails. + */ +func swapOut( + swap0To1 bool, + amountOut *big.Int, + colReserves CollateralReserves, + debtReserves DebtReserves, + inDecimals int64, + outDecimals int64, +) (*big.Int, *big.Int, error) { + var amountOutAdjusted *big.Int + + if outDecimals > DexAmountsDecimals { + amountOutAdjusted = new(big.Int).Div(amountOut, new(big.Int).Exp(big.NewInt(10), big.NewInt(outDecimals-DexAmountsDecimals), nil)) + } else { + amountOutAdjusted = new(big.Int).Mul(amountOut, new(big.Int).Exp(big.NewInt(10), big.NewInt(DexAmountsDecimals-outDecimals), nil)) + } + + amountIn, _, err := swapOutAdjusted(swap0To1, amountOutAdjusted, colReserves, debtReserves) + + if err != nil { + return nil, nil, err + } + + if inDecimals > DexAmountsDecimals { + amountIn = new(big.Int).Mul(amountIn, new(big.Int).Exp(big.NewInt(10), big.NewInt(inDecimals-DexAmountsDecimals), nil)) + } else { + amountIn = new(big.Int).Div(amountIn, new(big.Int).Exp(big.NewInt(10), big.NewInt(DexAmountsDecimals-inDecimals), nil)) + } + + return amountIn, amountOut, nil +} diff --git a/pkg/liquidity-source/fluid/dex-t1/pool_simulator_test.go b/pkg/liquidity-source/fluid/dex-t1/pool_simulator_test.go new file mode 100644 index 000000000..58c3a0ccf --- /dev/null +++ b/pkg/liquidity-source/fluid/dex-t1/pool_simulator_test.go @@ -0,0 +1,456 @@ +package dexT1 + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + poolpkg "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/util/bignumber" +) + +func TestPoolSimulator_CalcAmountOut(t *testing.T) { + testCases := []struct { + name string + poolSimulator *PoolSimulator + param poolpkg.CalcAmountOutParams + expectedAmountOut *big.Int + expectedError error + }{ + { + name: "it should return correct amount", + poolSimulator: &PoolSimulator{ + Pool: poolpkg.Pool{ + Info: poolpkg.PoolInfo{ + Address: "0x0B1a513ee24972DAEf112bC777a5610d4325C9e7", + Exchange: "fluid-dex-t1", + Type: "fluid-dex-t1", + Tokens: []string{"0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"}, + Reserves: []*big.Int{bignumber.NewBig("18760613183894"), bignumber.NewBig("22123580158026")}, + BlockNumber: 20836530, + SwapFee: bignumber.NewBig("100"), + }, + }, + CollateralReserves: CollateralReserves{ + Token0RealReserves: bignumber.NewBig("2169934539358"), + Token1RealReserves: bignumber.NewBig("19563846299171"), + Token0ImaginaryReserves: bignumber.NewBig("62490032619260838"), + Token1ImaginaryReserves: bignumber.NewBig("73741038977020279"), + }, + DebtReserves: DebtReserves{ + Token0Debt: bignumber.NewBig("16590678644536"), + Token1Debt: bignumber.NewBig("2559733858855"), + Token0RealReserves: bignumber.NewBig("2169108220421"), + Token1RealReserves: bignumber.NewBig("19572550738602"), + Token0ImaginaryReserves: bignumber.NewBig("62511862774117387"), + Token1ImaginaryReserves: bignumber.NewBig("73766803277429176"), + }, + Token0Decimals: 18, + Token1Decimals: 18, + }, + param: poolpkg.CalcAmountOutParams{ + TokenAmountIn: poolpkg.TokenAmount{ + Amount: bignumber.NewBig("1000000000000000000"), // 1 wstETH + Token: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + }, + TokenOut: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, + // expected amount without fee see math_test.go 1180035404724000000 + // for resolver estimateSwapIn result at very similar reserves values (hardcoded reserves above taken some blocks before). + // resolver says estimateSwapIn result should be 1179917367073000000 + // we get here incl. fee 0.01% -> 1179917402128000000. + expectedAmountOut: bignumber.NewBig("1179917402128000000"), + }, + { + name: "it should return correct amount for 0.5 wstETH", + poolSimulator: &PoolSimulator{ + Pool: poolpkg.Pool{ + Info: poolpkg.PoolInfo{ + Address: "0x0B1a513ee24972DAEf112bC777a5610d4325C9e7", + Exchange: "fluid-dex-t1", + Type: "fluid-dex-t1", + Tokens: []string{"0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"}, + Reserves: []*big.Int{bignumber.NewBig("18760613183894"), bignumber.NewBig("22123580158026")}, + BlockNumber: 20836530, + SwapFee: bignumber.NewBig("100"), + }, + }, + CollateralReserves: CollateralReserves{ + Token0RealReserves: bignumber.NewBig("2169934539358"), + Token1RealReserves: bignumber.NewBig("19563846299171"), + Token0ImaginaryReserves: bignumber.NewBig("62490032619260838"), + Token1ImaginaryReserves: bignumber.NewBig("73741038977020279"), + }, + DebtReserves: DebtReserves{ + Token0Debt: bignumber.NewBig("16590678644536"), + Token1Debt: bignumber.NewBig("2559733858855"), + Token0RealReserves: bignumber.NewBig("2169108220421"), + Token1RealReserves: bignumber.NewBig("19572550738602"), + Token0ImaginaryReserves: bignumber.NewBig("62511862774117387"), + Token1ImaginaryReserves: bignumber.NewBig("73766803277429176"), + }, + Token0Decimals: 18, + Token1Decimals: 18, + }, + param: poolpkg.CalcAmountOutParams{ + TokenAmountIn: poolpkg.TokenAmount{ + Amount: bignumber.NewBig("500000000000000000"), // 0.5 wstETH + Token: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + }, + TokenOut: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, + // approximately expected: + // 1179917402128000000 / 2 = + // 589958701064000000 + expectedAmountOut: bignumber.NewBig("589961060629000000"), + }, + { + name: "it should return correct amount for 0.8 ETH", + poolSimulator: &PoolSimulator{ + Pool: poolpkg.Pool{ + Info: poolpkg.PoolInfo{ + Address: "0x0B1a513ee24972DAEf112bC777a5610d4325C9e7", + Exchange: "fluid-dex-t1", + Type: "fluid-dex-t1", + Tokens: []string{"0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"}, + Reserves: []*big.Int{bignumber.NewBig("18760613183894"), bignumber.NewBig("22123580158026")}, + BlockNumber: 20836530, + SwapFee: bignumber.NewBig("100"), + }, + }, + CollateralReserves: CollateralReserves{ + Token0RealReserves: bignumber.NewBig("2169934539358"), + Token1RealReserves: bignumber.NewBig("19563846299171"), + Token0ImaginaryReserves: bignumber.NewBig("62490032619260838"), + Token1ImaginaryReserves: bignumber.NewBig("73741038977020279"), + }, + DebtReserves: DebtReserves{ + Token0Debt: bignumber.NewBig("16590678644536"), + Token1Debt: bignumber.NewBig("2559733858855"), + Token0RealReserves: bignumber.NewBig("2169108220421"), + Token1RealReserves: bignumber.NewBig("19572550738602"), + Token0ImaginaryReserves: bignumber.NewBig("62511862774117387"), + Token1ImaginaryReserves: bignumber.NewBig("73766803277429176"), + }, + Token0Decimals: 18, + Token1Decimals: 18, + }, + param: poolpkg.CalcAmountOutParams{ + TokenAmountIn: poolpkg.TokenAmount{ + Amount: bignumber.NewBig("800000000000000000"), // 0.8 ETH + Token: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, + TokenOut: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + }, + // for approximate check expected value check: + // if 1e18 wstETH from test above results in 1179917402128000000 ETH + // then for 1 ETH we should get 0.847516951776864996 WSTETH + // and following for 0.8 ETH 0.678013561421491997. + expectedAmountOut: bignumber.NewBig("677868867152000000"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := tc.poolSimulator.CalcAmountOut(tc.param) + + if tc.expectedError != nil { + assert.ErrorIs(t, err, tc.expectedError) + } + + t.Logf("Expected Amount Out: %s", tc.expectedAmountOut.String()) + t.Logf("Result Amount: %s", result.TokenAmountOut.Amount.String()) + t.Logf("Fee Amount: %s", result.Fee.Amount.String()) + + if tc.expectedAmountOut != nil { + assert.Zero(t, tc.expectedAmountOut.Cmp(result.TokenAmountOut.Amount)) + } + }) + } +} + +func TestPoolSimulator_CalcAmountIn(t *testing.T) { + testCases := []struct { + name string + poolSimulator *PoolSimulator + param poolpkg.CalcAmountInParams + expectedAmountIn *big.Int + expectedError error + }{ + { + name: "it should return correct amount", + poolSimulator: &PoolSimulator{ + Pool: poolpkg.Pool{ + Info: poolpkg.PoolInfo{ + Address: "0x0B1a513ee24972DAEf112bC777a5610d4325C9e7", + Exchange: "fluid-dex-t1", + Type: "fluid-dex-t1", + Tokens: []string{"0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"}, + Reserves: []*big.Int{bignumber.NewBig("18760613183894"), bignumber.NewBig("22123580158026")}, + BlockNumber: 20836530, + SwapFee: bignumber.NewBig("100"), + }, + }, + CollateralReserves: CollateralReserves{ + Token0RealReserves: bignumber.NewBig("2169934539358"), + Token1RealReserves: bignumber.NewBig("19563846299171"), + Token0ImaginaryReserves: bignumber.NewBig("62490032619260838"), + Token1ImaginaryReserves: bignumber.NewBig("73741038977020279"), + }, + DebtReserves: DebtReserves{ + Token0Debt: bignumber.NewBig("16590678644536"), + Token1Debt: bignumber.NewBig("2559733858855"), + Token0RealReserves: bignumber.NewBig("2169108220421"), + Token1RealReserves: bignumber.NewBig("19572550738602"), + Token0ImaginaryReserves: bignumber.NewBig("62511862774117387"), + Token1ImaginaryReserves: bignumber.NewBig("73766803277429176"), + }, + Token0Decimals: 18, + Token1Decimals: 18, + }, + param: poolpkg.CalcAmountInParams{ + TokenAmountOut: poolpkg.TokenAmount{ + Amount: bignumber.NewBig("1179917402128000000"), + Token: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, + TokenIn: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + }, + // expected very close to 1000000000000000000 + expectedAmountIn: bignumber.NewBig("999999989997999800"), + }, + { + name: "it should return correct amount for 0.5 wstETH", + poolSimulator: &PoolSimulator{ + Pool: poolpkg.Pool{ + Info: poolpkg.PoolInfo{ + Address: "0x0B1a513ee24972DAEf112bC777a5610d4325C9e7", + Exchange: "fluid-dex-t1", + Type: "fluid-dex-t1", + Tokens: []string{"0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"}, + Reserves: []*big.Int{bignumber.NewBig("18760613183894"), bignumber.NewBig("22123580158026")}, + BlockNumber: 20836530, + SwapFee: bignumber.NewBig("100"), + }, + }, + CollateralReserves: CollateralReserves{ + Token0RealReserves: bignumber.NewBig("2169934539358"), + Token1RealReserves: bignumber.NewBig("19563846299171"), + Token0ImaginaryReserves: bignumber.NewBig("62490032619260838"), + Token1ImaginaryReserves: bignumber.NewBig("73741038977020279"), + }, + DebtReserves: DebtReserves{ + Token0Debt: bignumber.NewBig("16590678644536"), + Token1Debt: bignumber.NewBig("2559733858855"), + Token0RealReserves: bignumber.NewBig("2169108220421"), + Token1RealReserves: bignumber.NewBig("19572550738602"), + Token0ImaginaryReserves: bignumber.NewBig("62511862774117387"), + Token1ImaginaryReserves: bignumber.NewBig("73766803277429176"), + }, + Token0Decimals: 18, + Token1Decimals: 18, + }, + param: poolpkg.CalcAmountInParams{ + TokenAmountOut: poolpkg.TokenAmount{ + Amount: bignumber.NewBig("589961060629000000"), + Token: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, + TokenIn: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + }, + // expected very close to 500000000000000000 + expectedAmountIn: bignumber.NewBig("499999994997999800"), + }, + { + name: "it should return correct amount for 0.8 ETH", + poolSimulator: &PoolSimulator{ + Pool: poolpkg.Pool{ + Info: poolpkg.PoolInfo{ + Address: "0x0B1a513ee24972DAEf112bC777a5610d4325C9e7", + Exchange: "fluid-dex-t1", + Type: "fluid-dex-t1", + Tokens: []string{"0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"}, + Reserves: []*big.Int{bignumber.NewBig("18760613183894"), bignumber.NewBig("22123580158026")}, + BlockNumber: 20836530, + SwapFee: bignumber.NewBig("100"), + }, + }, + CollateralReserves: CollateralReserves{ + Token0RealReserves: bignumber.NewBig("2169934539358"), + Token1RealReserves: bignumber.NewBig("19563846299171"), + Token0ImaginaryReserves: bignumber.NewBig("62490032619260838"), + Token1ImaginaryReserves: bignumber.NewBig("73741038977020279"), + }, + DebtReserves: DebtReserves{ + Token0Debt: bignumber.NewBig("16590678644536"), + Token1Debt: bignumber.NewBig("2559733858855"), + Token0RealReserves: bignumber.NewBig("2169108220421"), + Token1RealReserves: bignumber.NewBig("19572550738602"), + Token0ImaginaryReserves: bignumber.NewBig("62511862774117387"), + Token1ImaginaryReserves: bignumber.NewBig("73766803277429176"), + }, + Token0Decimals: 18, + Token1Decimals: 18, + }, + param: poolpkg.CalcAmountInParams{ + TokenAmountOut: poolpkg.TokenAmount{ + Amount: bignumber.NewBig("677868867152000000"), + Token: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + }, + TokenIn: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, + // expected very close to 800000000000000000 + expectedAmountIn: bignumber.NewBig("799999991997999800"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := tc.poolSimulator.CalcAmountIn(tc.param) + + if tc.expectedError != nil { + assert.ErrorIs(t, err, tc.expectedError) + } + + t.Logf("Expected Amount In: %s", tc.expectedAmountIn.String()) + t.Logf("Result Amount: %s", result.TokenAmountIn.Amount.String()) + t.Logf("Fee Amount: %s", result.Fee.Amount.String()) + + if tc.expectedAmountIn != nil { + assert.Zero(t, tc.expectedAmountIn.Cmp(result.TokenAmountIn.Amount)) + } + }) + } +} + +var colReservesOne = CollateralReserves{ + Token0RealReserves: big.NewInt(20000000006000000), + Token1RealReserves: big.NewInt(20000000000500000), + Token0ImaginaryReserves: big.NewInt(389736659726997981), + Token1ImaginaryReserves: big.NewInt(389736659619871949), +} + +var colReservesEmpty = CollateralReserves{ + Token0RealReserves: big.NewInt(0), + Token1RealReserves: big.NewInt(0), + Token0ImaginaryReserves: big.NewInt(0), + Token1ImaginaryReserves: big.NewInt(0), +} + +var debtReservesEmpty = DebtReserves{ + Token0RealReserves: big.NewInt(0), + Token1RealReserves: big.NewInt(0), + Token0ImaginaryReserves: big.NewInt(0), + Token1ImaginaryReserves: big.NewInt(0), +} + +var debtReservesOne = DebtReserves{ + Token0RealReserves: big.NewInt(9486832995556050), + Token1RealReserves: big.NewInt(9486832993079885), + Token0ImaginaryReserves: big.NewInt(184868330099560759), + Token1ImaginaryReserves: big.NewInt(184868330048879109), +} + +func assertSwapInResult(t *testing.T, expected bool, amountIn *big.Int, colReserves CollateralReserves, debtReserves DebtReserves, expectedAmountIn string, expectedAmountOut string) { + inAmt, outAmt, _ := swapInAdjusted(expected, amountIn, colReserves, debtReserves) + + require.Equal(t, expectedAmountIn, inAmt.String()) + require.Equal(t, expectedAmountOut, outAmt.String()) +} + +func assertSwapOutResult(t *testing.T, expected bool, amountOut *big.Int, colReserves CollateralReserves, debtReserves DebtReserves, expectedAmountIn string, expectedAmountOut string) { + inAmt, outAmt, _ := swapOutAdjusted(expected, amountOut, colReserves, debtReserves) + + require.Equal(t, expectedAmountIn, inAmt.String()) + require.Equal(t, expectedAmountOut, outAmt.String()) +} + +func TestPoolSimulator_SwapIn(t *testing.T) { + t.Run("TestPoolSimulator_SwapIn", func(t *testing.T) { + assertSwapInResult(t, true, big.NewInt(1e15), colReservesOne, debtReservesOne, "1000000000000000", "998262697204710") + assertSwapInResult(t, true, big.NewInt(1e15), colReservesEmpty, debtReservesOne, "1000000000000000", "994619847016724") + assertSwapInResult(t, true, big.NewInt(1e15), colReservesOne, debtReservesEmpty, "1000000000000000", "997440731289905") + assertSwapInResult(t, false, big.NewInt(1e15), colReservesOne, debtReservesOne, "1000000000000000", "998262697752553") + assertSwapInResult(t, false, big.NewInt(1e15), colReservesEmpty, debtReservesOne, "1000000000000000", "994619847560607") + assertSwapInResult(t, false, big.NewInt(1e15), colReservesOne, debtReservesEmpty, "1000000000000000", "997440731837532") + }) +} +func TestPoolSimulator_SwapInCompareEstimateIn(t *testing.T) { + t.Run("TestPoolSimulator_SwapInCompareEstimateIn", func(t *testing.T) { + expectedAmountIn := "1000000000000000000" + expectedAmountOut := "1180035404724000000" + + colReserves := CollateralReserves{ + Token0RealReserves: big.NewInt(2169934539358), + Token1RealReserves: big.NewInt(19563846299171), + Token0ImaginaryReserves: big.NewInt(62490032619260838), + Token1ImaginaryReserves: big.NewInt(73741038977020279), + } + debtReserves := DebtReserves{ + Token0Debt: big.NewInt(16590678644536), + Token1Debt: big.NewInt(2559733858855), + Token0RealReserves: big.NewInt(2169108220421), + Token1RealReserves: big.NewInt(19572550738602), + Token0ImaginaryReserves: big.NewInt(62511862774117387), + Token1ImaginaryReserves: big.NewInt(73766803277429176), + } + + amountIn := big.NewInt(1e12) + inAmt, outAmt, _ := swapInAdjusted(true, amountIn, colReserves, debtReserves) + + require.Equal(t, expectedAmountIn, big.NewInt(0).Mul(inAmt, big.NewInt(1e6)).String()) + require.Equal(t, expectedAmountOut, big.NewInt(0).Mul(outAmt, big.NewInt(1e6)).String()) + + // swapIn should do the conversion for token decimals + _, outAmtSwapIn, _ := swapIn(true, big.NewInt(1e18), colReserves, debtReserves, 18, 18) + require.Equal(t, expectedAmountOut, outAmtSwapIn.String()) + }) +} + +func TestPoolSimulator_SwapOut(t *testing.T) { + t.Run("TestPoolSimulator_SwapOut", func(t *testing.T) { + assertSwapOutResult(t, true, big.NewInt(1e15), colReservesOne, debtReservesOne, "1001743360284199", "1000000000000000") + assertSwapOutResult(t, true, big.NewInt(1e15), colReservesEmpty, debtReservesOne, "1005438674786548", "1000000000000000") + assertSwapOutResult(t, true, big.NewInt(1e15), colReservesOne, debtReservesEmpty, "1002572435818386", "1000000000000000") + assertSwapOutResult(t, false, big.NewInt(1e15), colReservesOne, debtReservesOne, "1001743359733488", "1000000000000000") + assertSwapOutResult(t, false, big.NewInt(1e15), colReservesEmpty, debtReservesOne, "1005438674233767", "1000000000000000") + assertSwapOutResult(t, false, big.NewInt(1e15), colReservesOne, debtReservesEmpty, "1002572435266527", "1000000000000000") + }) +} + +func TestPoolSimulator_SwapInOut(t *testing.T) { + t.Run("TestPoolSimulator_SwapInOut", func(t *testing.T) { + assertSwapInResult(t, true, big.NewInt(1e15), colReservesOne, debtReservesOne, "1000000000000000", "998262697204710") + + assertSwapOutResult(t, true, big.NewInt(998262697204710), colReservesOne, debtReservesOne, "999999999999998", "998262697204710") + + assertSwapInResult(t, false, big.NewInt(1e15), colReservesOne, debtReservesOne, "1000000000000000", "998262697752553") + + assertSwapOutResult(t, false, big.NewInt(998262697752553), colReservesOne, debtReservesOne, "999999999999998", "998262697752553") + }) +} + +func TestPoolSimulator_SwapInOutDebtEmpty(t *testing.T) { + t.Run("TestPoolSimulator_SwapInOutDebtEmpty", func(t *testing.T) { + assertSwapInResult(t, true, big.NewInt(1e15), colReservesEmpty, debtReservesOne, "1000000000000000", "994619847016724") + + assertSwapOutResult(t, true, big.NewInt(994619847016724), colReservesEmpty, debtReservesOne, "999999999999999", "994619847016724") + + assertSwapInResult(t, false, big.NewInt(1e15), colReservesEmpty, debtReservesOne, "1000000000000000", "994619847560607") + + assertSwapOutResult(t, false, big.NewInt(994619847560607), colReservesEmpty, debtReservesOne, "999999999999999", "994619847560607") + }) + +} + +func TestPoolSimulator_SwapInOutColEmpty(t *testing.T) { + t.Run("TestPoolSimulator_SwapInOutColEmpty", func(t *testing.T) { + assertSwapInResult(t, true, big.NewInt(1e15), colReservesOne, debtReservesEmpty, "1000000000000000", "997440731289905") + + assertSwapOutResult(t, true, big.NewInt(997440731289905), colReservesOne, debtReservesEmpty, "999999999999999", "997440731289905") + + assertSwapInResult(t, false, big.NewInt(1e15), colReservesOne, debtReservesEmpty, "1000000000000000", "997440731837532") + + assertSwapOutResult(t, false, big.NewInt(997440731837532), colReservesOne, debtReservesEmpty, "999999999999999", "997440731837532") + }) +} diff --git a/pkg/liquidity-source/fluid/dex-t1/pool_tracker.go b/pkg/liquidity-source/fluid/dex-t1/pool_tracker.go new file mode 100644 index 000000000..4f277b6a6 --- /dev/null +++ b/pkg/liquidity-source/fluid/dex-t1/pool_tracker.go @@ -0,0 +1,83 @@ +package dexT1 + +import ( + "context" + "encoding/json" + "math/big" + "time" + + "github.com/KyberNetwork/logger" + + "github.com/KyberNetwork/ethrpc" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/entity" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/ethereum/go-ethereum/common" +) + +type PoolTracker struct { + config Config + ethrpcClient *ethrpc.Client +} + +func NewPoolTracker(config *Config, ethrpcClient *ethrpc.Client) *PoolTracker { + return &PoolTracker{ + config: *config, + ethrpcClient: ethrpcClient, + } +} + +func (t *PoolTracker) GetNewPoolState( + ctx context.Context, + p entity.Pool, + _ pool.GetNewPoolStateParams, +) (entity.Pool, error) { + poolReserves, blockNumber, err := t.getPoolReserves(ctx, p.Address) + if err != nil { + return p, err + } + + extra := PoolExtra{ + CollateralReserves: poolReserves.CollateralReserves, + DebtReserves: poolReserves.DebtReserves, + } + + extraBytes, err := json.Marshal(extra) + if err != nil { + logger.WithFields(logger.Fields{"dexType": DexType, "error": err}).Error("Error marshaling extra data") + return p, err + } + + p.SwapFee = float64(poolReserves.Fee.Int64()) / float64(FeePercentPrecision) + p.Extra = string(extraBytes) + p.BlockNumber = blockNumber + p.Timestamp = time.Now().Unix() + p.Reserves = entity.PoolReserves{ + new(big.Int).Add(poolReserves.CollateralReserves.Token0RealReserves, poolReserves.DebtReserves.Token0RealReserves).String(), + new(big.Int).Add(poolReserves.CollateralReserves.Token1RealReserves, poolReserves.DebtReserves.Token1RealReserves).String(), + } + + return p, nil +} + +func (t *PoolTracker) getPoolReserves(ctx context.Context, poolAddress string) (*PoolWithReserves, uint64, error) { + pool := &PoolWithReserves{} + + req := t.ethrpcClient.R().SetContext(ctx) + req.AddCall(ðrpc.Call{ + ABI: dexReservesResolverABI, + Target: t.config.DexReservesResolver, + Method: DRRMethodGetPoolReservesAdjusted, + Params: []interface{}{common.HexToAddress(poolAddress)}, + }, []interface{}{&pool}) + + resp, err := req.Aggregate() + if err != nil { + logger.WithFields(logger.Fields{ + "dexType": DexType, + "error": err, + }).Error("Failed to get pool reserves") + return nil, 0, err + } + + return pool, resp.BlockNumber.Uint64(), nil +} diff --git a/pkg/liquidity-source/fluid/dex-t1/pool_tracker_test.go b/pkg/liquidity-source/fluid/dex-t1/pool_tracker_test.go new file mode 100644 index 000000000..3b24f922b --- /dev/null +++ b/pkg/liquidity-source/fluid/dex-t1/pool_tracker_test.go @@ -0,0 +1,121 @@ +package dexT1 + +import ( + "context" + "encoding/json" + "math/big" + "os" + "testing" + + "github.com/KyberNetwork/ethrpc" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/entity" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/valueobject" + "github.com/KyberNetwork/logger" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestPoolTracker(t *testing.T) { + _ = logger.SetLogLevel("debug") + + if os.Getenv("CI") != "" { + t.Skip() + } + + var ( + config = Config{ + DexReservesResolver: "0xE8a07a32489BD9d5a00f01A55749Cf5cB854Fd13", + } + ) + + logger.Debugf("Starting TestPoolTracker with config: %+v", config) + + client := ethrpc.New("https://ethereum.kyberengineering.io") + client.SetMulticallContract(common.HexToAddress("0x5ba1e12693dc8f9c48aad8770482f4739beed696")) + + poolTracker := NewPoolTracker(&config, client) + require.NotNil(t, poolTracker) + logger.Debugf("PoolTracker initialized: %+v", poolTracker) + + t.Run("wstETH_ETH_Pool", func(t *testing.T) { + poolAddr := "0x0B1a513ee24972DAEf112bC777a5610d4325C9e7" + + staticExtraBytes, _ := json.Marshal(&StaticExtra{ + DexReservesResolver: config.DexReservesResolver, + }) + + testPool := entity.Pool{ + Address: poolAddr, + Exchange: string(valueobject.ExchangeFluidDexT1), + Type: DexType, + Reserves: entity.PoolReserves{"0", "0"}, + Tokens: []*entity.PoolToken{ + { + Address: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + Weight: 1, + Swappable: true, + Decimals: 18, + }, + { + Address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + Weight: 1, + Swappable: true, + Decimals: 18, + }, + }, + StaticExtra: string(staticExtraBytes), + } + + logger.Debugf("Testing wstETH_ETH_Pool with address: %s", poolAddr) + + newPool, err := poolTracker.GetNewPoolState(context.Background(), testPool, pool.GetNewPoolStateParams{}) + require.NoError(t, err) + logger.Debugf("GetNewPoolState completed for wstETH_ETH_Pool, new pool: %+v", newPool) + + var extra PoolExtra + err = json.Unmarshal([]byte(newPool.Extra), &extra) + require.NoError(t, err) + require.Equal(t, poolAddr, newPool.Address) + + require.Equal(t, true, newPool.Tokens[0].Swappable) + require.Equal(t, true, newPool.Tokens[1].Swappable) + require.Equal(t, 0.01, newPool.SwapFee) + + token0RealReserves := extra.CollateralReserves.Token0RealReserves + token0Debt := extra.DebtReserves.Token0RealReserves + expectedToken0Reserve := new(big.Int).Add(token0RealReserves, token0Debt).String() + require.Equal(t, expectedToken0Reserve, newPool.Reserves[0], "Reserve should be equal to Token0RealReserves + Token0Debt") + + token1RealReserves := extra.CollateralReserves.Token1RealReserves + token1Debt := extra.DebtReserves.Token1RealReserves + expectedToken1Reserve := new(big.Int).Add(token1RealReserves, token1Debt).String() + require.Equal(t, expectedToken1Reserve, newPool.Reserves[1], "Reserve should be equal to Token1RealReserves + Token1Debt") + + require.True(t, extra.CollateralReserves.Token0RealReserves.Cmp(big.NewInt(0)) > 0) + require.True(t, extra.CollateralReserves.Token1RealReserves.Cmp(big.NewInt(0)) > 0) + require.True(t, extra.CollateralReserves.Token0ImaginaryReserves.Cmp(big.NewInt(0)) > 0) + require.True(t, extra.CollateralReserves.Token1ImaginaryReserves.Cmp(big.NewInt(0)) > 0) + require.True(t, extra.DebtReserves.Token0Debt.Cmp(big.NewInt(0)) > 0) + require.True(t, extra.DebtReserves.Token1Debt.Cmp(big.NewInt(0)) > 0) + require.True(t, extra.DebtReserves.Token0RealReserves.Cmp(big.NewInt(0)) > 0) + require.True(t, extra.DebtReserves.Token1RealReserves.Cmp(big.NewInt(0)) > 0) + require.True(t, extra.DebtReserves.Token0ImaginaryReserves.Cmp(big.NewInt(0)) > 0) + + logger.Debugf("Debt Reserves: Token0Debt: %s", extra.DebtReserves.Token0Debt.String()) + logger.Debugf("Debt Reserves: Token1Debt: %s", extra.DebtReserves.Token1Debt.String()) + logger.Debugf("Debt Reserves: Token0RealReserves: %s", extra.DebtReserves.Token0RealReserves.String()) + logger.Debugf("Debt Reserves: Token1RealReserves: %s", extra.DebtReserves.Token1RealReserves.String()) + logger.Debugf("Debt Reserves: Token0ImaginaryReserves: %s", extra.DebtReserves.Token0ImaginaryReserves.String()) + logger.Debugf("Debt Reserves: Token1ImaginaryReserves: %s", extra.DebtReserves.Token1ImaginaryReserves.String()) + + logger.Debugf("Collateral Reserves: Token0RealReserves: %s", extra.CollateralReserves.Token0RealReserves.String()) + logger.Debugf("Collateral Reserves: Token1RealReserves: %s", extra.CollateralReserves.Token1RealReserves.String()) + logger.Debugf("Collateral Reserves: Token0ImaginaryReserves: %s", extra.CollateralReserves.Token0ImaginaryReserves.String()) + logger.Debugf("Collateral Reserves: Token1ImaginaryReserves: %s", extra.CollateralReserves.Token1ImaginaryReserves.String()) + + jsonEncoded, _ := json.MarshalIndent(newPool, "", " ") + t.Logf("Updated wstETH-ETH Pool: %s\n", string(jsonEncoded)) + }) + +} diff --git a/pkg/liquidity-source/fluid/dex-t1/types.go b/pkg/liquidity-source/fluid/dex-t1/types.go new file mode 100644 index 000000000..d50f55b08 --- /dev/null +++ b/pkg/liquidity-source/fluid/dex-t1/types.go @@ -0,0 +1,50 @@ +package dexT1 + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +type PoolMeta struct { + BlockNumber uint64 `json:"blockNumber"` +} + +type PoolExtra struct { + CollateralReserves CollateralReserves + DebtReserves DebtReserves +} + +type CollateralReserves struct { + Token0RealReserves *big.Int `json:"token0RealReserves"` + Token1RealReserves *big.Int `json:"token1RealReserves"` + Token0ImaginaryReserves *big.Int `json:"token0ImaginaryReserves"` + Token1ImaginaryReserves *big.Int `json:"token1ImaginaryReserves"` +} + +type DebtReserves struct { + Token0Debt *big.Int `json:"token0Debt"` + Token1Debt *big.Int `json:"token1Debt"` + Token0RealReserves *big.Int `json:"token0RealReserves"` + Token1RealReserves *big.Int `json:"token1RealReserves"` + Token0ImaginaryReserves *big.Int `json:"token0ImaginaryReserves"` + Token1ImaginaryReserves *big.Int `json:"token1ImaginaryReserves"` +} + +type PoolWithReserves struct { + PoolAddress common.Address `json:"poolAddress"` + Token0Address common.Address `json:"token0Address"` + Token1Address common.Address `json:"token1Address"` + Fee *big.Int `json:"fee"` + CollateralReserves CollateralReserves `json:"collateralReserves"` + DebtReserves DebtReserves `json:"debtReserves"` +} + +type Gas struct { + Swap int64 +} + +type StaticExtra struct { + DexReservesResolver string `json:"dexReservesResolver"` + HasNative bool `json:"hasNative"` +} diff --git a/pkg/liquidity-source/fluid/vault-t1/pool_list_updater_test.go b/pkg/liquidity-source/fluid/vault-t1/pool_list_updater_test.go index ed15149b0..7be40c83a 100644 --- a/pkg/liquidity-source/fluid/vault-t1/pool_list_updater_test.go +++ b/pkg/liquidity-source/fluid/vault-t1/pool_list_updater_test.go @@ -41,6 +41,11 @@ func TestPoolListUpdater(t *testing.T) { require.NoError(t, err) require.True(t, len(pools) >= 23) + staticExtraBytes, _ := json.Marshal(&StaticExtra{ + VaultLiquidationResolver: config.VaultLiquidationResolver, + HasNative: true, + }) + expectedPool0 := entity.Pool{ Address: "0xeAbBfca72F8a8bf14C4ac59e69ECB2eB69F0811C", Exchange: "fluid-vault-t1", @@ -48,17 +53,26 @@ func TestPoolListUpdater(t *testing.T) { Reserves: []string{"0", "0"}, Tokens: []*entity.PoolToken{ { - Address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + Address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", Weight: 1, Swappable: true, }, { - Address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", - Weight: 1, + Address: "", + Weight: 1, + Swappable: true, }, }, + StaticExtra: string(staticExtraBytes), } + require.Equal(t, expectedPool0, pools[0]) + + staticExtraBytes, _ = json.Marshal(&StaticExtra{ + VaultLiquidationResolver: config.VaultLiquidationResolver, + HasNative: false, + }) + expectedPool21 := entity.Pool{ Address: "0x3A0b7c8840D74D39552EF53F586dD8c3d1234C40", Exchange: "fluid-vault-t1", @@ -66,18 +80,19 @@ func TestPoolListUpdater(t *testing.T) { Reserves: []string{"0", "0"}, Tokens: []*entity.PoolToken{ { - Address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", + Address: "0xdac17f958d2ee523a2206206994597c13d831ec7", Weight: 1, Swappable: true, }, { - Address: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", - Weight: 1, + Address: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + Weight: 1, + Swappable: true, }, }, + StaticExtra: string(staticExtraBytes), } - require.Equal(t, expectedPool0, pools[0]) require.Equal(t, expectedPool21, pools[21]) // Log all pools diff --git a/pkg/liquidity-source/fluid/vault-t1/pool_tracker_test.go b/pkg/liquidity-source/fluid/vault-t1/pool_tracker_test.go index a908c1629..335508a97 100644 --- a/pkg/liquidity-source/fluid/vault-t1/pool_tracker_test.go +++ b/pkg/liquidity-source/fluid/vault-t1/pool_tracker_test.go @@ -16,8 +16,9 @@ import ( ) func TestPoolTracker(t *testing.T) { - // @dev test is guaranteed to work on block (because liquidation is available) - // const testBlockNumber = uint64(20812089) + // @dev test is guaranteed to work on block (because liquidation is available). + // just add the following line in pool_tracker.go getPoolSwapData() and comment out t.Skip here: + // req.SetBlockNumber(big.NewInt(20812089)) t.Skip() _ = logger.SetLogLevel("debug") @@ -47,17 +48,11 @@ func TestPoolTracker(t *testing.T) { Tokens: []*entity.PoolToken{ { Address: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", - Name: "wstETH", - Symbol: "wstETH", - Decimals: 18, Weight: 1, Swappable: true, }, { Address: "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee", - Name: "weETH", - Symbol: "weETH", - Decimals: 18, Weight: 1, Swappable: true, }, diff --git a/pkg/msgpack/register_pool_types.go b/pkg/msgpack/register_pool_types.go index e69d35be2..70136eb5c 100644 --- a/pkg/msgpack/register_pool_types.go +++ b/pkg/msgpack/register_pool_types.go @@ -28,6 +28,7 @@ import ( pkg_liquiditysource_ethervista "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/ether-vista" pkg_liquiditysource_etherfi_eeth "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/etherfi/eeth" 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_genericsimplerate "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/generic-simple-rate" pkg_liquiditysource_gyroscope_2clp "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/gyroscope/2clp" @@ -145,6 +146,7 @@ func init() { msgpack.RegisterConcreteType(&pkg_liquiditysource_ethervista.PoolSimulator{}) msgpack.RegisterConcreteType(&pkg_liquiditysource_etherfi_eeth.PoolSimulator{}) 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_genericsimplerate.PoolSimulator{}) msgpack.RegisterConcreteType(&pkg_liquiditysource_gyroscope_2clp.PoolSimulator{}) diff --git a/pkg/pooltypes/pooltypes.go b/pkg/pooltypes/pooltypes.go index c823270b7..a92def6b2 100644 --- a/pkg/pooltypes/pooltypes.go +++ b/pkg/pooltypes/pooltypes.go @@ -24,6 +24,7 @@ import ( ethervista "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/ether-vista" "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/etherfi/eeth" "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" generic_simple_rate "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/generic-simple-rate" gyro2clp "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/gyroscope/2clp" @@ -230,7 +231,6 @@ type Types struct { RenzoEZETH string Slipstream string NuriV2 string - FluidVaultT1 string EtherVista string MkrSky string DaiUsds string @@ -242,6 +242,8 @@ type Types struct { RingSwap string PrimeETH string StaderETHx string + FluidVaultT1 string + FluidDexT1 string MantleETH string OndoUSDY string Clipper string @@ -360,7 +362,6 @@ var ( RenzoEZETH: ezeth.DexType, Slipstream: slipstream.DexType, NuriV2: nuriv2.DexType, - FluidVaultT1: fluidVaultT1.DexType, EtherVista: ethervista.DexType, MkrSky: mkrsky.DexType, DaiUsds: daiusds.DexType, @@ -372,6 +373,8 @@ var ( RingSwap: ringswap.DexType, PrimeETH: primeeth.DexType, StaderETHx: staderethx.DexType, + FluidVaultT1: fluidVaultT1.DexType, + FluidDexT1: fluidDexT1.DexType, MantleETH: meth.DexType, OndoUSDY: ondo_usdy.DexType, Clipper: clipper.DexType, diff --git a/pkg/valueobject/exchange.go b/pkg/valueobject/exchange.go index a98a9fcdb..7ced6da8a 100644 --- a/pkg/valueobject/exchange.go +++ b/pkg/valueobject/exchange.go @@ -345,7 +345,6 @@ var ( ExchangeQuickSwapUniV3 Exchange = "quickswap-uni-v3" ExchangeAmbient Exchange = "ambient" - ExchangeFluidVaultT1 Exchange = "fluid-vault-t1" ExchangeMaverickV2 Exchange = "maverick-v2" ExchangeEtherVista Exchange = "ether-vista" ExchangeLitePSM Exchange = "lite-psm" @@ -358,6 +357,8 @@ var ( ExchangePrimeETH Exchange = "primeeth" ExchangeStaderETHx Exchange = "staderethx" ExchangeFrxETH Exchange = "frxeth" + ExchangeFluidVaultT1 Exchange = "fluid-vault-t1" + ExchangeFluidDexT1 Exchange = "fluid-dex-t1" ExchangeMantleETH Exchange = "meth" ExchangeOndoUSDY Exchange = "ondo-usdy" @@ -643,12 +644,13 @@ var AMMSourceSet = map[Exchange]struct{}{ ExchangeSectaV3: {}, ExchangeQuickSwapUniV3: {}, ExchangeAmbient: {}, - ExchangeFluidVaultT1: {}, ExchangeMaverickV2: {}, ExchangeEtherVista: {}, ExchangeLitePSM: {}, ExchangeMkrSky: {}, ExchangeDaiUsds: {}, + ExchangeFluidVaultT1: {}, + ExchangeFluidDexT1: {}, ExchangeUsd0PP: {}, ExchangeWBETH: {}, ExchangeOETH: {},