diff --git a/src/abi/integral/factory.json b/src/abi/integral/factory.json new file mode 100644 index 000000000..9654d3e55 --- /dev/null +++ b/src/abi/integral/factory.json @@ -0,0 +1,333 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnerSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token0", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token1", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "pair", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "PairCreated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "allPairs", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPairsLength", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "collect", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "address", + "name": "oracle", + "type": "address" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "createPair", + "outputs": [ + { + "internalType": "address", + "name": "pair", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "getPair", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "setBurnFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "setMintFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "address", + "name": "oracle", + "type": "address" + } + ], + "name": "setOracle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + } + ], + "name": "setOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "setSwapFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "setTrader", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/abi/integral/oracle.json b/src/abi/integral/oracle.json new file mode 100644 index 000000000..4ff9c64ae --- /dev/null +++ b/src/abi/integral/oracle.json @@ -0,0 +1,501 @@ +[ + { + "inputs": [ + { + "internalType": "uint8", + "name": "_xDecimals", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "_yDecimals", + "type": "uint8" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnerSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint32", + "name": "interval", + "type": "uint32" + } + ], + "name": "TwapIntervalSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "uniswapPair", + "type": "address" + } + ], + "name": "UniswapPairSet", + "type": "event" + }, + { + "inputs": [], + "name": "decimalsConverter", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "xLeft", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "xBefore", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "yBefore", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "depositTradeXIn", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "yLeft", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "xBefore", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "yBefore", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "depositTradeYIn", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "getAveragePrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPriceInfo", + "outputs": [ + { + "internalType": "uint256", + "name": "priceAccumulator", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "priceTimestamp", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getSpotPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "swapFee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1In", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "getSwapAmount0Out", + "outputs": [ + { + "internalType": "uint256", + "name": "amount0Out", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "swapFee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount0In", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "getSwapAmount1Out", + "outputs": [ + { + "internalType": "uint256", + "name": "amount1Out", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "inverse", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "swapFee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_amountOut", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "getSwapAmountInMaxOut", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "inverse", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "swapFee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_amountOut", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "getSwapAmountInMinOut", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + } + ], + "name": "setOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_interval", + "type": "uint32" + } + ], + "name": "setTwapInterval", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_uniswapPair", + "type": "address" + } + ], + "name": "setUniswapPair", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "xAfter", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "xBefore", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "yBefore", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "tradeX", + "outputs": [ + { + "internalType": "uint256", + "name": "yAfter", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "yAfter", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "xBefore", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "yBefore", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "tradeY", + "outputs": [ + { + "internalType": "uint256", + "name": "xAfter", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "twapInterval", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "uniswapPair", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "xDecimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "yDecimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/src/abi/integral/pool.json b/src/abi/integral/pool.json new file mode 100644 index 000000000..e72bc20a9 --- /dev/null +++ b/src/abi/integral/pool.json @@ -0,0 +1,1041 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0Out", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1Out", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "liquidityIn", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "Burn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0In", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1In", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "liquidityOut", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "Mint", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "SetBurnFee", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "SetMintFee", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "SetOracle", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "SetSwapFee", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "SetTrader", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0In", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1In", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0Out", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1Out", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "Swap", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MINIMUM_LIQUIDITY", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "PERMIT_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "burn", + "outputs": [ + { + "internalType": "uint256", + "name": "amount0Out", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1Out", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "burnFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "collect", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "getDepositAmount0In", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "getDepositAmount1In", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getDomainSeparator", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getFees", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getReserves", + "outputs": [ + { + "internalType": "uint112", + "name": "", + "type": "uint112" + }, + { + "internalType": "uint112", + "name": "", + "type": "uint112" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount1Out", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "getSwapAmount0In", + "outputs": [ + { + "internalType": "uint256", + "name": "swapAmount0In", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount1In", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "getSwapAmount0Out", + "outputs": [ + { + "internalType": "uint256", + "name": "swapAmount0Out", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount0Out", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "getSwapAmount1In", + "outputs": [ + { + "internalType": "uint256", + "name": "swapAmount1In", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount0In", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "getSwapAmount1Out", + "outputs": [ + { + "internalType": "uint256", + "name": "swapAmount1Out", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token0", + "type": "address" + }, + { + "internalType": "address", + "name": "_token1", + "type": "address" + }, + { + "internalType": "address", + "name": "_oracle", + "type": "address" + }, + { + "internalType": "address", + "name": "_trader", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "liquidityOut", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "mintFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "oracle", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "setBurnFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "setMintFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_oracle", + "type": "address" + } + ], + "name": "setOracle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "setSwapFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_trader", + "type": "address" + } + ], + "name": "setTrader", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount0Out", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1Out", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "swap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "swapFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "sync", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "token0", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "token1", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "trader", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/abi/integral/relayer.json b/src/abi/integral/relayer.json new file mode 100644 index 000000000..884a3be68 --- /dev/null +++ b/src/abi/integral/relayer.json @@ -0,0 +1,1536 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Approve", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountInMax", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "wrapUnwrap", + "type": "bool" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "orderContract", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "orderId", + "type": "uint256" + } + ], + "name": "Buy", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "delay", + "type": "address" + } + ], + "name": "DelaySet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "gasCost", + "type": "uint256" + } + ], + "name": "EthTransferGasCostSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "limit", + "type": "uint256" + } + ], + "name": "ExecutionGasLimitSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "_factory", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "_delay", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "_weth", + "type": "address" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "oneInchRouter", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "whitelisted", + "type": "bool" + } + ], + "name": "OneInchRouterWhitelisted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnerSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "pair", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "name": "PairEnabledSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "delayOrderId", + "type": "uint256" + } + ], + "name": "RebalanceSellWithDelay", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "oneInchRouter", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "gas", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "RebalanceSellWithOneInch", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "rebalancer", + "type": "address" + } + ], + "name": "RebalancerSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "wrapUnwrap", + "type": "bool" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "orderContract", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "orderId", + "type": "uint256" + } + ], + "name": "Sell", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "pair", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "SwapFeeSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "limit", + "type": "uint256" + } + ], + "name": "TokenLimitMaxMultiplierSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "limit", + "type": "uint256" + } + ], + "name": "TokenLimitMinSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "pair", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "tolerance", + "type": "uint16" + } + ], + "name": "ToleranceSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "pair", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "interval", + "type": "uint32" + } + ], + "name": "TwapIntervalSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "UnwrapWeth", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdraw", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "WrapEth", + "type": "event" + }, + { + "inputs": [], + "name": "DELAY_ADDRESS", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "EXECUTION_GAS_LIMIT", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "FACTORY_ADDRESS", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "WETH_ADDRESS", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "__RESERVED__OLD_DELAY", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "__RESERVED__OLD_ETH_TRANSFER_GAS_COST", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "__RESERVED__OLD_EXECUTION_GAS_LIMIT", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "__RESERVED__OLD_FACTORY", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "__RESERVED__OLD_TOKEN_LIMIT_MAX_MULTIPLIER", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "__RESERVED__OLD_TOKEN_LIMIT_MIN", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "__RESERVED__OLD_TOLERANCE", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "__RESERVED__OLD_TWAP_INTERVAL", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "__RESERVED__OLD_WETH", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "__RESERVED__SLOT_6_USED_IN_PREVIOUS_VERSIONS", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountInMax", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wrapUnwrap", + "type": "bool" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint32", + "name": "submitDeadline", + "type": "uint32" + } + ], + "internalType": "struct ITwapRelayer.BuyParams", + "name": "buyParams", + "type": "tuple" + } + ], + "name": "buy", + "outputs": [ + { + "internalType": "uint256", + "name": "orderId", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "delay", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "ethTransferGasCost", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "executionGasLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token0", + "type": "address" + }, + { + "internalType": "address", + "name": "token1", + "type": "address" + } + ], + "name": "getPoolState", + "outputs": [ + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "limitMin0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "limitMax0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "limitMin1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "limitMax1", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pair", + "type": "address" + }, + { + "internalType": "bool", + "name": "inverted", + "type": "bool" + } + ], + "name": "getPriceByPairAddress", + "outputs": [ + { + "internalType": "uint8", + "name": "xDecimals", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "yDecimals", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenOut", + "type": "address" + } + ], + "name": "getPriceByTokenAddresses", + "outputs": [ + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "getTokenLimitMaxMultiplier", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "getTokenLimitMin", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "getTolerance", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "getTwapInterval", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "initializeLock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "initialized", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isOneInchRouterWhitelisted", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isPairEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "name": "quoteBuy", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + } + ], + "name": "quoteSell", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + } + ], + "name": "rebalanceSellWithDelay", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "address", + "name": "oneInchRouter", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_gas", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "rebalanceSellWithOneInch", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "rebalancer", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wrapUnwrap", + "type": "bool" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint32", + "name": "submitDeadline", + "type": "uint32" + } + ], + "internalType": "struct ITwapRelayer.SellParams", + "name": "sellParams", + "type": "tuple" + } + ], + "name": "sell", + "outputs": [ + { + "internalType": "uint256", + "name": "orderId", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + } + ], + "name": "setOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pair", + "type": "address" + }, + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "name": "setPairEnabled", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_rebalancer", + "type": "address" + } + ], + "name": "setRebalancer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pair", + "type": "address" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "setSwapFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "swapFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "tokenLimitMaxMultiplier", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "tokenLimitMin", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pair", + "type": "address" + } + ], + "name": "tolerance", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pair", + "type": "address" + } + ], + "name": "twapInterval", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "unwrapWeth", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "weth", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "oneInchRouter", + "type": "address" + }, + { + "internalType": "bool", + "name": "whitelisted", + "type": "bool" + } + ], + "name": "whitelistOneInchRouter", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "wrapEth", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/src/dex/index.ts b/src/dex/index.ts index 4b8efa8a4..4d9abf49d 100644 --- a/src/dex/index.ts +++ b/src/dex/index.ts @@ -87,6 +87,7 @@ import { Wombat } from './wombat/wombat'; import { Swell } from './swell/swell'; import { PharaohV1 } from './solidly/forks-override/pharaohV1'; import { EtherFi } from './etherfi'; +import { Integral } from './integral/integral'; const LegacyDexes = [ CurveV2, @@ -170,6 +171,7 @@ const Dexes = [ Wombat, Swell, PharaohV1, + Integral, ]; export type LegacyDexConstructor = new (dexHelper: IDexHelper) => IDexTxBuilder< diff --git a/src/dex/integral/config.ts b/src/dex/integral/config.ts new file mode 100644 index 000000000..f2403e3da --- /dev/null +++ b/src/dex/integral/config.ts @@ -0,0 +1,22 @@ +import { DexParams } from './types'; +import { DexConfigMap, AdapterMappings } from '../../types'; +import { Network } from '../../constants'; + +export const IntegralConfig: DexConfigMap = { + Integral: { + [Network.MAINNET]: { + factoryAddress: '0xC480b33eE5229DE3FbDFAD1D2DCD3F3BAD0C56c6', + relayerAddress: '0xd17b3c9784510E33cD5B87b490E79253BcD81e2E', + subgraphURL: + 'https://api.thegraph.com/subgraphs/name/integralhq/integral-size', + }, + [Network.ARBITRUM]: { + factoryAddress: '0x717EF162cf831db83c51134734A15D1EBe9E516a', + relayerAddress: '0x3c6951FDB433b5b8442e7aa126D50fBFB54b5f42', + subgraphURL: + 'https://api.thegraph.com/subgraphs/name/integralhq/integral-size-arbitrum', + }, + }, +}; + +export const Adapters: Record = {}; diff --git a/src/dex/integral/context.ts b/src/dex/integral/context.ts new file mode 100644 index 000000000..0ba366f52 --- /dev/null +++ b/src/dex/integral/context.ts @@ -0,0 +1,204 @@ +import { Address } from '@paraswap/core'; +import { IntegralEventPool } from './integral-pool'; +import { IntegralPricing } from './integral-pricing'; +import { IntegralFactory } from './integral-factory'; +import { IntegralRelayer } from './integral-relayer'; +import { Network } from '../../constants'; +import { IDexHelper } from '../../dex-helper'; +import { Logger } from 'log4js'; +import { Interface } from '@ethersproject/abi'; +import { IntegralPool, PoolInitProps, RelayerState, Requires } from './types'; +import { + getPoolIdentifier, + onPoolCreatedAddPool, + onRelayerPoolEnabledSet, + onTransferUpdateBalance, +} from './helpers'; +import { IntegralToken } from './integral-token'; + +export class IntegralContext { + static instance: IntegralContext; + + private _pools: { [poolId: string]: IntegralPool } = {}; + private _tokens: { [tokenAddress: Address]: IntegralToken } = {}; + private _factory: IntegralFactory; + private _relayer: IntegralRelayer; + + logger: Logger; + + private constructor( + readonly network: Network, + readonly dexKey: string, + readonly dexHelper: IDexHelper, + readonly erc20Interface: Interface, + readonly factoryAddress: Address, + readonly relayerAddress: Address, + ) { + this.logger = dexHelper.getLogger(dexKey); + this._factory = new IntegralFactory( + dexHelper, + dexKey, + this.factoryAddress, + onPoolCreatedAddPool, + this.logger, + ); + this._relayer = new IntegralRelayer( + dexHelper, + dexKey, + this.erc20Interface, + this.relayerAddress, + {}, + onRelayerPoolEnabledSet, + this.logger, + ); + } + + async addPools( + pools: PoolInitProps, + blockNumber: number, + initPhase: boolean = false, + ) { + this._relayer.addPools(pools); + if (initPhase) { + await this._relayer.initialize(blockNumber); + } + const _relayerState = this._relayer.getState(blockNumber); + const relayerState = _relayerState + ? _relayerState + : await this._relayer.generateState(blockNumber); + if (!_relayerState) { + this._relayer.setState(relayerState, blockNumber); + } + const poolInitProps = this.removeDisabledPoolInitProps(relayerState, pools); + + const bases = Object.entries(poolInitProps).map(([poolAddress, p]) => { + const poolId = getPoolIdentifier(this.dexKey, p.token0, p.token1); + const base = + (this._pools[poolId] && this._pools[poolId].base) || + new IntegralEventPool( + this.dexKey, + this.network, + this.dexHelper, + poolAddress, + p.token0, + p.token1, + this.logger, + ); + this._pools[poolId] = { base, enabled: true }; + return base; + }); + await Promise.all( + bases.map(base => !base.isInitialized && base.initialize(blockNumber)), + ); + + const pricings = await Promise.all( + bases.map(async base => { + const _poolState = base.getState(blockNumber); + const poolState = _poolState + ? _poolState + : await base.generateState(blockNumber); + const poolId = getPoolIdentifier(this.dexKey, base.token0, base.token1); + const pricing = + (this._pools[poolId] && this._pools[poolId].pricing) || + new IntegralPricing( + this.dexHelper, + poolId, + this.erc20Interface, + poolState.uniswapPool, + base.token0, + base.token1, + poolState.uniswapPoolFee, + this.logger, + this.network, + ); + this._pools[poolId].pricing = pricing; + return pricing; + }), + ); + await Promise.all( + pricings.map( + pricing => !pricing.isInitialized && pricing.initialize(blockNumber), + ), + ); + + const initIntegralToken = (token: Address) => + new IntegralToken( + this.dexHelper, + this.dexKey, + this.erc20Interface, + token, + this.relayerAddress, + onTransferUpdateBalance, + this.logger, + ); + bases.map(base => { + this._tokens[base.token0] = + this._tokens[base.token0] || initIntegralToken(base.token0); + this._tokens[base.token1] = + this._tokens[base.token1] || initIntegralToken(base.token1); + }); + await Promise.all( + Object.values(this._tokens).map( + token => !token.isInitialized && token.initialize(blockNumber), + ), + ); + } + + getPoolAddresses() { + return Object.values(this._pools) + .filter( + (pool): pool is Requires => + !!pool.base && pool.enabled, + ) + .map(({ base }) => base.poolAddress); + } + + private removeDisabledPoolInitProps( + state: RelayerState, + props: PoolInitProps, + ) { + Object.entries(state.pools).forEach( + ([poolAddress, poolState]) => + !poolState.isEnabled && delete props[poolAddress], + ); + return props; + } + + public static initialize( + network: Network, + dexKey: string, + dexHelper: IDexHelper, + erc20Interface: Interface, + factoryAddress: Address, + relayerAddress: Address, + ) { + if (!this.instance) { + this.instance = new IntegralContext( + network, + dexKey, + dexHelper, + erc20Interface, + factoryAddress, + relayerAddress, + ); + } + return this.instance; + } + + public static getInstance() { + if (!this.instance) { + throw new Error('IntegralContext instance not initialized'); + } + return this.instance; + } + + get pools() { + return this._pools; + } + get factory() { + return this._factory; + } + get relayer() { + return this._relayer; + } +} diff --git a/src/dex/integral/helpers.ts b/src/dex/integral/helpers.ts new file mode 100644 index 000000000..32f975855 --- /dev/null +++ b/src/dex/integral/helpers.ts @@ -0,0 +1,223 @@ +import _ from 'lodash'; +import { Address } from '../../types'; +import { _require } from '../../utils'; +import { FullMath } from '../uniswap-v3/contract-math/FullMath'; +import { Oracle } from '../uniswap-v3/contract-math/Oracle'; +import { TickMath } from '../uniswap-v3/contract-math/TickMath'; +import { PoolState as UniswapPoolState } from '../uniswap-v3/types'; +import { IntegralContext } from './context'; +import { PoolStates, RelayerPoolState, RelayerState } from './types'; +import { sortTokens } from './utils'; + +export function getDecimalsConverter( + decimals0: number, + decimals1: number, + inverted: boolean, +) { + return ( + 10n ** + (18n + BigInt(inverted ? decimals1 - decimals0 : decimals0 - decimals1)) + ); +} + +export function getPrice(states: PoolStates, inverted: boolean) { + const { base, pricing, relayer } = states; + const decimalsConverter = getDecimalsConverter( + base.decimals0, + base.decimals1, + false, + ); + const spotPrice = getSpotPrice(pricing, decimalsConverter); + const averagePrice = getAveragePrice(relayer, pricing, decimalsConverter); + if (inverted) { + return 10n ** 36n / (spotPrice > averagePrice ? spotPrice : averagePrice); + } else { + return spotPrice < averagePrice ? spotPrice : averagePrice; + } +} + +export function getSpotPrice( + pricing: UniswapPoolState, + decimalsConverter: bigint, +) { + const sqrtPriceX96 = pricing.slot0.sqrtPriceX96; + const priceX128 = FullMath.mulDiv(sqrtPriceX96, sqrtPriceX96, 2n ** 64n); + return FullMath.mulDiv(priceX128, decimalsConverter, 2n ** 128n); +} + +export function getAveragePrice( + relayer: RelayerPoolState, + pricing: UniswapPoolState, + decimalsConverter: bigint, +) { + _require(relayer.twapInterval > 0, 'Twap Interval not set'); + const secondsAgo = BigInt(relayer.twapInterval); + const secondsAgos = [secondsAgo, 0n]; + const tickCumulatives = getTickCumulatives(pricing, secondsAgos); + + const delta = tickCumulatives[1] - tickCumulatives[0]; + let arithmeticMeanTick = delta / secondsAgo; + if (delta < 0 && delta % secondsAgo != 0n) { + --arithmeticMeanTick; + } + + const sqrtRatioX96 = TickMath.getSqrtRatioAtTick(arithmeticMeanTick); + const ratioX128 = FullMath.mulDiv(sqrtRatioX96, sqrtRatioX96, 2n ** 64n); + return FullMath.mulDiv(ratioX128, decimalsConverter, 2n ** 128n); +} + +export function getTickCumulatives( + poolState: UniswapPoolState, + secondsAgos: bigint[], +) { + const { tickCumulatives } = secondsAgos.reduce( + (memo, secondsAgo, i) => { + [memo.tickCumulatives[i], memo.secondsPerLiquidityCumulativeX128s[i]] = + Oracle.observeSingle( + poolState, + BigInt.asUintN(32, poolState.blockTimestamp), + secondsAgo, + poolState.slot0.tick, + poolState.slot0.observationIndex, + poolState.liquidity, + poolState.slot0.observationCardinality, + ); + return memo; + }, + { + tickCumulatives: Array(secondsAgos.length), + secondsPerLiquidityCumulativeX128s: Array(secondsAgos.length), + }, + ); + + return tickCumulatives; +} + +export function isInverted(tokenIn: Address, tokenOut: Address) { + const [, token1] = + tokenIn.toLowerCase() < tokenOut.toLowerCase() + ? [tokenIn, tokenOut] + : [tokenOut, tokenIn]; + return tokenIn === token1; +} + +export function getPoolIdentifier( + dexKey: string, + srcAddress: Address, + destAddress: Address, +) { + const tokenAddresses = sortTokens( + srcAddress.toLowerCase(), + destAddress.toLowerCase(), + ).join('_'); + return `${dexKey}_${tokenAddresses}`; +} + +export async function onPoolSwapForRelayer( + blockNumber: number, + poolAddress: Address, + recipient: Address, + amount0Out: bigint, + amount1Out: bigint, + _context?: IntegralContext, +) { + const context = _context || IntegralContext.getInstance(); + if (recipient.toLowerCase() === context.relayerAddress.toLowerCase()) { + const pools = context.relayer.getPools(); + if (pools[poolAddress.toLowerCase()]) { + const token0 = pools[poolAddress.toLowerCase()].token0; + const token1 = pools[poolAddress.toLowerCase()].token1; + + let _state = context.relayer.getStaleState(); + if (!_state) { + _state = await context.relayer.generateState(blockNumber); + context.relayer.setState(_state, blockNumber); + return; + } else { + blockNumber = context.relayer.getStateBlockNumber(); + } + + const state: RelayerState = _.cloneDeep(_state); + if (amount1Out > 0n) { + state.tokens[token1].balance += amount1Out; + } else { + state.tokens[token0].balance += amount0Out; + } + context.relayer.setState(state, blockNumber); + } else { + context.logger.error( + 'Integral Relayer: Pool address not found for', + poolAddress, + ); + } + } +} + +function getPoolBackReferencedFrom( + context: IntegralContext, + poolAddress: Address, +) { + return Object.entries(context.pools).find( + ([, pool]) => pool.base?.poolAddress === poolAddress, + ); +} + +export async function onRelayerPoolEnabledSet( + poolAddress: Address, + state: RelayerPoolState, + blockNumber: number, +) { + const context = IntegralContext.getInstance(); + const poolEntry = getPoolBackReferencedFrom(context, poolAddress); + if (state.isEnabled && (!poolEntry || !poolEntry[1].enabled)) { + const _factoryState = context.factory.getStaleState(); + const factoryState = _factoryState + ? _factoryState + : await context.factory.generateState(blockNumber); + const { token0, token1 } = factoryState.pools[poolAddress]; + await context.addPools({ [poolAddress]: { token0, token1 } }, blockNumber); + } else if (!state.isEnabled && poolEntry && poolEntry[1].enabled) { + context.pools[poolEntry[0]].enabled = false; + } +} + +export function onRebalanceSellOrderExecuted( + blockNumber: number, + orderId: bigint, +) { + const context = IntegralContext.getInstance(); + context.relayer.executeOrder(orderId, blockNumber); +} + +export async function onPoolCreatedAddPool( + token0: Address, + token1: Address, + poolAddress: Address, + blockNumber: number, +) { + const context = IntegralContext.getInstance(); + await context.addPools({ [poolAddress]: { token0, token1 } }, blockNumber); +} + +export async function onTransferUpdateBalance( + token: Address, + from: Address, + to: Address, + amount: bigint, + blockNumber: number, + _context?: IntegralContext, +) { + const context = _context || IntegralContext.getInstance(); + let _state = context.relayer.getStaleState(); + if (!_state) { + context.logger.error('Integral Token: Relayer stale state not found'); + return; + } + const state: RelayerState = _.cloneDeep(_state); + if (context.relayer.relayerAddress.toLowerCase() === from) { + state.tokens[token].balance -= amount; + } else if (context.relayer.relayerAddress.toLowerCase() === to) { + state.tokens[token].balance += amount; + } + context.relayer.setState(state, blockNumber); +} diff --git a/src/dex/integral/integral-e2e.test.ts b/src/dex/integral/integral-e2e.test.ts new file mode 100644 index 000000000..d27bd1275 --- /dev/null +++ b/src/dex/integral/integral-e2e.test.ts @@ -0,0 +1,118 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +import { testE2E } from './tests/utils-e2e'; +import { + Tokens, + Holders, + NativeTokenSymbols, +} from '../../../tests/constants-e2e'; +import { Network, ContractMethod, SwapSide } from '../../constants'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { generateConfig } from '../../config'; +import { ethers } from 'ethers'; + +jest.setTimeout(50 * 1000); + +describe('Integral E2E', () => { + const dexKey = 'Integral'; + + describe('Integral MAINNET', () => { + const network = Network.MAINNET; + + const tokens = Tokens[network]; + + const holders = Holders[network]; + + const provider = new StaticJsonRpcProvider( + generateConfig(network).privateHttpProvider, + network, + ); + + const nativeTokenSymbol = NativeTokenSymbols[network]; + + const testData = [['USDC', 'USDT', 51000, 51000, 17]]; + const testBlock = 19831195; + + const sideToContractMethods = { + [SwapSide.SELL]: ContractMethod.simpleSwap, + [SwapSide.BUY]: ContractMethod.simpleBuy, + }; + + for (const [ + tokenASymbol, + tokenBSymbol, + valueA, + valueB, + valueNative, + ] of testData) { + const tokenA = tokens[tokenASymbol]; + const tokenB = tokens[tokenBSymbol]; + const tokenAAmount = ethers.utils + .parseUnits(valueA.toString(), tokenA.decimals) + .toString(); + const tokenBAmount = ethers.utils + .parseUnits(valueB.toString(), tokenB.decimals) + .toString(); + const nativeTokenAmount = ethers.utils + .parseUnits(valueNative.toString(), 18) + .toString(); + it(nativeTokenSymbol + ` -> ${tokenASymbol}`, async () => { + await testE2E( + tokens[tokenASymbol], + tokens[nativeTokenSymbol], + holders[tokenASymbol], + tokenAAmount, + SwapSide.SELL, + dexKey, + sideToContractMethods[SwapSide.SELL], + network, + provider, + testBlock, + ); + }); + it(`${tokenASymbol} -> ` + nativeTokenSymbol, async () => { + await testE2E( + tokens[tokenASymbol], + tokens[nativeTokenSymbol], + holders[tokenASymbol], + nativeTokenAmount, + SwapSide.BUY, + dexKey, + sideToContractMethods[SwapSide.BUY], + network, + provider, + testBlock, + ); + }); + it(`${tokenASymbol} -> ${tokenBSymbol}`, async () => { + await testE2E( + tokens[tokenASymbol], + tokens[tokenBSymbol], + holders[tokenASymbol], + tokenAAmount, + SwapSide.SELL, + dexKey, + sideToContractMethods[SwapSide.SELL], + network, + provider, + testBlock, + ); + }); + it(`${tokenBSymbol} -> ${tokenASymbol}`, async () => { + await testE2E( + tokens[tokenASymbol], + tokens[tokenBSymbol], + holders[tokenASymbol], + tokenAAmount, + SwapSide.BUY, + dexKey, + sideToContractMethods[SwapSide.BUY], + network, + provider, + testBlock, + ); + }); + } + }); +}); diff --git a/src/dex/integral/integral-events.test.ts b/src/dex/integral/integral-events.test.ts new file mode 100644 index 000000000..253cb0f44 --- /dev/null +++ b/src/dex/integral/integral-events.test.ts @@ -0,0 +1,449 @@ +/* eslint-disable no-console */ +import dotenv from 'dotenv'; +dotenv.config(); + +import { + IntegralFactory, + OnPoolCreatedCallback, +} from '../integral/integral-factory'; +import { Network } from '../../constants'; +import { Address } from '../../types'; +import { DummyDexHelper } from '../../dex-helper/index'; +import { testEventSubscriber } from '../../../tests/utils-events'; +import { + FactoryState, + PoolState, + RelayerState, + TokenState, +} from '../integral/types'; +import { IntegralConfig } from './config'; +import { IntegralEventPool } from './integral-pool'; +import { Tokens } from '../../../tests/constants-e2e'; +import { IntegralRelayer, OnPoolEnabledSetCallback } from './integral-relayer'; +import ERC20ABI from '../../abi/erc20.json'; +import { Interface } from 'ethers/lib/utils'; +import { onTransferUpdateBalance } from './helpers'; +import { + updateEventSubscriber, + checkEventSubscriber, +} from './tests/utils-events'; +import { IntegralToken } from './integral-token'; +import { IntegralContext } from './context'; + +jest.setTimeout(50 * 1000); +const dexKey = 'Integral'; +const network = Network.MAINNET; +const dexHelper = new DummyDexHelper(network); +const logger = dexHelper.getLogger(dexKey); +const erc20Interface = new Interface(ERC20ABI); + +const TEST_POOL_ADDRESS = '0x6ec472b613012a492693697FA551420E60567eA7'; // usdc-usdt pool address +const _TEST_POOLS = { + ['0x6ec472b613012a492693697FA551420E60567eA7']: ['USDC', 'USDT'], + ['0x2fe16Dd18bba26e457B7dD2080d5674312b026a2']: ['WETH', 'USDC'], + ['0x048f0e7ea2CFD522a4a058D1b1bDd574A0486c46']: ['WETH', 'USDT'], + ['0x37F6dF71b40c50b2038329CaBf5FDa3682Df1ebF']: ['WETH', 'WBTC'], + ['0x29b57D56a114aE5BE3c129240898B3321A70A300']: ['WETH', 'wstETH'], + ['0x61fA1CEe13CEEAF20C30611c5e6dA48c595F7dB2']: [ + 'WETH', + '0xD33526068D116cE69F19A9ee46F0bd304F21A51f', + ], // RPL + ['0x045950A37c59d75496BB4Af68c05f9066A4C7e27']: [ + 'WETH', + '0x48C3399719B582dD63eB5AADf12A40B4C3f52FA2', + ], // SWISE + ['0xbEE7Ef1adfaa628536Ebc0C1EBF082DbDC27265F']: [ + 'WETH', + '0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32', + ], // LDO +}; +const TEST_POOLS = initTestPools(); +const dummyCallback = () => {}; + +function initTestPools() { + return Object.entries(_TEST_POOLS).reduce<{ + [poolAddress: Address]: { token0: Address; token1: Address }; + }>((memo, [poolAddress, [symbolA, symbolB]]) => { + const tokenA = + (Tokens[network][symbolA] && + Tokens[network][symbolA].address.toLowerCase()) || + symbolA.toLowerCase(); + const tokenB = + (Tokens[network][symbolB] && + Tokens[network][symbolB].address.toLowerCase()) || + symbolB.toLowerCase(); + const [token0, token1] = + tokenA < tokenB ? [tokenA, tokenB] : [tokenB, tokenA]; + memo[poolAddress] = { token0, token1 }; + return memo; + }, {}); +} + +async function fetchFactoryState( + integralFactory: IntegralFactory, + blockNumber: number, + factoryAddress: string, +): Promise { + const message = `Integral Factory: ${factoryAddress} blockNumber ${blockNumber}`; + console.log(`Fetching state ${message}`); + return integralFactory.generateState(blockNumber); +} + +async function fetchPoolState( + integralPool: IntegralEventPool, + blockNumber: number, + poolAddress: string, +): Promise { + const message = `Integral Pool: ${poolAddress} blockNumber ${blockNumber}`; + console.log(`Fetching state ${message}`); + return integralPool.generateState(blockNumber); +} + +async function fetchTokenState( + integralToken: IntegralToken, + blockNumber: number, + tokenAddress: string, +): Promise { + const message = `ERC20 Token: ${tokenAddress} blockNumber ${blockNumber}`; + console.log(`Fetching state ${message}`); + return integralToken.generateState(); +} + +async function fetchRelayerState( + integralRelayer: IntegralRelayer, + blockNumber: number, + relayerAddress: string, +): Promise { + const message = `Integral Relayer: ${relayerAddress} blockNumber ${blockNumber}`; + console.log(`Fetching state ${message}`); + return integralRelayer.generateState(blockNumber); +} + +// eventName -> blockNumbers +type EventMappings = Record; + +describe('Integral Factory Events Mainnet', function () { + const onPoolCreated: OnPoolCreatedCallback = + dummyCallback as unknown as OnPoolCreatedCallback; + let integralFactory: IntegralFactory; + + const factoryAddress = + IntegralConfig[dexKey][network].factoryAddress.toLowerCase(); + // poolAddress -> EventMappings + const eventsToTest: Record = { + [factoryAddress]: { + PairCreated: [ + 14423189, 14538708, 14722391, 15001878, 16638150, 17022679, 17333304, + 19011983, 19011987, 19011992, 19012000, 19483224, 19483230, 19483234, + 19483237, + ], + }, + }; + + beforeEach(async () => { + integralFactory = new IntegralFactory( + dexHelper, + dexKey, + factoryAddress, + onPoolCreated, + logger, + ); + }); + + Object.entries(eventsToTest).forEach( + ([factoryAddress, events]: [string, EventMappings]) => { + describe(`Events for Factory: ${factoryAddress}`, () => { + Object.entries(events).forEach( + ([eventName, blockNumbers]: [string, number[]]) => { + describe(`${eventName}`, () => { + blockNumbers.forEach((blockNumber: number) => { + it(`State after ${blockNumber}`, async function () { + await testEventSubscriber( + integralFactory, + integralFactory.addressesSubscribed, + (_blockNumber: number) => + fetchFactoryState( + integralFactory, + _blockNumber, + factoryAddress, + ), + blockNumber, + `${dexKey}_${factoryAddress}`, + dexHelper.provider, + ); + }); + }); + }); + }, + ); + }); + }, + ); +}); + +describe('Integral Pool Events Mainnet', function () { + let integralPool: IntegralEventPool; + + const eventsToTest: Record = { + [TEST_POOL_ADDRESS]: { + SetSwapFee: [17022687], + SetMintFee: [], + SetBurnFee: [], + }, + }; + + beforeEach(async () => { + integralPool = new IntegralEventPool( + dexKey, + network, + dexHelper, + TEST_POOL_ADDRESS, + TEST_POOLS[TEST_POOL_ADDRESS].token0, + TEST_POOLS[TEST_POOL_ADDRESS].token1, + logger, + ); + }); + + Object.entries(eventsToTest).forEach( + ([poolAddress, events]: [string, EventMappings]) => { + describe(`Events for Pool: ${poolAddress}`, () => { + Object.entries(events).forEach( + ([eventName, blockNumbers]: [string, number[]]) => { + describe(`${eventName}`, () => { + blockNumbers.forEach((blockNumber: number) => { + it(`State after ${blockNumber}`, async function () { + await testEventSubscriber( + integralPool, + integralPool.addressesSubscribed, + (_blockNumber: number) => + fetchPoolState(integralPool, _blockNumber, poolAddress), + blockNumber, + `${dexKey}_${poolAddress}`, + dexHelper.provider, + ); + }); + }); + }); + }, + ); + }); + }, + ); +}); + +describe('Integral Relayer Events Mainnet', function () { + let integralRelayer: IntegralRelayer; + const onPoolEnabledSet = dummyCallback as unknown as OnPoolEnabledSetCallback; + const relayerAddress = + IntegralConfig[dexKey][network].relayerAddress.toLowerCase(); + + const eventsToTest: Record = { + [relayerAddress]: { + SwapFeeSet: [19460786], + PairEnabledSet: [19133248, 19488170, 19488805], + UnwrapWeth: [ + 19117714, 19216294, 19272845, 19326496, 19361829, 19389103, 19430619, + 19473032, 19565232, 19632898, 19660246, 19757979, + ], + WrapEth: [], + }, + }; + + beforeEach(async () => { + integralRelayer = new IntegralRelayer( + dexHelper, + dexKey, + erc20Interface, + relayerAddress, + TEST_POOLS, + onPoolEnabledSet, + logger, + ); + }); + + Object.entries(eventsToTest).forEach( + ([relayerAddress, events]: [string, EventMappings]) => { + describe(`Events for Relayer: ${relayerAddress}`, () => { + Object.entries(events).forEach( + ([eventName, blockNumbers]: [string, number[]]) => { + describe(`${eventName}`, () => { + blockNumbers.forEach((blockNumber: number) => { + it(`State after ${blockNumber}`, async function () { + await testEventSubscriber( + integralRelayer, + integralRelayer.addressesSubscribed, + (_blockNumber: number) => + fetchRelayerState( + integralRelayer, + _blockNumber, + relayerAddress, + ), + blockNumber, + `${dexKey}_${relayerAddress}`, + dexHelper.provider, + ); + }); + }); + }); + }, + ); + }); + }, + ); +}); + +describe('Integral Multiple Events Mainnet', function () { + let context: IntegralContextTest; + const onPoolEnabledSet = dummyCallback as unknown as OnPoolEnabledSetCallback; + const relayerAddress = + IntegralConfig[dexKey][network].relayerAddress.toLowerCase(); + + const eventsToTest: Record = { + ['']: { + Transfer: [ + 18490280, 18567011, 18633511, 18713381, 18764112, 18831592, 18912551, + 19016598, 19117714, 19216294, 19241751, 19272845, 19326496, 19361829, + 19389103, 19430619, 19430725, 19473032, 19565232, 19632898, 19660246, + 19696426, 19701836, 19703192, 19709820, 19711173, 19715839, 19717874, + 19723287, 19724410, 19728665, 19733351, 19735237, 19738144, 19739161, + 19748150, 19748662, 19749146, 19752286, 19754012, 19757979, 19760993, + 19761502, 19768592, 19770633, 19773869, 19774874, 19776897, 19776905, + 19777396, 19782027, 19783245, 19787735, 19788241, 19789955, 19791466, + 19793053, 19793061, 19793780, 19794272, 19794281, 19794289, 19794773, + 19795346, 19795425, 19795435, 19795444, 19795464, 19795468, 19795572, + 19795580, 19795622, 19795773, 19795777, 19795831, 19795875, 19795886, + 19795962, 19796005, 19796069, 19796232, 19796233, 19796234, 19796386, + 19796563, 19796612, 19796613, 19796698, 19796829, 19796839, 19796842, + 19797051, 19797061, 19797219, 19797227, 19797230, 19797511, 19797537, + 19797546, 19797554, 19797574, 19797672, 19797836, 19797922, 19798001, + 19798043, 19798047, 19798051, 19798057, 19798124, 19798218, 19798354, + 19798447, 19798513, 19798547, 19798602, 19798734, 19798856, 19798935, + 19799058, 19799063, 19799066, 19799153, 19799177, 19799244, 19799323, + 19799350, 19799418, 19799463, 19799605, 19799625, 19799764, 19799832, + 19799833, 19799845, 19799896, 19799897, 19799907, 19799980, 19800011, + 19800169, 19800324, 19800555, 19800657, 19800794, 19800796, 19800816, + 19800868, 19801069, 19801083, 19801353, 19801525, 19801609, 19801650, + 19801686, 19801779, 19801845, 19801921, 19801929, 19801965, 19802092, + 19802184, 19802315, 19802334, 19802762, 19802797, 19802822, 19802836, + 19802837, 19802841, 19802886, 19802923, 19803364, 19803407, 19803528, + 19803633, 19803759, 19803765, 19803852, 19803854, 19803933, 19804429, + 19804437, + ], + }, + }; + + beforeEach(async () => { + context = new IntegralContextTest( + {}, + {} as IntegralRelayer, + relayerAddress, + ); + const onTransfer = ( + token: Address, + from: Address, + to: Address, + amount: bigint, + blockNumber: number, + ) => + onTransferUpdateBalance( + token, + from, + to, + amount, + blockNumber, + context as unknown as IntegralContext, + ); + const tokensMap = Object.values(TEST_POOLS).reduce<{ + [tokenAddress: Address]: null; + }>((memo, { token0, token1 }) => { + memo[token0.toLowerCase()] = null; + memo[token1.toLowerCase()] = null; + return memo; + }, {}); + Object.keys(tokensMap).map(tokenAddress => { + context.tokens[tokenAddress] = new IntegralToken( + dexHelper, + dexKey, + erc20Interface, + tokenAddress, + relayerAddress, + onTransfer, + logger, + ); + }); + + const integralRelayer = new IntegralRelayer( + dexHelper, + dexKey, + erc20Interface, + relayerAddress.toLowerCase(), + TEST_POOLS, + onPoolEnabledSet, + logger, + ); + context.relayer = integralRelayer; + }); + + Object.entries(eventsToTest).forEach( + ([_, events]: [string, EventMappings]) => { + describe(`Events for Pool & Relayer`, () => { + Object.entries(events).forEach( + ([eventName, blockNumbers]: [string, number[]]) => { + describe(`${eventName}`, () => { + blockNumbers.forEach((blockNumber: number) => { + it(`State after ${blockNumber}`, async function () { + await updateEventSubscriber( + context.relayer, + context.relayer.addressesSubscribed, + (_blockNumber: number) => + fetchRelayerState( + context.relayer, + _blockNumber, + relayerAddress, + ), + blockNumber, + `${dexKey}_${relayerAddress}`, + dexHelper.provider, + ); + const tokenArray = Object.values(context.tokens); + await Promise.all( + tokenArray.map(async t => + updateEventSubscriber( + t, + t.addressesSubscribed, + (_blockNumber: number) => + fetchTokenState(t, _blockNumber, t.tokenAddress), + blockNumber, + `${dexKey}_${t.tokenAddress}`, + dexHelper.provider, + ), + ), + ); + await checkEventSubscriber( + context.relayer, + (_blockNumber: number) => + fetchRelayerState( + context.relayer, + _blockNumber, + relayerAddress, + ), + blockNumber, + `${dexKey}_${relayerAddress}`, + ); + }); + }); + }); + }, + ); + }); + }, + ); +}); + +class IntegralContextTest { + constructor( + public tokens: { [tokenAddress: Address]: IntegralToken }, + public relayer: IntegralRelayer, + public relayerAddress: Address, + ) {} +} diff --git a/src/dex/integral/integral-factory.ts b/src/dex/integral/integral-factory.ts new file mode 100644 index 000000000..fff7c1db8 --- /dev/null +++ b/src/dex/integral/integral-factory.ts @@ -0,0 +1,146 @@ +import { Interface } from '@ethersproject/abi'; +import { LogDescription } from 'ethers/lib/utils'; +import { IDexHelper } from '../../dex-helper'; +import { StatefulEventSubscriber } from '../../stateful-event-subscriber'; +import { Address, BlockHeader, Log, Logger } from '../../types'; +import { FactoryState } from './types'; +import IntegralFactoryABI from '../../abi/integral/factory.json'; +import IntegralPoolABI from '../../abi/integral/pool.json'; +import { addressDecode } from '../../lib/decoders'; +import { MultiCallParams } from '../../lib/multi-wrapper'; + +export type OnPoolCreatedCallback = ( + token0: Address, + token1: Address, + poolAddress: Address, + blockNumber: number, +) => Promise; + +export class IntegralFactory extends StatefulEventSubscriber { + handlers: { + [event: string]: ( + event: any, + state: FactoryState, + blockHeader: Readonly, + ) => Promise; + } = {}; + + logDecoder: (log: Log) => any; + + public readonly factoryIface = new Interface(IntegralFactoryABI); + + constructor( + readonly dexHelper: IDexHelper, + parentName: string, + protected readonly factoryAddress: Address, + protected readonly onPoolCreated: OnPoolCreatedCallback, + logger: Logger, + mapKey: string = '', + ) { + super(parentName, `${parentName} Factory`, dexHelper, logger, true, mapKey); + + this.addressesSubscribed = [factoryAddress]; + + this.logDecoder = (log: Log) => this.factoryIface.parseLog(log); + + this.handlers['PairCreated'] = this.handlePairCreated.bind(this); + } + + async generateState(blockNumber?: number | 'latest'): Promise { + const factory = new this.dexHelper.web3Provider.eth.Contract( + IntegralFactoryABI as any, + this.factoryAddress, + ); + const poolCountResult = await factory.methods + .allPairsLength() + .call(undefined, blockNumber); + const poolCount = Number(poolCountResult); + const poolAddressCallDatas: MultiCallParams[] = [ + ...Array(poolCount), + ].map((_, i) => { + return { + target: this.factoryAddress, + callData: factory.methods.allPairs(i).encodeABI(), + decodeFunction: addressDecode, + }; + }); + const poolAddresses = + await this.dexHelper.multiWrapper.tryAggregate( + false, + poolAddressCallDatas, + blockNumber, + this.dexHelper.multiWrapper.defaultBatchSize, + false, + ); + + const poolIface = new Interface(IntegralPoolABI); + const poolInfosCallDatas: MultiCallParams[] = poolAddresses + .map(poolAddress => { + return [ + { + target: poolAddress.returnData, + callData: poolIface.encodeFunctionData('token0', []), + decodeFunction: addressDecode, + }, + { + target: poolAddress.returnData, + callData: poolIface.encodeFunctionData('token1', []), + decodeFunction: addressDecode, + }, + ]; + }) + .flat(); + const poolInfos = await this.dexHelper.multiWrapper.aggregate( + poolInfosCallDatas, + blockNumber, + ); + + const state: FactoryState = { pools: {} }; + poolAddresses.forEach((poolAddress, i) => { + const tokens: [Address, Address] = [ + poolInfos[i * 2].toLowerCase(), + poolInfos[i * 2 + 1].toLowerCase(), + ]; + state.pools[poolAddress.returnData.toLowerCase()] = { + token0: tokens[0], + token1: tokens[1], + }; + }); + return state; + } + + protected async processLog( + state: FactoryState, + log: Readonly, + blockHeader: Readonly, + ): Promise { + const event = this.logDecoder(log); + if (event.name in this.handlers) { + return await this.handlers[event.name](event, state, blockHeader); + } + + return state; + } + + async handlePairCreated( + event: LogDescription, + state: FactoryState, + blockHeader: Readonly, + ) { + const tokens: [Address, Address] = [ + event.args.token0.toLowerCase(), + event.args.token1.toLowerCase(), + ]; + state.pools[event.args.pair.toLowerCase()] = { + token0: tokens[0], + token1: tokens[1], + }; + await this.onPoolCreated( + tokens[0], + tokens[1], + event.args.pair.toLowerCase(), + blockHeader.number, + ); + return state; + } +} diff --git a/src/dex/integral/integral-integration.test.ts b/src/dex/integral/integral-integration.test.ts new file mode 100644 index 000000000..559616902 --- /dev/null +++ b/src/dex/integral/integral-integration.test.ts @@ -0,0 +1,220 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +import { DummyDexHelper } from '../../dex-helper/index'; +import { Network, SwapSide } from '../../constants'; +import { BI_POWS } from '../../bigint-constants'; +import { Integral } from './integral'; +import { + checkPoolPrices, + checkPoolsLiquidity, + checkConstantPoolPrices, +} from '../../../tests/utils'; +import { Tokens } from '../../../tests/constants-e2e'; +import { Token } from '../../types'; +import { IntegralConfig } from './config'; +import IntegralRelayerABI from '../../abi/integral/relayer.json'; +import { Interface } from '@ethersproject/abi'; +import { uint256ToBigInt } from '../../lib/decoders'; +import { MultiCallParams } from '../../lib/multi-wrapper'; + +jest.setTimeout(50 * 1000); +const network = Network.MAINNET; +const testDatas: { + tokenA: Token; + tokenB: Token; + block: number; + data: number[]; +}[] = [ + { + tokenA: { symbol: 'USDC' } as Token, + tokenB: { symbol: 'USDT' } as Token, + block: 19608294, + data: [ + 0, 5100, 10200, 15300, 20400, 25500, 30600, 35700, 40800, 45900, 51000, + ], + }, + { + tokenA: { symbol: 'USDT' } as Token, + tokenB: { symbol: 'WETH' } as Token, + block: 19831195, + data: [0, 1.7, 3.4, 5.1, 6.8, 8.5, 10.2, 11.9, 13.6, 15.3, 17], + }, +]; +initializeTokens(); + +const dexHelper = new DummyDexHelper(network); +const dexKey = 'Integral'; + +function initializeTokens() { + for (const [i, testData] of testDatas.entries()) { + const tokenA = Tokens[network][testData.tokenA.symbol!]; + const tokenB = Tokens[network][testData.tokenB.symbol!]; + tokenA.symbol = testData.tokenA.symbol; + tokenB.symbol = testData.tokenB.symbol; + testDatas[i].tokenA = tokenA; + testDatas[i].tokenB = tokenB; + } +} + +function initializeAmounts(token: Token, units: number[]) { + const precision = 10 ** 18; + return units.map( + unit => + (BigInt(unit * precision) * BI_POWS[token.decimals]) / BigInt(precision), + ); +} + +async function checkOnChainPricing( + relayerAddress: string, + funcName: string, + blockNumber: number, + prices: bigint[], + TokenA: Token, + TokenB: Token, + expectedAmounts: bigint[], +) { + const relayerInterface = new Interface(IntegralRelayerABI); + const callData = expectedAmounts + .slice(1) + .map>(amount => ({ + target: relayerAddress, + callData: relayerInterface.encodeFunctionData(funcName, [ + TokenA.address, + TokenB.address, + amount, + ]), + decodeFunction: uint256ToBigInt, + })); + const results = await dexHelper.multiWrapper.tryAggregate( + false, + callData, + blockNumber, + dexHelper.multiWrapper.defaultBatchSize, + false, + ); + + const expectedPrices = [0n].concat(results.map(result => result.returnData)); + expect(prices).toEqual(expectedPrices); +} + +for (const { tokenA, tokenB, data, block } of testDatas) { + describe(`Integral Pool ${tokenB.symbol}-${tokenA.symbol}`, function () { + let integral: Integral; + + beforeAll(async () => { + integral = new Integral(network, dexKey, dexHelper); + await integral.initializePricing(block); + }); + + it(`getPoolIdentifiers and getPricesVolume SELL [${tokenB.symbol}, ${tokenA.symbol}]`, async function () { + const amounts = initializeAmounts(tokenB, data); + const pools = await integral.getPoolIdentifiers( + tokenB, + tokenA, + SwapSide.SELL, + block, + ); + console.log( + `${tokenA.symbol} <> ${tokenB.symbol} Pool Identifiers: `, + pools, + ); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await integral.getPricesVolume( + tokenB, + tokenA, + amounts, + SwapSide.SELL, + block, + pools, + ); + console.log( + `${tokenA.symbol} <> ${tokenB.symbol} Pool Prices, SELL: `, + poolPrices, + ); + + expect(poolPrices).not.toBeNull(); + if (integral.hasConstantPriceLargeAmounts) { + checkConstantPoolPrices(poolPrices!, amounts, dexKey); + } else { + checkPoolPrices(poolPrices!, amounts, SwapSide.SELL, dexKey); + } + + // Check if onchain pricing equals to calculated ones + await checkOnChainPricing( + IntegralConfig[dexKey][network].relayerAddress, + 'quoteSell', + block, + poolPrices![0].prices, + tokenB, + tokenA, + amounts, + ); + }); + + it(`getPoolIdentifiers and getPricesVolume BUY [${tokenA.symbol}, ${tokenB.symbol}]`, async function () { + const amounts = initializeAmounts(tokenB, data); + const pools = await integral.getPoolIdentifiers( + tokenA, + tokenB, + SwapSide.BUY, + block, + ); + console.log( + `${tokenA.symbol} <> ${tokenB.symbol} Pool Identifiers: `, + pools, + ); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await integral.getPricesVolume( + tokenA, + tokenB, + amounts, + SwapSide.BUY, + block, + pools, + ); + console.log( + `${tokenA.symbol} <> ${tokenB.symbol} Pool Prices, Buy: `, + poolPrices, + ); + + expect(poolPrices).not.toBeNull(); + if (integral.hasConstantPriceLargeAmounts) { + checkConstantPoolPrices(poolPrices!, amounts, dexKey); + } else { + checkPoolPrices(poolPrices!, amounts, SwapSide.BUY, dexKey); + } + + // Check if onchain pricing equals to calculated ones + await checkOnChainPricing( + IntegralConfig[dexKey][network].relayerAddress, + 'quoteBuy', + block, + poolPrices![0].prices, + tokenA, + tokenB, + amounts, + ); + }); + }); +} + +describe('Integral Top Pools', function () { + const integral = new Integral(network, dexKey, dexHelper); + const { tokenA } = testDatas[0]; + it(`getTopPoolsForToken [${tokenA.symbol}]`, async function () { + const poolLiquidity = await integral.getTopPoolsForToken( + tokenA.address, + 10, + ); + console.log(`${tokenA.symbol} Top Pools:`, poolLiquidity); + + if (!integral.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity(poolLiquidity, tokenA.address, dexKey); + } + }); +}); diff --git a/src/dex/integral/integral-pool.ts b/src/dex/integral/integral-pool.ts new file mode 100644 index 000000000..acbf57eff --- /dev/null +++ b/src/dex/integral/integral-pool.ts @@ -0,0 +1,198 @@ +import _ from 'lodash'; +import { Interface } from '@ethersproject/abi'; +import { LogDescription } from 'ethers/lib/utils'; +import { AsyncOrSync } from 'ts-essentials'; +import { BlockHeader, Log, Logger } from '../../types'; +import { StatefulEventSubscriber } from '../../stateful-event-subscriber'; +import { IDexHelper } from '../../dex-helper/idex-helper'; +import { PoolState } from './types'; +import IntegralPoolABI from '../../abi/integral/pool.json'; +import IntegralOracleABI from '../../abi/integral/oracle.json'; +import UniswapV3PoolABI from '../../abi/uniswap-v3/UniswapV3Pool.abi.json'; +import { Address } from '../../types'; +import { MultiCallParams } from '../../lib/multi-wrapper'; +import { + addressDecode, + uint256ToBigInt, + uint8ToNumber, +} from '../../lib/decoders'; +import { AbiItem } from 'web3-utils'; +import { BigNumber } from 'ethers'; + +export class IntegralEventPool extends StatefulEventSubscriber { + handlers: { + [event: string]: ( + event: any, + pool: PoolState, + blockHeader: Readonly, + ) => AsyncOrSync; + } = {}; + + logDecoder: (log: Log) => any; + + addressesSubscribed: string[]; + + readonly poolAddress: Address; + readonly token0: Address; + readonly token1: Address; + + constructor( + public parentName: string, + protected network: number, + protected dexHelper: IDexHelper, + poolAddress: Address, + token0: Address, + token1: Address, + logger: Logger, + protected poolIface = new Interface(IntegralPoolABI), + ) { + super(parentName, `${token0}_${token1}`, dexHelper, logger); + this.token0 = token0.toLowerCase(); + this.token1 = token1.toLowerCase(); + this.poolAddress = poolAddress.toLowerCase(); + + this.logDecoder = (log: Log) => this.poolIface.parseLog(log); + this.addressesSubscribed = [this.poolAddress]; + this.handlers['SetSwapFee'] = this.handleSetSwapFee.bind(this); + this.handlers['SetMintFee'] = this.handleSetMintFee.bind(this); + this.handlers['SetBurnFee'] = this.handleSetBurnFee.bind(this); + } + + /** + * The function is called every time any of the subscribed + * addresses release log. The function accepts the current + * state, updates the state according to the log, and returns + * the updated state. + * @param state - Current state of event subscriber + * @param log - Log released by one of the subscribed addresses + * @returns Updates state of the event subscriber after the log + */ + protected async processLog( + state: PoolState, + log: Readonly, + blockHeader: Readonly, + ): Promise { + try { + const event = this.logDecoder(log); + if (event.name in this.handlers) { + return await this.handlers[event.name](event, state, blockHeader); + } + return state; + } catch (e) { + this.logger.error( + `Error_${this.parentName}_processLog could not parse the log with topic ${log.topics}:`, + e, + ); + return null; + } + } + + /** + * The function generates state using on-chain calls. This + * function is called to regenerate state if the event based + * system fails to fetch events and the local state is no + * more correct. + * @param blockNumber - Blocknumber for which the state should + * should be generated + * @returns state of the event subscriber at blocknumber + */ + async generateState(blockNumber: number): Promise> { + const state = {} as PoolState; + const pool = new this.dexHelper.web3Provider.eth.Contract( + IntegralPoolABI as AbiItem[], + this.poolAddress, + ); + const poolInfoCallDatas: MultiCallParams[] = [ + { + target: this.poolAddress, + callData: pool.methods.oracle().encodeABI(), + decodeFunction: addressDecode, + }, + { + target: this.poolAddress, + callData: pool.methods.mintFee().encodeABI(), + decodeFunction: uint256ToBigInt, + }, + { + target: this.poolAddress, + callData: pool.methods.burnFee().encodeABI(), + decodeFunction: uint256ToBigInt, + }, + { + target: this.poolAddress, + callData: pool.methods.swapFee().encodeABI(), + decodeFunction: uint256ToBigInt, + }, + ]; + const poolInfos = await this.dexHelper.multiWrapper.aggregate< + string | bigint + >(poolInfoCallDatas, blockNumber); + const oracleAddress = poolInfos[0].toString(); + + state.mintFee = BigInt(poolInfos[1]); + state.burnFee = BigInt(poolInfos[2]); + state.swapFee = BigInt(poolInfos[3]); + state.oracle = oracleAddress; + + const historyState = this.getStaleState(); + if (!historyState) { + const oracle = new this.dexHelper.web3Provider.eth.Contract( + IntegralOracleABI as AbiItem[], + oracleAddress, + ); + const oracleInfosCallDatas: MultiCallParams[] = [ + { + target: oracleAddress, + callData: oracle.methods.xDecimals().encodeABI(), + decodeFunction: uint8ToNumber, + }, + { + target: oracleAddress, + callData: oracle.methods.yDecimals().encodeABI(), + decodeFunction: uint8ToNumber, + }, + { + target: oracleAddress, + callData: oracle.methods.uniswapPair().encodeABI(), + decodeFunction: addressDecode, + }, + ]; + const oracleInfos = await this.dexHelper.multiWrapper.aggregate< + number | string + >(oracleInfosCallDatas, blockNumber); + + state.uniswapPool = oracleInfos[2].toString(); + state.decimals0 = Number(oracleInfos[0]); + state.decimals1 = Number(oracleInfos[1]); + + const uniswapPool = new this.dexHelper.web3Provider.eth.Contract( + UniswapV3PoolABI as AbiItem[], + state.uniswapPool, + ); + const fee = await uniswapPool.methods.fee().call(undefined, blockNumber); + state.uniswapPoolFee = BigInt(fee); + } else { + state.uniswapPool = historyState.uniswapPool; + state.decimals0 = historyState.decimals0; + state.decimals1 = historyState.decimals1; + state.uniswapPoolFee = historyState.uniswapPoolFee; + } + + return state; + } + + handleSetSwapFee(event: LogDescription, pool: PoolState) { + pool.swapFee = BigInt(BigNumber.from(event.args.fee).toString()); + return pool; + } + + handleSetMintFee(event: LogDescription, pool: PoolState) { + pool.mintFee = BigInt(BigNumber.from(event.args.fee).toString()); + return pool; + } + + handleSetBurnFee(event: LogDescription, pool: PoolState) { + pool.burnFee = BigInt(BigNumber.from(event.args.fee).toString()); + return pool; + } +} diff --git a/src/dex/integral/integral-pricing.ts b/src/dex/integral/integral-pricing.ts new file mode 100644 index 000000000..0a7e77428 --- /dev/null +++ b/src/dex/integral/integral-pricing.ts @@ -0,0 +1,166 @@ +import { Interface } from '@ethersproject/abi'; +import { IDexHelper } from '../../dex-helper'; +import { Address, Logger } from '../../types'; +import { UniswapV3EventPool } from '../uniswap-v3/uniswap-v3-pool'; +import { UniswapV3Config } from '../uniswap-v3/config'; +import UniswapV3StateMulticallABI from '../../abi/uniswap-v3/UniswapV3StateMulticall.abi.json'; +import { Network } from '../../constants'; +import { AbiItem } from 'web3-utils'; +import { + DecodedStateMultiCallResultWithRelativeBitmaps, + OracleObservation, + PoolState, +} from '../uniswap-v3/types'; +import { assert } from 'ts-essentials'; +import { decodeStateMultiCallResultWithObservation } from './utils'; +import { + _reduceTickBitmap, + _reduceTicks, +} from '../uniswap-v3/contract-math/utils'; +import { bigIntify } from '../../utils'; +import { TickBitMap } from '../uniswap-v3/contract-math/TickBitMap'; +import { Observation } from './types'; + +export class IntegralPricing extends UniswapV3EventPool { + private readonly providedPoolAddress: Address; + constructor( + readonly dexHelper: IDexHelper, + parentName: string, + erc20Interface: Interface, + poolAddress: Address, + token0: Address, + token1: Address, + feeCode: bigint, + logger: Logger, + network: Network, + mapKey: string = '', + ) { + const uniswapConfig = UniswapV3Config['UniswapV3'][network]; + const stateMultiContract = new dexHelper.web3Provider.eth.Contract( + uniswapConfig.stateMultiCallAbi !== undefined + ? uniswapConfig.stateMultiCallAbi + : (UniswapV3StateMulticallABI as AbiItem[]), + uniswapConfig.stateMulticall, + ); + super( + dexHelper, + parentName, + stateMultiContract, + undefined, + erc20Interface, + uniswapConfig.factory, + feeCode, + token0, + token1, + logger, + mapKey, + uniswapConfig.initHash, + ); + this.providedPoolAddress = poolAddress; + } + + get poolAddress() { + return this.providedPoolAddress; + } + + set poolAddress(_address: Address) {} + + async generateState(blockNumber: number): Promise> { + const callData = this._getStateRequestCallData(); + + const [resBalance0, resBalance1, resState] = + await this.dexHelper.multiWrapper.tryAggregate< + bigint | DecodedStateMultiCallResultWithRelativeBitmaps + >( + false, + callData, + blockNumber, + this.dexHelper.multiWrapper.defaultBatchSize, + false, + ); + + // Quite ugly solution, but this is the one that fits to current flow. + // I think UniswapV3 callbacks subscriptions are complexified for no reason. + // Need to be revisited later + assert(resState.success, 'Pool does not exist'); + + const [balance0, balance1, _state] = [ + resBalance0.returnData, + resBalance1.returnData, + resState.returnData, + ] as [bigint, bigint, DecodedStateMultiCallResultWithRelativeBitmaps]; + + const observationsCallData = [ + ...Array(_state.slot0.observationCardinality), + ].map((_, i) => { + return { + target: this.poolAddress, + callData: this.poolIface.encodeFunctionData('observations', [i]), + decodeFunction: decodeStateMultiCallResultWithObservation, + }; + }); + + const resObservations = + await this.dexHelper.multiWrapper.tryAggregate( + false, + observationsCallData, + blockNumber, + 13107, + false, + ); + const observations = resObservations.reduce((memo, obs, i) => { + memo[i] = { + blockTimestamp: BigInt(obs.returnData.blockTimestamp), + tickCumulative: BigInt(obs.returnData.tickCumulative.toString()), + secondsPerLiquidityCumulativeX128: BigInt( + obs.returnData.secondsPerLiquidityCumulativeX128.toString(), + ), + initialized: obs.returnData.initialized, + }; + return memo; + }, {} as { [index: number]: OracleObservation }); + + const tickBitmap = {}; + const ticks = {}; + + _reduceTickBitmap(tickBitmap, _state.tickBitmap); + _reduceTicks(ticks, _state.ticks); + + const currentTick = bigIntify(_state.slot0.tick); + const tickSpacing = bigIntify(_state.tickSpacing); + + const startTickBitmap = TickBitMap.position(currentTick / tickSpacing)[0]; + const requestedRange = this.getBitmapRangeToRequest(); + + return { + pool: _state.pool, + blockTimestamp: bigIntify(_state.blockTimestamp), + slot0: { + sqrtPriceX96: bigIntify(_state.slot0.sqrtPriceX96), + tick: currentTick, + observationIndex: +_state.slot0.observationIndex, + observationCardinality: +_state.slot0.observationCardinality, + observationCardinalityNext: +_state.slot0.observationCardinalityNext, + feeProtocol: bigIntify(_state.slot0.feeProtocol), + }, + liquidity: bigIntify(_state.liquidity), + fee: this.feeCode, + tickSpacing, + maxLiquidityPerTick: bigIntify(_state.maxLiquidityPerTick), + tickBitmap, + ticks, + observations, + isValid: true, + startTickBitmap, + lowestKnownTick: + (BigInt.asIntN(24, startTickBitmap - requestedRange) << 8n) * + tickSpacing, + highestKnownTick: + ((BigInt.asIntN(24, startTickBitmap + requestedRange) << 8n) + + BigInt.asIntN(24, 255n)) * + tickSpacing, + balance0, + balance1, + }; + } +} diff --git a/src/dex/integral/integral-relayer.ts b/src/dex/integral/integral-relayer.ts new file mode 100644 index 000000000..677ebe782 --- /dev/null +++ b/src/dex/integral/integral-relayer.ts @@ -0,0 +1,285 @@ +import { BigNumber, ethers } from 'ethers'; +import { Interface } from '@ethersproject/abi'; +import { LogDescription } from 'ethers/lib/utils'; +import { IDexHelper } from '../../dex-helper'; +import { StatefulEventSubscriber } from '../../stateful-event-subscriber'; +import { Address, BlockHeader, Log, Logger } from '../../types'; +import { + PoolInitProps, + RelayerPoolState, + RelayerState, + RelayerTokensState, +} from './types'; +import IntegralRelayerABI from '../../abi/integral/relayer.json'; +import { AsyncOrSync, DeepReadonly, assert } from 'ts-essentials'; +import { MultiCallParams } from '../../lib/multi-wrapper'; +import { uint256ToBigInt } from '../../lib/decoders'; +import { uint32ToNumber } from './utils'; + +export type OnPoolEnabledSetCallback = ( + poolAddress: Address, + poolState: RelayerPoolState, + blockNumber: number, +) => Promise; + +export class IntegralRelayer extends StatefulEventSubscriber { + handlers: { + [event: string]: ( + event: any, + state: RelayerState, + blockHeader: Readonly, + ) => AsyncOrSync; + } = {}; + + logDecoder: (log: Log) => any; + + private pools: { + [poolAddress: Address]: { token0: Address; token1: Address }; + } = {}; + private orders: { + [id: string]: { enqueuedAt: number; executedAt: number }; + } = {}; + public readonly relayerIface = new Interface(IntegralRelayerABI); + + constructor( + readonly dexHelper: IDexHelper, + parentName: string, + readonly erc20Interface: Interface, + readonly relayerAddress: Address, + initPools: PoolInitProps, + protected readonly onPoolEnabledSet: OnPoolEnabledSetCallback, + logger: Logger, + mapKey: string = '', + ) { + super(parentName, `${parentName} Relayer`, dexHelper, logger, true, mapKey); + this.addPools(initPools); + this.addressesSubscribed = [relayerAddress]; + this.logDecoder = (log: Log) => this.relayerIface.parseLog(log); + this.handlers['SwapFeeSet'] = this.handleSwapFeeSet.bind(this); + this.handlers['PairEnabledSet'] = this.handlePairEnabledSet.bind(this); + this.handlers['WrapEth'] = this.handleWrapEth.bind(this); + this.handlers['UnwrapWeth'] = this.handleUnwrapWeth.bind(this); + this.handlers['Upgraded'] = this.handleUpgraded.bind(this); + } + + addPools(more: PoolInitProps) { + Object.keys(more).forEach(_poolAddress => { + const poolAddress = _poolAddress.toLowerCase(); + if (!this.pools[poolAddress]) { + this.pools[poolAddress] = { + token0: more[_poolAddress].token0.toLowerCase(), + token1: more[_poolAddress].token1.toLowerCase(), + }; + } + }); + } + + getPools() { + return this.pools; + } + + executeOrder(id: bigint, blockNumber: number) { + this.orders[id.toString()].executedAt = blockNumber; + delete this.orders[id.toString()]; + } + + async generateState(blockNumber?: number | 'latest'): Promise { + const relayer = new this.dexHelper.web3Provider.eth.Contract( + IntegralRelayerABI as any, + this.relayerAddress, + ); + const pools = Object.keys(this.pools); + const tokenBalances = this.initTokens(); + const tokens = Object.keys(tokenBalances); + const relayerCallDatas: MultiCallParams[] = pools + .map(poolAddress => [ + { + target: this.relayerAddress, + callData: relayer.methods.isPairEnabled(poolAddress).encodeABI(), + decodeFunction: uint256ToBigInt, + }, + { + target: this.relayerAddress, + callData: relayer.methods.swapFee(poolAddress).encodeABI(), + decodeFunction: uint256ToBigInt, + }, + { + target: this.relayerAddress, + callData: relayer.methods.getTwapInterval(poolAddress).encodeABI(), + decodeFunction: uint32ToNumber, + }, + ]) + .flat(); + + const relayerAndTokenCallDatas = relayerCallDatas.concat( + tokens + .map(token => [ + { + target: token, + callData: this.erc20Interface.encodeFunctionData('balanceOf', [ + this.relayerAddress, + ]), + decodeFunction: uint256ToBigInt, + }, + { + target: this.relayerAddress, + callData: relayer.methods.getTokenLimitMin(token).encodeABI(), + decodeFunction: uint256ToBigInt, + }, + { + target: this.relayerAddress, + callData: relayer.methods + .getTokenLimitMaxMultiplier(token) + .encodeABI(), + decodeFunction: uint256ToBigInt, + }, + ]) + .flat(), + ); + + const relayerInfos = await this.dexHelper.multiWrapper.aggregate< + boolean | bigint | number + >(relayerAndTokenCallDatas, blockNumber); + + const [relayerItemCount, tokenItemCount] = [3, 3]; + const state: RelayerState = { pools: {}, tokens: tokenBalances }; + const offset = pools.length * relayerItemCount; + const tokenLimits: { + [tokenAddress: Address]: { min: bigint; maxMultiplier: bigint }; + } = {}; + tokens.forEach((token, i) => { + const [balance, min, maxMultiplier] = [ + BigInt(relayerInfos[i * tokenItemCount + offset]), + BigInt(relayerInfos[i * tokenItemCount + offset + 1]), + BigInt(relayerInfos[i * tokenItemCount + offset + 2]), + ]; + tokenLimits[token] = { min, maxMultiplier }; + state.tokens[token] = { balance }; + }); + + pools.forEach((poolAddress, i) => { + const [isEnabled, swapFee, twapInterval] = [ + Boolean(relayerInfos[i * relayerItemCount]), + BigInt(relayerInfos[i * relayerItemCount + 1]), + Number(relayerInfos[i * relayerItemCount + 2]), + ]; + + const min0 = tokenLimits[this.pools[poolAddress].token0].min; + const min1 = tokenLimits[this.pools[poolAddress].token1].min; + const maxMultiplier0 = + tokenLimits[this.pools[poolAddress].token0].maxMultiplier; + const maxMultiplier1 = + tokenLimits[this.pools[poolAddress].token1].maxMultiplier; + + state.pools[poolAddress] = { + isEnabled, + swapFee, + twapInterval, + limits: { min0, min1, maxMultiplier0, maxMultiplier1 }, + }; + }); + + return state; + } + + protected async processLog( + state: DeepReadonly, + log: Readonly, + blockHeader: Readonly, + ): Promise { + const event = this.logDecoder(log); + if (event.name in this.handlers) { + await this.handlers[event.name](event, state, blockHeader); + } + + return state; + } + + handleSwapFeeSet( + event: LogDescription, + state: RelayerState, + _blockHeader: Readonly, + ) { + const poolAddress = event.args.pair.toLowerCase(); + state.pools[poolAddress].swapFee = BigInt(event.args.fee); + return state; + } + + async handlePairEnabledSet( + event: LogDescription, + state: RelayerState, + blockHeader: Readonly, + ) { + const poolAddress = event.args.pair.toLowerCase(); + state.pools[poolAddress].isEnabled = Boolean(event.args.enabled); + await this.onPoolEnabledSet( + poolAddress, + state.pools[poolAddress], + blockHeader.number, + ); + return state; + } + + async handleWrapEth( + event: LogDescription, + state: RelayerState, + _blockHeader: Readonly, + ) { + const tokenAddress = + this.dexHelper.config.data.wrappedNativeTokenAddress.toLowerCase(); + const amountIn = BigNumber.from(event.args.amount).toBigInt(); + + if (state.tokens[tokenAddress]) { + state.tokens[tokenAddress].balance += amountIn; + } else { + state.tokens[tokenAddress] = { balance: amountIn }; + } + return state; + } + + async handleUnwrapWeth( + event: LogDescription, + state: RelayerState, + blockHeader: Readonly, + ) { + const tokenAddress = + this.dexHelper.config.data.wrappedNativeTokenAddress.toLowerCase(); + const amountOut = BigNumber.from(event.args.amount).toBigInt(); + // state balance is out of sync + if ( + !state.tokens[tokenAddress] || + state.tokens[tokenAddress].balance < amountOut + ) { + return await this.generateState(blockHeader.number); + } + state.tokens[tokenAddress].balance -= amountOut; + return state; + } + + async handleUpgraded( + _event: LogDescription, + state: RelayerState, + blockHeader: Readonly, + ) { + try { + return await this.generateState(blockHeader.number); + } catch (e) { + this.logger.error( + `Integral Relayer: Contract upgraded, please upgrade event pool`, + e, + ); + return state; + } + } + + private initTokens() { + return Object.entries(this.pools).reduce( + (memo, [, { token0, token1 }]) => { + memo[token0] = (!!memo[token0] && memo[token0]) || { balance: 0n }; + memo[token1] = (!!memo[token1] && memo[token1]) || { balance: 0n }; + return memo; + }, + {}, + ); + } +} diff --git a/src/dex/integral/integral-token.ts b/src/dex/integral/integral-token.ts new file mode 100644 index 000000000..77c8ebcb6 --- /dev/null +++ b/src/dex/integral/integral-token.ts @@ -0,0 +1,93 @@ +import { BigNumber } from 'ethers'; +import { Interface } from '@ethersproject/abi'; +import { LogDescription } from 'ethers/lib/utils'; +import { IDexHelper } from '../../dex-helper'; +import { StatefulEventSubscriber } from '../../stateful-event-subscriber'; +import { Address, BlockHeader, Log, Logger } from '../../types'; +import { TokenState } from './types'; + +export type OnTransferCallback = ( + token: Address, + from: Address, + to: Address, + amount: bigint, + blockNumber: number, +) => Promise; + +export class IntegralToken extends StatefulEventSubscriber { + handlers: { + [event: string]: ( + event: any, + state: TokenState, + blockHeader: Readonly, + ) => Promise; + } = {}; + + logDecoder: (log: Log) => any; + + constructor( + readonly dexHelper: IDexHelper, + parentName: string, + protected readonly erc20Interface: Interface, + readonly tokenAddress: Address, + readonly relayerAddress: Address, + protected readonly onTransfer: OnTransferCallback, + logger: Logger, + mapKey: string = '', + ) { + super(parentName, `ERC20 Token`, dexHelper, logger, true, mapKey); + this.addressesSubscribed = [tokenAddress]; + this.logDecoder = (log: Log) => { + let ret; + try { + ret = this.erc20Interface.parseLog(log); + } catch (error) { + if (error instanceof Error) { + if (error.message.startsWith('no matching event')) { + // ignore custom events for some tokens + return null; + } + } + throw error; + } + return ret; + }; + this.handlers['Transfer'] = this.handleTransfer.bind(this); + } + + async generateState(): Promise { + return {}; + } + + protected async processLog( + state: TokenState, + log: Readonly, + blockHeader: Readonly, + ): Promise { + const event = this.logDecoder(log); + if (event && event.name in this.handlers) { + await this.handlers[event.name](event, state, blockHeader); + } + return state; + } + + async handleTransfer( + event: LogDescription, + state: TokenState, + _blockHeader: Readonly, + ) { + const from = event.args[0].toLowerCase(); + const to = event.args[1].toLowerCase(); + if (from === this.relayerAddress || to === this.relayerAddress) { + const amount = BigNumber.from(event.args[2]).toBigInt(); + await this.onTransfer( + this.tokenAddress, + from, + to, + amount, + _blockHeader.number, + ); + } + return state; + } +} diff --git a/src/dex/integral/integral.ts b/src/dex/integral/integral.ts new file mode 100644 index 000000000..cb4a6fedf --- /dev/null +++ b/src/dex/integral/integral.ts @@ -0,0 +1,527 @@ +import _ from 'lodash'; +import { DeepReadonly } from 'ts-essentials'; +import * as CALLDATA_GAS_COST from '../../calldata-gas-cost'; +import { + Token, + Address, + ExchangePrices, + AdapterExchangeParam, + SimpleExchangeParam, + PoolLiquidity, + Logger, + PoolPrices, +} from '../../types'; +import { SwapSide, Network } from '../../constants'; +import { + _require, + getBigIntPow, + getDexKeysWithNetwork, + isETHAddress, +} from '../../utils'; +import { IDex } from '../idex'; +import { IDexHelper } from '../../dex-helper/idex-helper'; +import { + IntegralData, + IntegralFunctions, + PoolStates, + QuotingProps, + RelayerPoolState, + RelayerTokensState, +} from './types'; +import { SimpleExchange } from '../simple-exchange'; +import { IntegralConfig, Adapters } from './config'; +import IntegralRelayerABI from '../../abi/integral/relayer.json'; +import { Interface } from '@ethersproject/abi'; +import { BI_POWS } from '../../bigint-constants'; +import { + getDecimalsConverter, + getPoolIdentifier, + getPrice, + isInverted, +} from './helpers'; +import { ceil_div, sortTokens } from './utils'; +import { IntegralContext } from './context'; + +const PRECISION = BI_POWS[18]; +const SUBGRAPH_TIMEOUT = 20 * 1000; +const relayerInterface = new Interface(IntegralRelayerABI); + +export class Integral extends SimpleExchange implements IDex { + protected subgraphURL: string | undefined; + readonly hasConstantPriceLargeAmounts = false; + + private readonly context: IntegralContext; + readonly isFeeOnTransferSupported = false; + + public static dexKeysWithNetwork: { key: string; networks: Network[] }[] = + getDexKeysWithNetwork(IntegralConfig); + + logger: Logger; + + constructor( + readonly network: Network, + readonly dexKey: string, + readonly dexHelper: IDexHelper, + protected adapters = Adapters[network] || {}, + ) { + super(dexHelper, dexKey); + this.logger = dexHelper.getLogger(dexKey); + + this.context = IntegralContext.initialize( + network, + dexKey, + dexHelper, + this.erc20Interface, + IntegralConfig[dexKey][network].factoryAddress.toLowerCase(), + IntegralConfig[dexKey][network].relayerAddress.toLowerCase(), + ); + this.subgraphURL = IntegralConfig[dexKey][network].subgraphURL; + } + + async initializePricing(blockNumber: number) { + await this.context.factory.initialize(blockNumber); + const factoryState = this.context.factory.getState(blockNumber); + if (factoryState) { + await this.context.addPools(factoryState.pools, blockNumber, true); + } + } + + // Returns the list of contract adapters (name and index) + // for a buy/sell. Return null if there are no adapters. + getAdapters(side: SwapSide): { name: string; index: number }[] | null { + return this.adapters[side] ? this.adapters[side] : null; + } + + async getPoolIdentifiers( + srcToken: Token, + destToken: Token, + side: SwapSide, + blockNumber: number, + ): Promise { + const src = this.dexHelper.config.wrapETH(srcToken); + const dest = this.dexHelper.config.wrapETH(destToken); + + if (src.address.toLowerCase() === dest.address.toLowerCase()) { + return []; + } + + const tokenAddresses = [ + src.address.toLowerCase(), + dest.address.toLowerCase(), + ] + .sort((a, b) => (a > b ? 1 : -1)) + .join('_'); + + const poolIdentifier = `${this.dexKey}_${tokenAddresses}`; + return [poolIdentifier]; + } + + // Returns pool prices for amounts. + // If limitPools is defined only pools in limitPools + // should be used. If limitPools is undefined then + // any pools can be used. + async getPricesVolume( + srcToken: Token, + destToken: Token, + amounts: bigint[], + side: SwapSide, + blockNumber: number, + limitPools?: string[], + ): Promise> { + const src = this.dexHelper.config.wrapETH(srcToken); + const dest = this.dexHelper.config.wrapETH(destToken); + if (src.address.toLowerCase() === dest.address.toLowerCase()) { + return null; + } + + const poolIdentifier = getPoolIdentifier( + this.dexKey, + src.address, + dest.address, + ); + if (limitPools && limitPools.every(p => p !== poolIdentifier)) { + return null; + } + + const props = await this.getQuotingProps(src, dest, blockNumber); + if (!props) { + return null; + } + + const unitAmount = getBigIntPow( + side == SwapSide.SELL ? src.decimals : dest.decimals, + ); + const unit = + side == SwapSide.SELL + ? this.quoteSell(unitAmount, props, false) + : this.quoteBuy(unitAmount, props, false); + try { + const prices = + side == SwapSide.SELL + ? amounts.map(amount => this.quoteSell(amount, props)) + : amounts.map(amount => this.quoteBuy(amount, props)); + + return [ + { + prices: prices, + unit: unit, + data: { + relayer: this.context.relayerAddress, + }, + exchange: this.dexKey, + poolIdentifier, + gasCost: 0, + poolAddresses: this.context.getPoolAddresses(), + }, + ]; + } catch (e) { + this.logger.error( + `Error_getPricesVolume could not get price with error:`, + e, + ); + return null; + } + } + + // Returns estimated gas cost of calldata for this DEX in multiSwap + getCalldataGasCost(poolPrices: PoolPrices): number | number[] { + return CALLDATA_GAS_COST.DEX_NO_PAYLOAD; + } + + // Encode params required by the exchange adapter + // Used for multiSwap, buy & megaSwap + // Hint: abiCoder.encodeParameter() could be useful + getAdapterParam( + srcToken: string, + destToken: string, + srcAmount: string, + destAmount: string, + data: IntegralData, + side: SwapSide, + ): AdapterExchangeParam { + const { relayer: exchange } = data; + + // Encode here the payload for adapter + const payload = '0x'; + + return { + targetExchange: exchange, + payload, + networkFee: '0', + }; + } + + // Encode call data used by simpleSwap like routers + // Used for simpleSwap & simpleBuy + // Hint: this.buildSimpleParamWithoutWETHConversion + // could be useful + async getSimpleParam( + srcToken: string, + destToken: string, + srcAmount: string, + destAmount: string, + data: IntegralData, + side: SwapSide, + ): Promise { + const { relayer: exchange } = data; + + // Encode here the transaction arguments + const swapData = relayerInterface.encodeFunctionData( + side === SwapSide.SELL ? IntegralFunctions.swap : IntegralFunctions.buy, + [ + { + ...{ + tokenIn: this.dexHelper.config.wrapETH({ + address: srcToken, + } as Token).address, + tokenOut: this.dexHelper.config.wrapETH({ + address: destToken, + } as Token).address, + wrapUnwrap: isETHAddress(srcToken) || isETHAddress(destToken), + to: this.augustusAddress, + submitDeadline: Math.floor(Date.now() / 1000) + 24 * 3600, + }, + ...(side === SwapSide.SELL + ? { amountIn: srcAmount, amountOutMin: destAmount } + : { amountInMax: srcAmount, amountOut: destAmount }), + }, + ], + ); + + return this.buildSimpleParamWithoutWETHConversion( + srcToken, + srcAmount, + destToken, + destAmount, + swapData, + exchange, + ); + } + + async updatePoolState(): Promise {} + + // Returns list of top pools based on liquidity. Max + // limit number pools should be returned. + async getTopPoolsForToken( + tokenAddress: Address, + limit: number, + ): Promise { + if (!this.subgraphURL) { + return []; + } + const query = ` + query ($token: Bytes!, $count: Int) { + pools0: pairs( + first: $count + orderBy: reserveUSD + orderDirection: desc + where: {token0: $token} + ) { + id + token0 { + id + decimals + } + token1 { + id + decimals + } + reserveUSD + } + pools1: pairs( + first: $count + orderBy: reserveUSD + orderDirection: desc + where: {token1: $token} + ) { + id + token0 { + id + decimals + } + token1 { + id + decimals + } + reserveUSD + } + }`; + const { data } = await this.dexHelper.httpRequest.post( + this.subgraphURL, + { + query, + variables: { token: tokenAddress.toLowerCase(), limit }, + }, + SUBGRAPH_TIMEOUT, + ); + + if (!(data && data.pools0 && data.pools1)) + throw new Error("Couldn't fetch the pools from the subgraph"); + const pools0 = _.map(data.pools0, pool => ({ + exchange: this.dexKey, + address: pool.id.toLowerCase(), + connectorTokens: [ + { + address: pool.token1.id.toLowerCase(), + decimals: parseInt(pool.token1.decimals), + }, + ], + liquidityUSD: parseFloat(pool.reserveUSD), + })); + + const pools1 = _.map(data.pools1, pool => ({ + exchange: this.dexKey, + address: pool.id.toLowerCase(), + connectorTokens: [ + { + address: pool.token0.id.toLowerCase(), + decimals: parseInt(pool.token0.decimals), + }, + ], + liquidityUSD: parseFloat(pool.reserveUSD), + })); + + return _.slice( + _.sortBy(_.concat(pools0, pools1), [pool => -1 * pool.liquidityUSD]), + 0, + limit, + ); + } + + async getPriceOnChain( + tokenIn: Token, + tokenOut: Token, + inverted: boolean, + blockNumber: number, + ): Promise { + try { + const calldata = [ + { + target: this.context.relayerAddress, + callData: relayerInterface.encodeFunctionData('getPoolState', [ + tokenIn.address, + tokenOut.address, + ]), + }, + ]; + const data: { returnData: any[] } = + await this.dexHelper.multiContract.methods + .aggregate(calldata) + .call({}, blockNumber); + + const result = relayerInterface.decodeFunctionResult( + 'getPoolState', + data.returnData[0], + ); + const _limits = inverted + ? [result.limitMin1, result.limitMax1] + : [result.limitMin0, result.limitMax0]; + const limits: [bigint, bigint] = [ + BigInt(_limits[0].toString()), + BigInt(_limits[1].toString()), + ]; + const [decimals0, decimals1] = inverted + ? [tokenOut.decimals, tokenIn.decimals] + : [tokenIn.decimals, tokenOut.decimals]; + return { + price: BigInt(result.price.toString()), + fee: BigInt(result.fee.toString()), + tokenOutLimits: limits, + decimalsConverter: getDecimalsConverter(decimals0, decimals1, inverted), + }; + } catch (e) { + this.logger.error( + `Error_getPriceOnChain could not get data with error:`, + e, + ); + return null; + } + } + + private async getQuotingProps(src: Token, dest: Token, blockNumber: number) { + const poolId = getPoolIdentifier(this.dexKey, src.address, dest.address); + const inverted = isInverted(src.address, dest.address); + + const states = this.getStates(poolId, blockNumber); + if (!states) { + return await this.getPriceOnChain(src, dest, inverted, blockNumber); + } + const { base, poolAddress, relayer, relayerTokens } = states; + + const price = getPrice(states, inverted); + + const token0 = sortTokens(src.address, dest.address)[0]; + const { max0, max1 } = this.getMaxLimits( + poolAddress, + relayer, + relayerTokens, + ); + const props: QuotingProps = { + price, + fee: relayer.swapFee, + tokenOutLimits: + dest.address === token0 + ? [relayer.limits.min0, max0] + : [relayer.limits.min1, max1], + decimalsConverter: getDecimalsConverter( + base.decimals0, + base.decimals1, + inverted, + ), + }; + return props; + } + + private quoteSell( + amountIn: bigint, + props: DeepReadonly, + checkLimit: boolean = true, + ) { + if (amountIn === 0n) { + return 0n; + } + + const fee = (amountIn * props.fee) / PRECISION; + const amountInMinusFee = amountIn - fee; + const amountOut = + (amountInMinusFee * props.price) / props.decimalsConverter; + if (checkLimit && !this.checkLimits(amountOut, props.tokenOutLimits)) { + throw new Error( + `Out of Limits - amountOut ${amountOut} not in between limits ${props.tokenOutLimits}`, + ); + } + return amountOut; + } + + private quoteBuy( + amountOut: bigint, + props: DeepReadonly, + checkLimit: boolean = true, + ) { + if (amountOut === 0n) { + return 0n; + } + + if (checkLimit && !this.checkLimits(amountOut, props.tokenOutLimits)) { + throw new Error( + `Out of Limits - amountOut ${amountOut} not in between limits ${props.tokenOutLimits}`, + ); + } + + const amountIn = ceil_div(amountOut * props.decimalsConverter, props.price); + if (amountIn <= 0n) { + return 0n; + } + const amountInPlusFee = ceil_div( + amountIn * PRECISION, + PRECISION - props.fee, + ); + return amountInPlusFee; + } + + private checkLimits(amount: bigint, limits: readonly [bigint, bigint]) { + if (amount < limits[0] || amount > limits[1]) { + return false; + } + return true; + } + + private getStates(poolId: string, blockNumber: number): PoolStates | null { + const poolStates = this.context.pools[poolId]; + if (poolStates.base) { + const base = poolStates.base.getState(blockNumber); + const pricing = + poolStates.pricing && poolStates.pricing.getState(blockNumber); + const relayerState = this.context.relayer.getState(blockNumber); + const relayer = + relayerState && relayerState.pools[poolStates.base.poolAddress]; + return base && pricing && relayer + ? { + base, + pricing, + relayer, + relayerTokens: relayerState.tokens, + poolAddress: poolStates.base.poolAddress, + } + : null; + } else { + return null; + } + } + + private getMaxLimits( + poolAddress: Address, + relayer: RelayerPoolState, + relayerTokens: RelayerTokensState, + ) { + const { token0, token1 } = + this.context.relayer.getPools()[poolAddress.toLowerCase()]; + const max0 = + (relayerTokens[token0].balance * relayer.limits.maxMultiplier0) / + 10n ** 18n; + const max1 = + (relayerTokens[token1].balance * relayer.limits.maxMultiplier1) / + 10n ** 18n; + return { max0, max1 }; + } +} diff --git a/src/dex/integral/tests/local-paraswap-sdk.ts b/src/dex/integral/tests/local-paraswap-sdk.ts new file mode 100644 index 000000000..a8956b354 --- /dev/null +++ b/src/dex/integral/tests/local-paraswap-sdk.ts @@ -0,0 +1,282 @@ +import * as _ from 'lodash'; +import { + DummyDexHelper, + DummyLimitOrderProvider, + IDexHelper, +} from '../../../dex-helper'; +import BigNumber from 'bignumber.js'; +import { TransactionBuilder } from '../../../transaction-builder'; +import { PricingHelper } from '../../../pricing-helper'; +import { DexAdapterService } from '../..'; +import { + Address, + Token, + OptimalRate, + TxObject, + TransferFeeParams, +} from '../../../types'; +import { SwapSide, NULL_ADDRESS, ContractMethod } from '../../../constants'; +import { LimitOrderExchange } from '../../limit-order-exchange'; +import { v4 as uuid } from 'uuid'; +import { DirectContractMethods } from '@paraswap/core/build/constants'; +import { ParaSwapVersion } from '@paraswap/core'; + +export interface IParaSwapSDK { + getPrices( + from: Token, + to: Token, + amount: bigint, + side: SwapSide, + contractMethod: ContractMethod, + _poolIdentifiers?: string[], + transferFees?: TransferFeeParams, + ): Promise; + + buildTransaction( + priceRoute: OptimalRate, + minMaxAmount: BigInt, + userAddress: Address, + ): Promise; + + initializePricing?(): Promise; + + releaseResources?(): Promise; + + dexHelper?: IDexHelper & { + replaceProviderWithRPC?: (rpcUrl: string) => void; + }; +} + +const chunks = 10; + +export class LocalParaswapSDK implements IParaSwapSDK { + dexHelper: IDexHelper; + dexAdapterService: DexAdapterService; + pricingHelper: PricingHelper; + transactionBuilder: TransactionBuilder; + + constructor( + protected network: number, + protected dexKey: string, + rpcUrl: string, + limitOrderProvider?: DummyLimitOrderProvider, + protected blockNumber?: number, + ) { + this.dexHelper = new DummyDexHelper(this.network, rpcUrl); + this.dexAdapterService = new DexAdapterService( + this.dexHelper, + this.network, + ); + this.pricingHelper = new PricingHelper( + this.dexAdapterService, + this.dexHelper.getLogger, + ); + this.transactionBuilder = new TransactionBuilder(this.dexAdapterService); + + const dex = this.dexAdapterService.getDexByKey(dexKey); + if (limitOrderProvider && dex instanceof LimitOrderExchange) { + dex.limitOrderProvider = limitOrderProvider; + } + } + + async initializePricing() { + const blockNumber = + this.blockNumber || + (await this.dexHelper.web3Provider.eth.getBlockNumber()); + await this.pricingHelper.initialize(blockNumber, [this.dexKey]); + } + + async releaseResources() { + await this.pricingHelper.releaseResources([this.dexKey]); + } + + async getPrices( + from: Token, + to: Token, + amount: bigint, + side: SwapSide, + contractMethod: ContractMethod, + _poolIdentifiers?: string[], + transferFees?: TransferFeeParams, + ): Promise { + const blockNumber = await this.dexHelper.web3Provider.eth.getBlockNumber(); + const poolIdentifiers = + (_poolIdentifiers && { [this.dexKey]: _poolIdentifiers }) || + (await this.pricingHelper.getPoolIdentifiers( + from, + to, + side, + blockNumber, + [this.dexKey], + )); + + const amounts = _.range(0, chunks + 1).map( + i => (amount * BigInt(i)) / BigInt(chunks), + ); + const poolPrices = await this.pricingHelper.getPoolPrices( + from, + to, + amounts, + side, + blockNumber, + [this.dexKey], + poolIdentifiers, + transferFees, + ); + + if (!poolPrices || poolPrices.length == 0) + throw new Error('Fail to get price for ' + this.dexKey); + + const finalPrice = poolPrices[0]; + const quoteAmount = finalPrice.prices[chunks]; + const srcAmount = ( + side === SwapSide.SELL ? amount : quoteAmount + ).toString(); + const destAmount = ( + side === SwapSide.SELL ? quoteAmount : amount + ).toString(); + + // eslint-disable-next-line no-console + console.log( + `Estimated gas cost for ${this.dexKey}: ${ + Array.isArray(finalPrice.gasCost) + ? finalPrice.gasCost[finalPrice.gasCost.length - 1] + : finalPrice.gasCost + }`, + ); + + const unoptimizedRate = { + blockNumber, + network: this.network, + srcToken: from.address, + srcDecimals: from.decimals, + srcAmount, + destToken: to.address, + destDecimals: to.decimals, + destAmount, + bestRoute: [ + { + percent: 100, + swaps: [ + { + srcToken: from.address, + srcDecimals: from.decimals, + destToken: to.address, + destDecimals: to.decimals, + swapExchanges: [ + { + exchange: finalPrice.exchange, + srcAmount, + destAmount, + percent: 100, + data: finalPrice.data, + poolAddresses: finalPrice.poolAddresses, + }, + ], + }, + ], + }, + ], + gasCostUSD: '0', + gasCost: '0', + others: [], + side, + version: ParaSwapVersion.V5, + tokenTransferProxy: this.dexHelper.config.data.tokenTransferProxyAddress, + contractAddress: this.dexHelper.config.data.augustusAddress, + }; + + const optimizedRate = this.pricingHelper.optimizeRate(unoptimizedRate); + + return { + ...optimizedRate, + hmac: '0', + srcUSD: '0', + destUSD: '0', + contractMethod, + partnerFee: 0, + }; + } + + async buildTransaction( + priceRoute: OptimalRate, + minMaxAmount: BigInt, + userAddress: Address, + ) { + // Set deadline to be 10 min from now + let deadline = Number((Math.floor(Date.now() / 1000) + 10 * 60).toFixed()); + + const slippageFactor = new BigNumber(minMaxAmount.toString()).div( + priceRoute.side === SwapSide.SELL + ? priceRoute.destAmount + : priceRoute.srcAmount, + ); + + const contractMethod = priceRoute.contractMethod; + + // Call preprocessTransaction for each exchange before we build transaction + try { + priceRoute.bestRoute = await Promise.all( + priceRoute.bestRoute.map(async route => { + route.swaps = await Promise.all( + route.swaps.map(async swap => { + swap.swapExchanges = await Promise.all( + swap.swapExchanges.map(async exchange => { + // Search in dexLib dexes + const dexLibExchange = this.pricingHelper.getDexByKey( + exchange.exchange, + ); + + if (dexLibExchange && dexLibExchange.preProcessTransaction) { + if (!dexLibExchange.getTokenFromAddress) { + throw new Error( + 'If you want to test preProcessTransaction, first need to implement getTokenFromAddress function', + ); + } + + const [preprocessedRoute, txInfo] = + await dexLibExchange.preProcessTransaction( + exchange, + dexLibExchange.getTokenFromAddress(swap.srcToken), + dexLibExchange.getTokenFromAddress(swap.destToken), + priceRoute.side, + { + slippageFactor, + txOrigin: userAddress, + isDirectMethod: DirectContractMethods.includes( + contractMethod as ContractMethod, + ), + }, + ); + + deadline = + txInfo.deadline && Number(txInfo.deadline) < deadline + ? Number(txInfo.deadline) + : deadline; + + return preprocessedRoute; + } + return exchange; + }), + ); + return swap; + }), + ); + return route; + }), + ); + } catch (e) { + throw e; + } + + return await this.transactionBuilder.build({ + priceRoute, + minMaxAmount: minMaxAmount.toString(), + userAddress, + partnerAddress: NULL_ADDRESS, + partnerFeePercent: '0', + deadline: deadline.toString(), + uuid: uuid(), + }); + } +} diff --git a/src/dex/integral/tests/tenderly-simulation.ts b/src/dex/integral/tests/tenderly-simulation.ts new file mode 100644 index 000000000..1a812892d --- /dev/null +++ b/src/dex/integral/tests/tenderly-simulation.ts @@ -0,0 +1,160 @@ +/* eslint-disable no-console */ +import { Address } from '@paraswap/core'; +import axios from 'axios'; +import { TxObject } from '../../../types'; + +const TENDERLY_TOKEN = process.env.TENDERLY_TOKEN; +const TENDERLY_ACCOUNT_ID = process.env.TENDERLY_ACCOUNT_ID; +const TENDERLY_PROJECT = process.env.TENDERLY_PROJECT; +const TENDERLY_FORK_ID = process.env.TENDERLY_FORK_ID; +const TENDERLY_FORK_LAST_TX_ID = process.env.TENDERLY_FORK_LAST_TX_ID; + +type SimulationResult = { + success: boolean; + gasUsed?: string; + url?: string; + transaction?: any; +}; + +type StateOverride = { + value: Record; +}; + +export type StateOverrides = { + networkID: string; + stateOverrides: Record; +}; + +type StateSimulateApiOverride = { + storage: { + value: Record; + }; +}; + +export interface TransactionSimulator { + forkId: string; + setup(): Promise; + + simulate( + params: TxObject, + stateOverrides?: StateOverrides, + ): Promise; +} + +export class TenderlySimulation implements TransactionSimulator { + lastTx: string = ''; + forkId: string = ''; + maxGasLimit = 80000000; + + constructor(private network: Number = 1, private blockNumber?: number) {} + + async setup() { + // Fork the mainnet + if (!TENDERLY_TOKEN) + throw new Error( + `TenderlySimulation_setup: TENDERLY_TOKEN not found in the env`, + ); + + if (TENDERLY_FORK_ID) { + if (!TENDERLY_FORK_LAST_TX_ID) throw new Error('Always set last tx id'); + this.forkId = TENDERLY_FORK_ID; + this.lastTx = TENDERLY_FORK_LAST_TX_ID; + return; + } + + try { + await process.nextTick(() => {}); // https://stackoverflow.com/questions/69169492/async-external-function-leaves-open-handles-jest-supertest-express + let res = await axios.post( + `https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT_ID}/project/${TENDERLY_PROJECT}/fork`, + { + network_id: this.network.toString(), + ...(this.blockNumber && { block_number: this.blockNumber }), + }, + { + timeout: 20000, + headers: { + 'x-access-key': TENDERLY_TOKEN, + }, + }, + ); + this.forkId = res.data.simulation_fork.id; + this.lastTx = res.data.root_transaction.id; + } catch (e) { + console.error(`TenderlySimulation_setup:`, e); + throw e; + } + } + + async simulate(params: TxObject, stateOverrides?: StateOverrides) { + let _params = { + from: params.from, + to: params.to, + save: true, + root: this.lastTx, + value: params.value || '0', + gas: this.maxGasLimit, + input: params.data, + state_objects: {}, + }; + try { + if (stateOverrides) { + await process.nextTick(() => {}); // https://stackoverflow.com/questions/69169492/async-external-function-leaves-open-handles-jest-supertest-express + const result = await axios.post( + ` + https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT_ID}/project/${TENDERLY_PROJECT}/contracts/encode-states`, + stateOverrides, + { + headers: { + 'x-access-key': TENDERLY_TOKEN!, + }, + }, + ); + + _params.state_objects = Object.keys(result.data.stateOverrides).reduce( + (acc, contract) => { + const _storage = result.data.stateOverrides[contract].value; + + acc[contract] = { + storage: _storage, + }; + return acc; + }, + {} as Record, + ); + } + + await process.nextTick(() => {}); // https://stackoverflow.com/questions/69169492/async-external-function-leaves-open-handles-jest-supertest-express + const { data } = await axios.post( + `https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT_ID}/project/${TENDERLY_PROJECT}/fork/${this.forkId}/simulate`, + _params, + { + timeout: 20 * 1000, + headers: { + 'x-access-key': TENDERLY_TOKEN!, + }, + }, + ); + const lastTx = data.simulation.id; + if (data.transaction.status) { + this.lastTx = lastTx; + return { + success: true, + gasUsed: data.transaction.gas_used, + url: `https://dashboard.tenderly.co/${TENDERLY_ACCOUNT_ID}/${TENDERLY_PROJECT}/fork/${this.forkId}/simulation/${lastTx}`, + transaction: data.transaction, + }; + } else { + return { + success: false, + url: `https://dashboard.tenderly.co/${TENDERLY_ACCOUNT_ID}/${TENDERLY_PROJECT}/fork/${this.forkId}/simulation/${lastTx}`, + error: `Simulation failed: ${data.transaction.error_info.error_message} at ${data.transaction.error_info.address}`, + }; + } + } catch (e) { + console.error(`TenderlySimulation_simulate:`, e); + return { + success: false, + }; + } + } +} diff --git a/src/dex/integral/tests/utils-e2e.ts b/src/dex/integral/tests/utils-e2e.ts new file mode 100644 index 000000000..61ab6bca4 --- /dev/null +++ b/src/dex/integral/tests/utils-e2e.ts @@ -0,0 +1,409 @@ +/* eslint-disable no-console */ +import { Interface } from '@ethersproject/abi'; +import { Provider } from '@ethersproject/providers'; +import { IParaSwapSDK, LocalParaswapSDK } from './local-paraswap-sdk'; +import { + TenderlySimulation, + TransactionSimulator, +} from './tenderly-simulation'; +import { + SwapSide, + ETHER_ADDRESS, + MAX_UINT, + Network, + ContractMethod, +} from '../../../constants'; +import { + OptimalRate, + TxObject, + Address, + Token, + TransferFeeParams, +} from '../../../types'; +import Erc20ABI from '../../../abi/erc20.json'; +import AugustusABI from '../../../abi/augustus.json'; +import { generateConfig } from '../../../config'; +import { DummyLimitOrderProvider } from '../../../dex-helper'; +import { constructSimpleSDK, SimpleFetchSDK } from '@paraswap/sdk'; +import axios from 'axios'; +import { generateDeployBytecode, sleep } from './utils'; + +export const testingEndpoint = process.env.E2E_TEST_ENDPOINT; + +const testContractProjectRootPath = process.env.TEST_CONTRACT_PROJECT_ROOT_PATH; +const testContractName = process.env.TEST_CONTRACT_NAME; +const testContractConfigFileName = process.env.TEST_CONTRACT_CONFIG_FILE_NAME; +const testContractRelativePath = process.env.TEST_CONTRACT_RELATIVE_PATH; +// Comma separated fields from config or actual values +const testContractDeployArgs = process.env.TEST_CONTRACT_DEPLOY_ARGS; + +// If you want to test against deployed and verified contract +const deployedTestContractAddress = process.env.DEPLOYED_TEST_CONTRACT_ADDRESS; +const testContractType = process.env.TEST_CONTRACT_TYPE; + +// Only for router tests +const testDirectRouterAbiPath = process.env.TEST_DIRECT_ROUTER_ABI_PATH; + +const directRouterIface = new Interface( + testDirectRouterAbiPath ? require(testDirectRouterAbiPath) : '[]', +); + +const testContractBytecode = generateDeployBytecode( + testContractProjectRootPath, + testContractName, + testContractConfigFileName, + testContractRelativePath, + testContractDeployArgs, + testContractType, +); + +const erc20Interface = new Interface(Erc20ABI); +const augustusInterface = new Interface(AugustusABI); + +const DEPLOYER_ADDRESS: { [nid: number]: string } = { + [Network.MAINNET]: '0xbe0eb53f46cd790cd13851d5eff43d12404d33e8', + [Network.BSC]: '0xf68a4b64162906eff0ff6ae34e2bb1cd42fef62d', + [Network.POLYGON]: '0x05182E579FDfCf69E4390c3411D8FeA1fb6467cf', + [Network.FANTOM]: '0x05182E579FDfCf69E4390c3411D8FeA1fb6467cf', + [Network.AVALANCHE]: '0xD6216fC19DB775Df9774a6E33526131dA7D19a2c', + [Network.OPTIMISM]: '0xf01121e808F782d7F34E857c27dA31AD1f151b39', + [Network.ARBITRUM]: '0xb38e8c17e38363af6ebdcb3dae12e0243582891d', +}; + +const MULTISIG: { [nid: number]: string } = { + [Network.MAINNET]: '0x36fEDC70feC3B77CAaf50E6C524FD7e5DFBD629A', + [Network.BSC]: '0xf14bed2cf725E79C46c0Ebf2f8948028b7C49659', + [Network.POLYGON]: '0x46DF4eb6f7A3B0AdF526f6955b15d3fE02c618b7', + [Network.FANTOM]: '0xECaB2dac955b94e49Ec09D6d68672d3B397BbdAd', + [Network.AVALANCHE]: '0x1e2ECA5e812D08D2A7F8664D69035163ff5BfEC2', + [Network.OPTIMISM]: '0x3b28A6f6291f7e8277751f2911Ac49C585d049f6', + [Network.ARBITRUM]: '0x90DfD8a6454CFE19be39EaB42ac93CD850c7f339', + [Network.BASE]: '0x6C674c8Df1aC663b822c4B6A56B4E5e889379AE0', +}; + +class APIParaswapSDK implements IParaSwapSDK { + paraSwap: SimpleFetchSDK; + + constructor(protected network: number, protected dexKey: string) { + this.paraSwap = constructSimpleSDK({ + chainId: network, + axios, + apiURL: testingEndpoint, + }); + } + + async getPrices( + from: Token, + to: Token, + amount: bigint, + side: SwapSide, + contractMethod: ContractMethod, + _poolIdentifiers?: string[], + ): Promise { + if (_poolIdentifiers) + throw new Error('PoolIdentifiers is not supported by the API'); + + const priceRoute = await this.paraSwap.swap.getRate({ + srcToken: from.address, + destToken: to.address, + side, + amount: amount.toString(), + options: { + includeDEXS: [this.dexKey], + // TODO: Improve typing + includeContractMethods: [contractMethod as any], + partner: 'any', + }, + srcDecimals: from.decimals, + destDecimals: to.decimals, + }); + return priceRoute as OptimalRate; + } + + async buildTransaction( + priceRoute: OptimalRate, + _minMaxAmount: BigInt, + userAddress: Address, + ): Promise { + const minMaxAmount = _minMaxAmount.toString(); + const swapParams = await this.paraSwap.swap.buildTx( + { + srcToken: priceRoute.srcToken, + srcDecimals: priceRoute.srcDecimals, + destDecimals: priceRoute.destDecimals, + destToken: priceRoute.destToken, + srcAmount: + priceRoute.side === SwapSide.SELL + ? priceRoute.srcAmount + : minMaxAmount, + destAmount: + priceRoute.side === SwapSide.SELL + ? minMaxAmount + : priceRoute.destAmount, + priceRoute, + userAddress, + partner: 'paraswap.io', + }, + { + ignoreChecks: true, + }, + ); + return swapParams as TxObject; + } +} + +function allowTokenTransferProxyParams( + tokenAddress: Address, + holderAddress: Address, + network: Network, +) { + const tokenTransferProxy = generateConfig(network).tokenTransferProxyAddress; + return { + from: holderAddress, + to: tokenAddress, + data: erc20Interface.encodeFunctionData('approve', [ + tokenTransferProxy, + MAX_UINT, + ]), + value: '0', + }; +} + +function deployContractParams(bytecode: string, network = Network.MAINNET) { + const ownerAddress = DEPLOYER_ADDRESS[network]; + if (!ownerAddress) throw new Error('No deployer address set for network'); + return { + from: ownerAddress, + data: bytecode, + value: '0', + }; +} + +function augustusSetImplementationParams( + contractAddress: Address, + network: Network, + functionName: string, +) { + const augustusAddress = generateConfig(network).augustusAddress; + if (!augustusAddress) throw new Error('No whitelist address set for network'); + const ownerAddress = MULTISIG[network]; + if (!ownerAddress) throw new Error('No whitelist owner set for network'); + + return { + from: ownerAddress, + to: augustusAddress, + data: augustusInterface.encodeFunctionData('setImplementation', [ + directRouterIface.getSighash(functionName), + contractAddress, + ]), + value: '0', + }; +} + +function augustusGrantRoleParams( + contractAddress: Address, + network: Network, + type: string = 'adapter', +) { + const augustusAddress = generateConfig(network).augustusAddress; + if (!augustusAddress) throw new Error('No whitelist address set for network'); + const ownerAddress = MULTISIG[network]; + if (!ownerAddress) throw new Error('No whitelist owner set for network'); + + let role: string; + switch (type) { + case 'adapter': + role = + '0x8429d542926e6695b59ac6fbdcd9b37e8b1aeb757afab06ab60b1bb5878c3b49'; + break; + case 'router': + role = + '0x7a05a596cb0ce7fdea8a1e1ec73be300bdb35097c944ce1897202f7a13122eb2'; + break; + default: + throw new Error(`Unrecognized type ${type}`); + } + + return { + from: ownerAddress, + to: augustusAddress, + data: augustusInterface.encodeFunctionData('grantRole', [ + role, + contractAddress, + ]), + value: '0', + }; +} + +function formatDeployMessage( + type: 'router' | 'adapter', + address: Address, + forkId: string, + contractName: string, + contractPath: string, +) { + // This formatting is useful for verification on Tenderly + return `Deployed ${type} contract with env params: + TENDERLY_FORK_ID=${forkId} + TENDERLY_VERIFY_CONTRACT_ADDRESS=${address} + TENDERLY_VERIFY_CONTRACT_NAME=${contractName} + TENDERLY_VERIFY_CONTRACT_PATH=${contractPath}`; +} + +export async function testE2E( + srcToken: Token, + destToken: Token, + senderAddress: Address, + _amount: string, + swapSide = SwapSide.SELL, + dexKey: string, + contractMethod: ContractMethod, + network: Network = Network.MAINNET, + _0: Provider, + blockNumber?: number, + poolIdentifiers?: string[], + limitOrderProvider?: DummyLimitOrderProvider, + transferFees?: TransferFeeParams, + // Specified in BPS: part of 10000 + slippage?: number, + sleepMs?: number, +) { + const amount = BigInt(_amount); + + const ts: TransactionSimulator = new TenderlySimulation(network, blockNumber); + await ts.setup(); + + if (srcToken.address.toLowerCase() !== ETHER_ADDRESS.toLowerCase()) { + const allowanceTx = await ts.simulate( + allowTokenTransferProxyParams(srcToken.address, senderAddress, network), + ); + if (!allowanceTx.success) console.log(allowanceTx.url); + expect(allowanceTx!.success).toEqual(true); + } + + if (deployedTestContractAddress) { + const whitelistTx = await ts.simulate( + augustusGrantRoleParams( + deployedTestContractAddress, + network, + testContractType || 'adapter', + ), + ); + expect(whitelistTx.success).toEqual(true); + console.log(`Successfully whitelisted ${deployedTestContractAddress}`); + + if (testContractType === 'router') { + const setImplementationTx = await ts.simulate( + augustusSetImplementationParams( + deployedTestContractAddress, + network, + contractMethod, + ), + ); + expect(setImplementationTx.success).toEqual(true); + } + } else if (testContractBytecode) { + const deployTx = await ts.simulate( + deployContractParams(testContractBytecode, network), + ); + + expect(deployTx.success).toEqual(true); + + const contractAddress = + deployTx.transaction?.transaction_info.contract_address; + console.log( + formatDeployMessage( + 'adapter', + contractAddress, + ts.forkId, + testContractName || '', + testContractRelativePath || '', + ), + ); + const whitelistTx = await ts.simulate( + augustusGrantRoleParams( + contractAddress, + network, + testContractType || 'adapter', + ), + ); + expect(whitelistTx.success).toEqual(true); + + if (testContractType === 'router') { + const setImplementationTx = await ts.simulate( + augustusSetImplementationParams( + contractAddress, + network, + contractMethod, + ), + ); + expect(setImplementationTx.success).toEqual(true); + } + } + + const useAPI = testingEndpoint && !poolIdentifiers; + // The API currently doesn't allow for specifying poolIdentifiers + const paraswap: IParaSwapSDK = useAPI + ? new APIParaswapSDK(network, dexKey) + : new LocalParaswapSDK( + network, + dexKey, + '', + limitOrderProvider, + blockNumber, + ); + + if (paraswap.initializePricing) await paraswap.initializePricing(); + + if (sleepMs) { + await sleep(sleepMs); + } + + if (paraswap.dexHelper?.replaceProviderWithRPC) { + paraswap.dexHelper?.replaceProviderWithRPC( + `https://rpc.tenderly.co/fork/${ts.forkId}`, + ); + } + + try { + const priceRoute = await paraswap.getPrices( + srcToken, + destToken, + amount, + swapSide, + contractMethod, + poolIdentifiers, + transferFees, + ); + expect(parseFloat(priceRoute.destAmount)).toBeGreaterThan(0); + + // Calculate slippage. Default is 1% + const _slippage = slippage || 100; + const minMaxAmount = + (swapSide === SwapSide.SELL + ? BigInt(priceRoute.destAmount) * (10000n - BigInt(_slippage)) + : BigInt(priceRoute.srcAmount) * (10000n + BigInt(_slippage))) / 10000n; + const swapParams = await paraswap.buildTransaction( + priceRoute, + minMaxAmount, + senderAddress, + ); + + const swapTx = await ts.simulate(swapParams); + // Only log gas estimate if testing against API + if (useAPI) { + const gasUsed = swapTx.gasUsed || '0'; + console.log( + `Gas Estimate API: ${priceRoute.gasCost}, Simulated: ${ + swapTx!.gasUsed + }, Difference: ${parseInt(priceRoute.gasCost) - parseInt(gasUsed)}`, + ); + } + console.log(`Tenderly URL: ${swapTx!.url}`); + expect(swapTx!.success).toEqual(true); + } finally { + if (paraswap.releaseResources) { + await paraswap.releaseResources(); + } + } +} diff --git a/src/dex/integral/tests/utils-events.ts b/src/dex/integral/tests/utils-events.ts new file mode 100644 index 000000000..90eb32194 --- /dev/null +++ b/src/dex/integral/tests/utils-events.ts @@ -0,0 +1,194 @@ +import _ from 'lodash'; +import fs from 'fs'; +import path from 'path'; +import { Log, BlockHeader, Address } from '../../../types'; +import { StatefulEventSubscriber } from '../../../stateful-event-subscriber'; +import { Provider } from '@ethersproject/providers'; +import { DeepReadonly } from 'ts-essentials'; + +const pathRoot = path.join(__dirname, '../../../../tests/'); +const configPath = './configs.json'; +const absConfigPath = path.join(pathRoot, configPath); +let configs: { [k: string]: any } = {}; +if (fs.existsSync(absConfigPath)) configs = require(absConfigPath); + +const statePath = './states.json'; +const absStatePath = path.join(pathRoot, statePath); +let states: { [k: string]: any } = {}; +if (fs.existsSync(absStatePath)) states = require(absStatePath); + +const logPath = './logs.json'; +const absLogPath = path.join(pathRoot, logPath); +let logs: { [k: string]: BlockInfo } = {}; +if (fs.existsSync(absLogPath)) logs = require(absLogPath); + +const bigintify = (val: string) => BigInt(val); +const stringify = (val: bigint) => val.toString(); + +interface BlockInfo { + logs: Log[]; + blockHeaders: { [blockNumber: number]: BlockHeader }; +} + +export async function updateEventSubscriber( + eventSubscriber: StatefulEventSubscriber, + subscribedAddress: Address[], + fetchState: (blocknumber: number) => Promise, + blockNumber: number, + cacheKey: string, + provider: Provider, +) { + // Get state of the subscriber block before the event was released + let poolState = getSavedState(blockNumber - 1, cacheKey); + if (!poolState) { + poolState = await fetchState(blockNumber - 1); + saveState(blockNumber - 1, cacheKey, poolState); + } + + // Set subscriber state before the event block + eventSubscriber.setState( + poolState as DeepReadonly, + blockNumber - 1, + ); + eventSubscriber.isTracking = () => true; + + // Get logs and blockHeader of the block when the event was emitted + let blockInfo = getSavedBlockInfo(blockNumber, cacheKey); + if (!blockInfo) { + const logs = ( + await Promise.all( + subscribedAddress.map(address => + provider.getLogs({ + fromBlock: blockNumber, + toBlock: blockNumber, + address, + }), + ), + ) + ) + .flat() + .sort((a, b) => a.logIndex - b.logIndex); + + blockInfo = { + logs, + blockHeaders: { + [blockNumber]: ( + (await provider.getBlock(blockNumber)) + ), + }, + }; + saveBlockInfo(blockNumber, cacheKey, blockInfo); + } + + // Update subscriber with event logs + await eventSubscriber.update(blockInfo.logs, blockInfo.blockHeaders); +} + +export async function checkEventSubscriber( + eventSubscriber: StatefulEventSubscriber, + fetchState: (blocknumber: number) => Promise, + blockNumber: number, + cacheKey: string, + stateCompare?: ( + state: SubscriberState, + expectedState: SubscriberState, + ) => void, +) { + // Get the expected state of the subscriber after the event + let expectedNewPoolState = getSavedState(blockNumber, cacheKey); + if (!expectedNewPoolState) { + expectedNewPoolState = await fetchState(blockNumber); + saveState(blockNumber, cacheKey, expectedNewPoolState); + } + + // Get the updated state of the subscriber + const newPoolState = eventSubscriber.getState(blockNumber); + + // Expect the updated state to be same as the expected state + if (stateCompare) { + expect(newPoolState).not.toBeNull(); + stateCompare( + newPoolState as SubscriberState, + expectedNewPoolState as SubscriberState, + ); + } else { + expect(newPoolState).toEqual(expectedNewPoolState); + } +} + +export function deepTypecast( + obj: any, + checker: (val: any) => boolean, + caster: (val: T) => any, +): any { + return _.forEach(obj, (val: any, key: any, obj: any) => { + obj[key] = checker(val) + ? caster(val) + : _.isObject(val) + ? deepTypecast(val, checker, caster) + : val; + }); +} + +export function getSavedConfig( + blockNumber: number, + cacheKey: string, +): Config | undefined { + const _config = configs[`${cacheKey}_${blockNumber}`]; + if (_config) { + const checker = (obj: any) => _.isString(obj) && obj.includes('bi@'); + const caster = (obj: string) => bigintify(obj.slice(3)); + return deepTypecast(_.cloneDeep(_config), checker, caster); + } + return undefined; +} + +export function saveConfig( + blockNumber: number, + cacheKey: string, + config: Config, +) { + const checker = (obj: any) => typeof obj === 'bigint'; + const caster = (obj: bigint) => 'bi@'.concat(stringify(obj)); + const _config = deepTypecast(_.cloneDeep(config), checker, caster); + configs[`${cacheKey}_${blockNumber}`] = _config; + fs.writeFileSync(absConfigPath, JSON.stringify(configs, null, 2)); +} + +export function getSavedState( + blockNumber: number, + cacheKey: string, +): SubscriberState | undefined { + const _state = states[`${cacheKey}_${blockNumber}`]; + if (_state) { + const checker = (obj: any) => _.isString(obj) && obj.includes('bi@'); + const caster = (obj: string) => bigintify(obj.slice(3)); + return deepTypecast(_.cloneDeep(_state), checker, caster); + } + return undefined; +} + +function saveState( + blockNumber: number, + cacheKey: string, + state: SubscriberState, +) { + const checker = (obj: any) => typeof obj === 'bigint'; + const caster = (obj: bigint) => 'bi@'.concat(stringify(obj)); + const _state = deepTypecast(_.cloneDeep(state), checker, caster); + states[`${cacheKey}_${blockNumber}`] = _state; + fs.writeFileSync(absStatePath, JSON.stringify(states, null, 2)); +} + +function getSavedBlockInfo(blockNumber: number, cacheKey: string): BlockInfo { + return logs[`${cacheKey}_${blockNumber}`]; +} + +function saveBlockInfo( + blockNumber: number, + cacheKey: string, + blockInfo: BlockInfo, +) { + logs[`${cacheKey}_${blockNumber}`] = blockInfo; + fs.writeFileSync(absLogPath, JSON.stringify(logs, null, 2)); +} diff --git a/src/dex/integral/tests/utils.ts b/src/dex/integral/tests/utils.ts new file mode 100644 index 000000000..8062bd948 --- /dev/null +++ b/src/dex/integral/tests/utils.ts @@ -0,0 +1,57 @@ +/* eslint-disable no-console */ +import { ethers } from 'ethers'; +import { assert } from 'ts-essentials'; + +export const sleep = (time: number) => + new Promise(resolve => { + setTimeout(resolve, time); + }); + +export const generateDeployBytecode = ( + testContractProjectRootPath: string | undefined, + testContractName: string | undefined, + testContractConfigFileName: string | undefined, + testContractRelativePath: string | undefined, + testContractDeployArgs: string | undefined, + testContractType: string | undefined, +): string => { + if ( + !testContractProjectRootPath || + !testContractName || + !testContractConfigFileName || + !testContractRelativePath || + !testContractDeployArgs || + !testContractType + ) { + return ''; + } + + const artifactJsonPath = `${testContractProjectRootPath}/artifacts/${testContractName}.json`; + const configJsonPath = `${testContractProjectRootPath}/config/${testContractConfigFileName}.json`; + const fieldsToPickFromConfig = testContractDeployArgs.split(','); + + const artifact = require(artifactJsonPath) as { + abi: ethers.ContractInterface; + bytecode: string; + }; + // I don't want to type this testing thing. If something is wrong, I will just throw + const config = require(configJsonPath) as any; + const factory = new ethers.ContractFactory(artifact.abi, artifact.bytecode); + + const args = fieldsToPickFromConfig.map(f => { + const value = config[f]; + if (value === undefined) { + console.log( + `Field ${f} is not defined in configJsonPath=${configJsonPath}. Using value as it is`, + ); + return f; + } + return value; + }); + + const deployedBytecode = factory.getDeployTransaction(...args).data; + + assert(deployedBytecode !== undefined, 'deployedBytecode is undefined'); + + return deployedBytecode.toString(); +}; diff --git a/src/dex/integral/types.ts b/src/dex/integral/types.ts new file mode 100644 index 000000000..f5d6393b7 --- /dev/null +++ b/src/dex/integral/types.ts @@ -0,0 +1,88 @@ +import { Address, Token } from '../../types'; +import { IntegralEventPool } from './integral-pool'; +import { PoolState as UniswapPoolState } from '../uniswap-v3/types'; +import { BigNumber } from 'ethers'; +import { IntegralPricing } from './integral-pricing'; + +export type Requires = Class & + Required>; + +export type IntegralPool = { + base?: IntegralEventPool; + pricing?: IntegralPricing; + enabled: boolean; +}; + +export type QuotingProps = { + price: bigint; + fee: bigint; + tokenOutLimits: [bigint, bigint]; + decimalsConverter: bigint; +}; + +export type PoolState = { + swapFee: bigint; + mintFee: bigint; + burnFee: bigint; + decimals0: number; + decimals1: number; + oracle: Address; + uniswapPool: Address; + uniswapPoolFee: bigint; +}; + +export type RelayerPoolState = { + isEnabled: boolean; + swapFee: bigint; + twapInterval: number; + limits: { + min0: bigint; + min1: bigint; + maxMultiplier0: bigint; + maxMultiplier1: bigint; + }; +}; +export type RelayerTokensState = { + [tokenAddress: string]: { balance: bigint }; +}; +export type RelayerState = { + pools: { [poolAddress: string]: RelayerPoolState }; + tokens: RelayerTokensState; +}; + +export type PoolInitProps = { + [poolAddress: string]: { token0: Address; token1: Address }; +}; +export type FactoryState = { pools: PoolInitProps }; + +export type PoolStates = { + base: PoolState; + relayer: RelayerPoolState; + relayerTokens: RelayerTokensState; + pricing: UniswapPoolState; + poolAddress: Address; +}; + +export type TokenState = Record; + +export type IntegralData = { + relayer: Address; +}; + +export type DexParams = { + factoryAddress: string; + relayerAddress: string; + subgraphURL?: string; +}; + +export enum IntegralFunctions { + swap = 'sell', + buy = 'buy', +} + +export type Observation = { + blockTimestamp: number; + initialized: boolean; + secondsPerLiquidityCumulativeX128: BigNumber; + tickCumulative: BigNumber; +}; diff --git a/src/dex/integral/utils.ts b/src/dex/integral/utils.ts new file mode 100644 index 000000000..b06e66933 --- /dev/null +++ b/src/dex/integral/utils.ts @@ -0,0 +1,53 @@ +import { BytesLike, ethers } from 'ethers'; +import { MultiResult } from '../../lib/multi-wrapper'; +import { extractSuccessAndValue, generalDecoder } from '../../lib/decoders'; +import { assert } from 'ts-essentials'; +import { Observation } from './types'; +import { Address } from '@paraswap/core'; + +export const uint32ToNumber = ( + result: MultiResult | BytesLike, +): number => { + return generalDecoder(result, ['uint32'], 0, value => + parseInt(value[0].toString(), 10), + ); +}; + +export function ceil_div(a: bigint, b: bigint) { + const c = a / b; + if (a != b * c) { + return c + 1n; + } else { + return c; + } +} + +export function sortTokens(srcAddress: Address, destAddress: Address) { + return [srcAddress, destAddress].sort((a, b) => (a < b ? -1 : 1)); +} + +export function decodeStateMultiCallResultWithObservation( + result: MultiResult | BytesLike, +): Observation { + const [isSuccess, toDecode] = extractSuccessAndValue(result); + + assert( + isSuccess && toDecode !== '0x', + `decodeStateMultiCallResultWithRelativeBitmaps failed to get decodable result: ${result}`, + ); + + const decoded = ethers.utils.defaultAbiCoder.decode( + [ + ` + tuple( + uint32 blockTimestamp, + int56 tickCumulative, + uint160 secondsPerLiquidityCumulativeX128, + bool initialized, + ) + `, + ], + toDecode, + )[0]; + return decoded as Observation; +} diff --git a/src/dex/uniswap-v3/contract-math/Oracle.ts b/src/dex/uniswap-v3/contract-math/Oracle.ts index 19b1918d1..3fa6ffd9b 100644 --- a/src/dex/uniswap-v3/contract-math/Oracle.ts +++ b/src/dex/uniswap-v3/contract-math/Oracle.ts @@ -23,7 +23,7 @@ export class Oracle { ): OracleObservation { const delta = blockTimestamp - last.blockTimestamp; return { - blockTimestamp: state.blockTimestamp, + blockTimestamp, tickCumulative: last.tickCumulative + BigInt.asIntN(56, tick) * delta, secondsPerLiquidityCumulativeX128: last.secondsPerLiquidityCumulativeX128 + @@ -93,12 +93,12 @@ export class Oracle { let beforeOrAt; let atOrAfter; while (true) { - i = (l + r) / 2; + i = Math.floor((l + r) / 2); beforeOrAt = state.observations[i % cardinality]; // we've landed on an uninitialized tick, keep searching higher (more recently) - if (!beforeOrAt.initialized) { + if (!beforeOrAt || !beforeOrAt.initialized) { l = i + 1; continue; } @@ -150,7 +150,8 @@ export class Oracle { } beforeOrAt = state.observations[(index + 1) % cardinality]; - if (!beforeOrAt.initialized) beforeOrAt = state.observations[0]; + if (!beforeOrAt || !beforeOrAt.initialized) + beforeOrAt = state.observations[0]; _require( Oracle.lte(time, beforeOrAt.blockTimestamp, target),