diff --git a/package.json b/package.json index 842aa74c3..028ba6c6b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@paraswap/dex-lib", - "version": "4.0.0", + "version": "4.0.8", "main": "build/index.js", "types": "build/index.d.ts", "repository": "https://github.com/paraswap/paraswap-dex-lib", @@ -52,6 +52,7 @@ }, "dependencies": { "@0x/utils": "^4.5.2", + "@balancer-labs/balancer-maths": "^0.0.19", "@balancer-labs/sor": "4.1.1-beta.4", "@bgd-labs/aave-address-book": "4.7.0", "@ethersproject/abi": "^5.7.0", diff --git a/scripts/run-dex-integration-v3.ts b/scripts/run-dex-integration-v3.ts new file mode 100644 index 000000000..cba5dd68d --- /dev/null +++ b/scripts/run-dex-integration-v3.ts @@ -0,0 +1,78 @@ +// npx ts-node scripts/run-dex-integration-v3.ts +/* eslint-disable no-console */ +import dotenv from 'dotenv'; +dotenv.config(); + +import { Network, SwapSide } from '../src/constants'; +import { BalancerV3 } from '../src/dex/balancer-v3/balancer-v3'; +import { DummyDexHelper } from '../src/dex-helper/index'; +import { BI_POWS } from '../src/bigint-constants'; + +const stataUSDC = { + address: '0x8a88124522dbbf1e56352ba3de1d9f78c143751e'.toLowerCase(), + decimals: 6, +}; + +const stataDAI = { + address: '0xde46e43f46ff74a23a65ebb0580cbe3dfe684a17'.toLowerCase(), + decimals: 18, +}; + +const stataUSDT = { + address: '0x978206fae13faf5a8d293fb614326b237684b750'.toLowerCase(), + decimals: 6, +}; + +const bal = { + address: '0xb19382073c7a0addbb56ac6af1808fa49e377b75'.toLowerCase(), + decimals: 18, +}; + +const daiAave = { + address: '0xff34b3d4aee8ddcd6f9afffb6fe49bd371b8a357'.toLowerCase(), + decimals: 18, +}; + +const usdcAave = { + address: '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8'.toLowerCase(), + decimals: 6, +}; + +const amounts = [0n, BI_POWS[6], BI_POWS[7], BI_POWS[8]]; + +async function main() { + const dexHelper = new DummyDexHelper(Network.SEPOLIA); + const blocknumber = await dexHelper.web3Provider.eth.getBlockNumber(); + + const balancerV3 = new BalancerV3(Network.SEPOLIA, 'BalancerV3', dexHelper); + + await balancerV3.initializePricing(blocknumber); + + const from = stataUSDC; + const to = stataUSDT; + // const from = daiAave; + // const to = usdcAave; + + const pools = await balancerV3.getPoolIdentifiers( + from, + to, + SwapSide.SELL, + blocknumber, + ); + console.log('Pool Identifiers: ', from.address, to.address, pools); + + const prices = await balancerV3.getPricesVolume( + from, + to, + amounts, + SwapSide.SELL, + blocknumber, + pools, + ); + console.log('Pool Prices: ', prices); + + const poolLiquidity = await balancerV3.getTopPoolsForToken(from.address, 10); + console.log('Top Pools:', poolLiquidity); +} + +main(); diff --git a/src/abi/balancer-v3/batch-router.json b/src/abi/balancer-v3/batch-router.json new file mode 100644 index 000000000..878e3173a --- /dev/null +++ b/src/abi/balancer-v3/batch-router.json @@ -0,0 +1,989 @@ +[ + { + "inputs": [ + { + "internalType": "contract IVault", + "name": "vault", + "type": "address" + }, + { + "internalType": "contract IWETH", + "name": "weth", + "type": "address" + }, + { + "internalType": "contract IPermit2", + "name": "permit2", + "type": "address" + }, + { + "internalType": "string", + "name": "version", + "type": "string" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "AddressInsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "ErrorSelectorNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "EthTransfer", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [], + "name": "InputLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientEth", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "bits", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "SafeCastOverflowedUintDowncast", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "SenderIsNotVault", + "type": "error" + }, + { + "inputs": [], + "name": "SwapDeadline", + "type": "error" + }, + { + "inputs": [], + "name": "TransientIndexOutOfBounds", + "type": "error" + }, + { + "inputs": [], + "name": "getSender", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "data", + "type": "bytes[]" + } + ], + "name": "multicall", + "outputs": [ + { + "internalType": "bytes[]", + "name": "results", + "type": "bytes[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct IRouterCommon.PermitApproval[]", + "name": "permitBatch", + "type": "tuple[]" + }, + { + "internalType": "bytes[]", + "name": "permitSignatures", + "type": "bytes[]" + }, + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "internalType": "uint48", + "name": "expiration", + "type": "uint48" + }, + { + "internalType": "uint48", + "name": "nonce", + "type": "uint48" + } + ], + "internalType": "struct IAllowanceTransfer.PermitDetails[]", + "name": "details", + "type": "tuple[]" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sigDeadline", + "type": "uint256" + } + ], + "internalType": "struct IAllowanceTransfer.PermitBatch", + "name": "permit2Batch", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "permit2Signature", + "type": "bytes" + }, + { + "internalType": "bytes[]", + "name": "multicallData", + "type": "bytes[]" + } + ], + "name": "permitBatchAndCall", + "outputs": [ + { + "internalType": "bytes[]", + "name": "results", + "type": "bytes[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "components": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "bool", + "name": "isBuffer", + "type": "bool" + } + ], + "internalType": "struct IBatchRouter.SwapPathStep[]", + "name": "steps", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "exactAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minAmountOut", + "type": "uint256" + } + ], + "internalType": "struct IBatchRouter.SwapPathExactAmountIn[]", + "name": "paths", + "type": "tuple[]" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "querySwapExactIn", + "outputs": [ + { + "internalType": "uint256[]", + "name": "pathAmountsOut", + "type": "uint256[]" + }, + { + "internalType": "address[]", + "name": "tokensOut", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "components": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "bool", + "name": "isBuffer", + "type": "bool" + } + ], + "internalType": "struct IBatchRouter.SwapPathStep[]", + "name": "steps", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "exactAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minAmountOut", + "type": "uint256" + } + ], + "internalType": "struct IBatchRouter.SwapPathExactAmountIn[]", + "name": "paths", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IBatchRouter.SwapExactInHookParams", + "name": "params", + "type": "tuple" + } + ], + "name": "querySwapExactInHook", + "outputs": [ + { + "internalType": "uint256[]", + "name": "pathAmountsOut", + "type": "uint256[]" + }, + { + "internalType": "address[]", + "name": "tokensOut", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "components": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "bool", + "name": "isBuffer", + "type": "bool" + } + ], + "internalType": "struct IBatchRouter.SwapPathStep[]", + "name": "steps", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "maxAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "exactAmountOut", + "type": "uint256" + } + ], + "internalType": "struct IBatchRouter.SwapPathExactAmountOut[]", + "name": "paths", + "type": "tuple[]" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "querySwapExactOut", + "outputs": [ + { + "internalType": "uint256[]", + "name": "pathAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "address[]", + "name": "tokensIn", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amountsIn", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "components": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "bool", + "name": "isBuffer", + "type": "bool" + } + ], + "internalType": "struct IBatchRouter.SwapPathStep[]", + "name": "steps", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "maxAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "exactAmountOut", + "type": "uint256" + } + ], + "internalType": "struct IBatchRouter.SwapPathExactAmountOut[]", + "name": "paths", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IBatchRouter.SwapExactOutHookParams", + "name": "params", + "type": "tuple" + } + ], + "name": "querySwapExactOutHook", + "outputs": [ + { + "internalType": "uint256[]", + "name": "pathAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "address[]", + "name": "tokensIn", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amountsIn", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "components": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "bool", + "name": "isBuffer", + "type": "bool" + } + ], + "internalType": "struct IBatchRouter.SwapPathStep[]", + "name": "steps", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "exactAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minAmountOut", + "type": "uint256" + } + ], + "internalType": "struct IBatchRouter.SwapPathExactAmountIn[]", + "name": "paths", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "swapExactIn", + "outputs": [ + { + "internalType": "uint256[]", + "name": "pathAmountsOut", + "type": "uint256[]" + }, + { + "internalType": "address[]", + "name": "tokensOut", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "components": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "bool", + "name": "isBuffer", + "type": "bool" + } + ], + "internalType": "struct IBatchRouter.SwapPathStep[]", + "name": "steps", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "exactAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minAmountOut", + "type": "uint256" + } + ], + "internalType": "struct IBatchRouter.SwapPathExactAmountIn[]", + "name": "paths", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IBatchRouter.SwapExactInHookParams", + "name": "params", + "type": "tuple" + } + ], + "name": "swapExactInHook", + "outputs": [ + { + "internalType": "uint256[]", + "name": "pathAmountsOut", + "type": "uint256[]" + }, + { + "internalType": "address[]", + "name": "tokensOut", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "components": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "bool", + "name": "isBuffer", + "type": "bool" + } + ], + "internalType": "struct IBatchRouter.SwapPathStep[]", + "name": "steps", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "maxAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "exactAmountOut", + "type": "uint256" + } + ], + "internalType": "struct IBatchRouter.SwapPathExactAmountOut[]", + "name": "paths", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "swapExactOut", + "outputs": [ + { + "internalType": "uint256[]", + "name": "pathAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "address[]", + "name": "tokensIn", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amountsIn", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "components": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "bool", + "name": "isBuffer", + "type": "bool" + } + ], + "internalType": "struct IBatchRouter.SwapPathStep[]", + "name": "steps", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "maxAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "exactAmountOut", + "type": "uint256" + } + ], + "internalType": "struct IBatchRouter.SwapPathExactAmountOut[]", + "name": "paths", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IBatchRouter.SwapExactOutHookParams", + "name": "params", + "type": "tuple" + } + ], + "name": "swapExactOutHook", + "outputs": [ + { + "internalType": "uint256[]", + "name": "pathAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "address[]", + "name": "tokensIn", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amountsIn", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/src/abi/balancer-v3/router.json b/src/abi/balancer-v3/router.json new file mode 100644 index 000000000..e28912159 --- /dev/null +++ b/src/abi/balancer-v3/router.json @@ -0,0 +1,1792 @@ +[ + { + "inputs": [ + { + "internalType": "contract IVault", + "name": "vault", + "type": "address" + }, + { + "internalType": "contract IWETH", + "name": "weth", + "type": "address" + }, + { + "internalType": "contract IPermit2", + "name": "permit2", + "type": "address" + }, + { + "internalType": "string", + "name": "version", + "type": "string" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "AddressInsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "ErrorSelectorNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "EthTransfer", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [], + "name": "InputLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientEth", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "bits", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "SafeCastOverflowedUintDowncast", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "SenderIsNotVault", + "type": "error" + }, + { + "inputs": [], + "name": "SwapDeadline", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "maxAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "minBptAmountOut", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "addLiquidityCustom", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amountsIn", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "bptAmountOut", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "maxAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "minBptAmountOut", + "type": "uint256" + }, + { + "internalType": "enum AddLiquidityKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IRouterCommon.AddLiquidityHookParams", + "name": "params", + "type": "tuple" + } + ], + "name": "addLiquidityHook", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amountsIn", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "bptAmountOut", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "maxAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "exactBptAmountOut", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "addLiquidityProportional", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amountsIn", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "uint256", + "name": "maxAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "exactBptAmountOut", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "addLiquiditySingleTokenExactOut", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "exactAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "minBptAmountOut", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "addLiquidityUnbalanced", + "outputs": [ + { + "internalType": "uint256", + "name": "bptAmountOut", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "amountsIn", + "type": "uint256[]" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "donate", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "getSender", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "exactAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "minBptAmountOut", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "initialize", + "outputs": [ + { + "internalType": "uint256", + "name": "bptAmountOut", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "exactAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "minBptAmountOut", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IRouter.InitializeHookParams", + "name": "params", + "type": "tuple" + } + ], + "name": "initializeHook", + "outputs": [ + { + "internalType": "uint256", + "name": "bptAmountOut", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "data", + "type": "bytes[]" + } + ], + "name": "multicall", + "outputs": [ + { + "internalType": "bytes[]", + "name": "results", + "type": "bytes[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct IRouterCommon.PermitApproval[]", + "name": "permitBatch", + "type": "tuple[]" + }, + { + "internalType": "bytes[]", + "name": "permitSignatures", + "type": "bytes[]" + }, + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "internalType": "uint48", + "name": "expiration", + "type": "uint48" + }, + { + "internalType": "uint48", + "name": "nonce", + "type": "uint48" + } + ], + "internalType": "struct IAllowanceTransfer.PermitDetails[]", + "name": "details", + "type": "tuple[]" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sigDeadline", + "type": "uint256" + } + ], + "internalType": "struct IAllowanceTransfer.PermitBatch", + "name": "permit2Batch", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "permit2Signature", + "type": "bytes" + }, + { + "internalType": "bytes[]", + "name": "multicallData", + "type": "bytes[]" + } + ], + "name": "permitBatchAndCall", + "outputs": [ + { + "internalType": "bytes[]", + "name": "results", + "type": "bytes[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "maxAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "minBptAmountOut", + "type": "uint256" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "queryAddLiquidityCustom", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amountsIn", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "bptAmountOut", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "maxAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "minBptAmountOut", + "type": "uint256" + }, + { + "internalType": "enum AddLiquidityKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IRouterCommon.AddLiquidityHookParams", + "name": "params", + "type": "tuple" + } + ], + "name": "queryAddLiquidityHook", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amountsIn", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "bptAmountOut", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256", + "name": "exactBptAmountOut", + "type": "uint256" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "queryAddLiquidityProportional", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amountsIn", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "uint256", + "name": "exactBptAmountOut", + "type": "uint256" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "queryAddLiquiditySingleTokenExactOut", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "exactAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "queryAddLiquidityUnbalanced", + "outputs": [ + { + "internalType": "uint256", + "name": "bptAmountOut", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256", + "name": "maxBptAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "minAmountsOut", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "queryRemoveLiquidityCustom", + "outputs": [ + { + "internalType": "uint256", + "name": "bptAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "minAmountsOut", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "maxBptAmountIn", + "type": "uint256" + }, + { + "internalType": "enum RemoveLiquidityKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IRouterCommon.RemoveLiquidityHookParams", + "name": "params", + "type": "tuple" + } + ], + "name": "queryRemoveLiquidityHook", + "outputs": [ + { + "internalType": "uint256", + "name": "bptAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256", + "name": "exactBptAmountIn", + "type": "uint256" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "queryRemoveLiquidityProportional", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256", + "name": "exactBptAmountIn", + "type": "uint256" + } + ], + "name": "queryRemoveLiquidityRecovery", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "exactBptAmountIn", + "type": "uint256" + } + ], + "name": "queryRemoveLiquidityRecoveryHook", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256", + "name": "exactBptAmountIn", + "type": "uint256" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "queryRemoveLiquiditySingleTokenExactIn", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "exactAmountOut", + "type": "uint256" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "queryRemoveLiquiditySingleTokenExactOut", + "outputs": [ + { + "internalType": "uint256", + "name": "bptAmountIn", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "enum SwapKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountGiven", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "limit", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IRouter.SwapSingleTokenHookParams", + "name": "params", + "type": "tuple" + } + ], + "name": "querySwapHook", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "exactAmountIn", + "type": "uint256" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "querySwapSingleTokenExactIn", + "outputs": [ + { + "internalType": "uint256", + "name": "amountCalculated", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "exactAmountOut", + "type": "uint256" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "querySwapSingleTokenExactOut", + "outputs": [ + { + "internalType": "uint256", + "name": "amountCalculated", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256", + "name": "maxBptAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "minAmountsOut", + "type": "uint256[]" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "removeLiquidityCustom", + "outputs": [ + { + "internalType": "uint256", + "name": "bptAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "minAmountsOut", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "maxBptAmountIn", + "type": "uint256" + }, + { + "internalType": "enum RemoveLiquidityKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IRouterCommon.RemoveLiquidityHookParams", + "name": "params", + "type": "tuple" + } + ], + "name": "removeLiquidityHook", + "outputs": [ + { + "internalType": "uint256", + "name": "bptAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256", + "name": "exactBptAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "minAmountsOut", + "type": "uint256[]" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "removeLiquidityProportional", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256", + "name": "exactBptAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "minAmountsOut", + "type": "uint256[]" + } + ], + "name": "removeLiquidityRecovery", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "exactBptAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "minAmountsOut", + "type": "uint256[]" + } + ], + "name": "removeLiquidityRecoveryHook", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256", + "name": "exactBptAmountIn", + "type": "uint256" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "minAmountOut", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "removeLiquiditySingleTokenExactIn", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256", + "name": "maxBptAmountIn", + "type": "uint256" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "exactAmountOut", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "removeLiquiditySingleTokenExactOut", + "outputs": [ + { + "internalType": "uint256", + "name": "bptAmountIn", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "exactAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minAmountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "swapSingleTokenExactIn", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "exactAmountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "swapSingleTokenExactOut", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "enum SwapKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountGiven", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "limit", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "wethIsEth", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IRouter.SwapSingleTokenHookParams", + "name": "params", + "type": "tuple" + } + ], + "name": "swapSingleTokenHook", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/src/abi/balancer-v3/vault-extension.json b/src/abi/balancer-v3/vault-extension.json new file mode 100644 index 000000000..c4f0f76c0 --- /dev/null +++ b/src/abi/balancer-v3/vault-extension.json @@ -0,0 +1,2834 @@ +[ + { + "inputs": [ + { + "internalType": "contract IVault", + "name": "mainVault", + "type": "address" + }, + { + "internalType": "contract IVaultAdmin", + "name": "vaultAdmin", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "AddressInsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "AfterAddLiquidityHookFailed", + "type": "error" + }, + { + "inputs": [], + "name": "AfterInitializeHookFailed", + "type": "error" + }, + { + "inputs": [], + "name": "AfterRemoveLiquidityHookFailed", + "type": "error" + }, + { + "inputs": [], + "name": "AfterSwapHookFailed", + "type": "error" + }, + { + "inputs": [], + "name": "AmountGivenZero", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxAmountIn", + "type": "uint256" + } + ], + "name": "AmountInAboveMax", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minAmountOut", + "type": "uint256" + } + ], + "name": "AmountOutBelowMin", + "type": "error" + }, + { + "inputs": [], + "name": "BalanceNotSettled", + "type": "error" + }, + { + "inputs": [], + "name": "BalanceOverflow", + "type": "error" + }, + { + "inputs": [], + "name": "BeforeAddLiquidityHookFailed", + "type": "error" + }, + { + "inputs": [], + "name": "BeforeInitializeHookFailed", + "type": "error" + }, + { + "inputs": [], + "name": "BeforeRemoveLiquidityHookFailed", + "type": "error" + }, + { + "inputs": [], + "name": "BeforeSwapHookFailed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxAmountIn", + "type": "uint256" + } + ], + "name": "BptAmountInAboveMax", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minAmountOut", + "type": "uint256" + } + ], + "name": "BptAmountOutBelowMin", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract IERC4626", + "name": "wrappedToken", + "type": "address" + } + ], + "name": "BufferAlreadyInitialized", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract IERC4626", + "name": "wrappedToken", + "type": "address" + } + ], + "name": "BufferNotInitialized", + "type": "error" + }, + { + "inputs": [], + "name": "BufferSharesInvalidOwner", + "type": "error" + }, + { + "inputs": [], + "name": "BufferSharesInvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + } + ], + "name": "BufferTotalSupplyTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "CannotReceiveEth", + "type": "error" + }, + { + "inputs": [], + "name": "CannotSwapSameToken", + "type": "error" + }, + { + "inputs": [], + "name": "CodecOverflow", + "type": "error" + }, + { + "inputs": [], + "name": "DoesNotSupportAddLiquidityCustom", + "type": "error" + }, + { + "inputs": [], + "name": "DoesNotSupportDonation", + "type": "error" + }, + { + "inputs": [], + "name": "DoesNotSupportRemoveLiquidityCustom", + "type": "error" + }, + { + "inputs": [], + "name": "DoesNotSupportUnbalancedLiquidity", + "type": "error" + }, + { + "inputs": [], + "name": "DynamicSwapFeeHookFailed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "allowance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "inputs": [], + "name": "ErrorSelectorNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [], + "name": "FeePrecisionTooHigh", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxAmountIn", + "type": "uint256" + } + ], + "name": "HookAdjustedAmountInAboveMax", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minAmountOut", + "type": "uint256" + } + ], + "name": "HookAdjustedAmountOutBelowMin", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "limit", + "type": "uint256" + } + ], + "name": "HookAdjustedSwapLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "poolHooksContract", + "type": "address" + }, + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "address", + "name": "poolFactory", + "type": "address" + } + ], + "name": "HookRegistrationFailed", + "type": "error" + }, + { + "inputs": [], + "name": "InputLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidAddLiquidityKind", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRemoveLiquidityKind", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidToken", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidTokenConfiguration", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidTokenDecimals", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidTokenType", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract IERC4626", + "name": "wrappedToken", + "type": "address" + } + ], + "name": "InvalidUnderlyingToken", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "issuedShares", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minIssuedShares", + "type": "uint256" + } + ], + "name": "IssuedSharesBelowMin", + "type": "error" + }, + { + "inputs": [], + "name": "MaxTokens", + "type": "error" + }, + { + "inputs": [], + "name": "MinTokens", + "type": "error" + }, + { + "inputs": [], + "name": "NotEnoughBufferShares", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract IERC4626", + "name": "wrappedToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "expectedUnderlyingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "actualUnderlyingAmount", + "type": "uint256" + } + ], + "name": "NotEnoughUnderlying", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract IERC4626", + "name": "wrappedToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "expectedWrappedAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "actualWrappedAmount", + "type": "uint256" + } + ], + "name": "NotEnoughWrapped", + "type": "error" + }, + { + "inputs": [], + "name": "NotStaticCall", + "type": "error" + }, + { + "inputs": [], + "name": "NotVaultDelegateCall", + "type": "error" + }, + { + "inputs": [], + "name": "OutOfBounds", + "type": "error" + }, + { + "inputs": [], + "name": "PauseBufferPeriodDurationTooLarge", + "type": "error" + }, + { + "inputs": [], + "name": "PercentageAboveMax", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "PoolAlreadyInitialized", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "PoolAlreadyRegistered", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "PoolInRecoveryMode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "PoolNotInRecoveryMode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "PoolNotInitialized", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "PoolNotPaused", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "PoolNotRegistered", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "PoolPauseWindowExpired", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "PoolPaused", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + } + ], + "name": "PoolTotalSupplyTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "ProtocolFeesExceedTotalCollected", + "type": "error" + }, + { + "inputs": [], + "name": "QueriesDisabled", + "type": "error" + }, + { + "inputs": [], + "name": "QueriesDisabledPermanently", + "type": "error" + }, + { + "inputs": [], + "name": "QuoteResultSpoofed", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "result", + "type": "bytes" + } + ], + "name": "Result", + "type": "error" + }, + { + "inputs": [], + "name": "RouterNotTrusted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "SafeCastOverflowedUintToInt", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "SenderIsNotVault", + "type": "error" + }, + { + "inputs": [], + "name": "SwapFeePercentageTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "SwapFeePercentageTooLow", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "limit", + "type": "uint256" + } + ], + "name": "SwapLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "name": "TokenAlreadyRegistered", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "name": "TokenNotRegistered", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "address", + "name": "expectedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "actualToken", + "type": "address" + } + ], + "name": "TokensMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "TokensNotSorted", + "type": "error" + }, + { + "inputs": [], + "name": "TradeAmountTooSmall", + "type": "error" + }, + { + "inputs": [], + "name": "VaultBuffersArePaused", + "type": "error" + }, + { + "inputs": [], + "name": "VaultIsNotUnlocked", + "type": "error" + }, + { + "inputs": [], + "name": "VaultNotPaused", + "type": "error" + }, + { + "inputs": [], + "name": "VaultPauseWindowDurationTooLarge", + "type": "error" + }, + { + "inputs": [], + "name": "VaultPauseWindowExpired", + "type": "error" + }, + { + "inputs": [], + "name": "VaultPaused", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract IERC4626", + "name": "wrappedToken", + "type": "address" + } + ], + "name": "WrapAmountTooSmall", + "type": "error" + }, + { + "inputs": [], + "name": "WrongProtocolFeeControllerDeployment", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract IERC4626", + "name": "wrappedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "underlyingToken", + "type": "address" + } + ], + "name": "WrongUnderlyingToken", + "type": "error" + }, + { + "inputs": [], + "name": "WrongVaultAdminDeployment", + "type": "error" + }, + { + "inputs": [], + "name": "WrongVaultExtensionDeployment", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "aggregateSwapFeePercentage", + "type": "uint256" + } + ], + "name": "AggregateSwapFeePercentageChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "aggregateYieldFeePercentage", + "type": "uint256" + } + ], + "name": "AggregateYieldFeePercentageChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "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": "contract IAuthorizer", + "name": "newAuthorizer", + "type": "address" + } + ], + "name": "AuthorizerChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC4626", + "name": "wrappedToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "burnedShares", + "type": "uint256" + } + ], + "name": "BufferSharesBurned", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC4626", + "name": "wrappedToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "issuedShares", + "type": "uint256" + } + ], + "name": "BufferSharesMinted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "liquidityProvider", + "type": "address" + }, + { + "indexed": true, + "internalType": "enum AddLiquidityKind", + "name": "kind", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "amountsAddedRaw", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "swapFeeAmountsRaw", + "type": "uint256[]" + } + ], + "name": "LiquidityAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC4626", + "name": "wrappedToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountUnderlying", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountWrapped", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "bufferBalances", + "type": "bytes32" + } + ], + "name": "LiquidityAddedToBuffer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "liquidityProvider", + "type": "address" + }, + { + "indexed": true, + "internalType": "enum RemoveLiquidityKind", + "name": "kind", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "amountsRemovedRaw", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "swapFeeAmountsRaw", + "type": "uint256[]" + } + ], + "name": "LiquidityRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC4626", + "name": "wrappedToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountUnderlying", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountWrapped", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "bufferBalances", + "type": "bytes32" + } + ], + "name": "LiquidityRemovedFromBuffer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "PoolInitialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "paused", + "type": "bool" + } + ], + "name": "PoolPausedStateChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "recoveryMode", + "type": "bool" + } + ], + "name": "PoolRecoveryModeStateChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "factory", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "enum TokenType", + "name": "tokenType", + "type": "uint8" + }, + { + "internalType": "contract IRateProvider", + "name": "rateProvider", + "type": "address" + }, + { + "internalType": "bool", + "name": "paysYieldFees", + "type": "bool" + } + ], + "indexed": false, + "internalType": "struct TokenConfig[]", + "name": "tokenConfig", + "type": "tuple[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "swapFeePercentage", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "pauseWindowEndTime", + "type": "uint32" + }, + { + "components": [ + { + "internalType": "address", + "name": "pauseManager", + "type": "address" + }, + { + "internalType": "address", + "name": "swapFeeManager", + "type": "address" + }, + { + "internalType": "address", + "name": "poolCreator", + "type": "address" + } + ], + "indexed": false, + "internalType": "struct PoolRoleAccounts", + "name": "roleAccounts", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bool", + "name": "enableHookAdjustedAmounts", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallBeforeInitialize", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallAfterInitialize", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallComputeDynamicSwapFee", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallBeforeSwap", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallAfterSwap", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallBeforeAddLiquidity", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallAfterAddLiquidity", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallBeforeRemoveLiquidity", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallAfterRemoveLiquidity", + "type": "bool" + }, + { + "internalType": "address", + "name": "hooksContract", + "type": "address" + } + ], + "indexed": false, + "internalType": "struct HooksConfig", + "name": "hooksConfig", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bool", + "name": "disableUnbalancedLiquidity", + "type": "bool" + }, + { + "internalType": "bool", + "name": "enableAddLiquidityCustom", + "type": "bool" + }, + { + "internalType": "bool", + "name": "enableRemoveLiquidityCustom", + "type": "bool" + }, + { + "internalType": "bool", + "name": "enableDonation", + "type": "bool" + } + ], + "indexed": false, + "internalType": "struct LiquidityManagement", + "name": "liquidityManagement", + "type": "tuple" + } + ], + "name": "PoolRegistered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IProtocolFeeController", + "name": "newProtocolFeeController", + "type": "address" + } + ], + "name": "ProtocolFeeControllerChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "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": "swapFeePercentage", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "swapFeeAmount", + "type": "uint256" + } + ], + "name": "Swap", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "swapFeePercentage", + "type": "uint256" + } + ], + "name": "SwapFeePercentageChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "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" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC4626", + "name": "wrappedToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "burnedShares", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "withdrawnUnderlying", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "bufferBalances", + "type": "bytes32" + } + ], + "name": "Unwrap", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "eventKey", + "type": "string" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "eventData", + "type": "bytes" + } + ], + "name": "VaultAuxiliary", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "paused", + "type": "bool" + } + ], + "name": "VaultBuffersPausedStateChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "paused", + "type": "bool" + } + ], + "name": "VaultPausedStateChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "VaultQueriesDisabled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "VaultQueriesEnabled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC4626", + "name": "wrappedToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "depositedUnderlying", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mintedShares", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "bufferBalances", + "type": "bytes32" + } + ], + "name": "Wrap", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "components": [ + { + "internalType": "enum SwapKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "amountGivenScaled18", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "balancesScaled18", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "indexIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "indexOut", + "type": "uint256" + }, + { + "internalType": "address", + "name": "router", + "type": "address" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct PoolSwapParams", + "name": "swapParams", + "type": "tuple" + } + ], + "name": "computeDynamicSwapFeePercentage", + "outputs": [ + { + "internalType": "uint256", + "name": "dynamicSwapFeePercentage", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "eventKey", + "type": "string" + }, + { + "internalType": "bytes", + "name": "eventData", + "type": "bytes" + } + ], + "name": "emitAuxiliaryEvent", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "getAddLiquidityCalledFlag", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "name": "getAggregateSwapFeeAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "name": "getAggregateYieldFeeAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "getBptRate", + "outputs": [ + { + "internalType": "uint256", + "name": "rate", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "getCurrentLiveBalances", + "outputs": [ + { + "internalType": "uint256[]", + "name": "balancesLiveScaled18", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC4626", + "name": "wrappedToken", + "type": "address" + } + ], + "name": "getERC4626BufferAsset", + "outputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "getHooksConfig", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "enableHookAdjustedAmounts", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallBeforeInitialize", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallAfterInitialize", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallComputeDynamicSwapFee", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallBeforeSwap", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallAfterSwap", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallBeforeAddLiquidity", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallAfterAddLiquidity", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallBeforeRemoveLiquidity", + "type": "bool" + }, + { + "internalType": "bool", + "name": "shouldCallAfterRemoveLiquidity", + "type": "bool" + }, + { + "internalType": "address", + "name": "hooksContract", + "type": "address" + } + ], + "internalType": "struct HooksConfig", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNonzeroDeltaCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "getPoolConfig", + "outputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "bool", + "name": "disableUnbalancedLiquidity", + "type": "bool" + }, + { + "internalType": "bool", + "name": "enableAddLiquidityCustom", + "type": "bool" + }, + { + "internalType": "bool", + "name": "enableRemoveLiquidityCustom", + "type": "bool" + }, + { + "internalType": "bool", + "name": "enableDonation", + "type": "bool" + } + ], + "internalType": "struct LiquidityManagement", + "name": "liquidityManagement", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "staticSwapFeePercentage", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "aggregateSwapFeePercentage", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "aggregateYieldFeePercentage", + "type": "uint256" + }, + { + "internalType": "uint40", + "name": "tokenDecimalDiffs", + "type": "uint40" + }, + { + "internalType": "uint32", + "name": "pauseWindowEndTime", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "isPoolRegistered", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isPoolInitialized", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isPoolPaused", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isPoolInRecoveryMode", + "type": "bool" + } + ], + "internalType": "struct PoolConfig", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "getPoolData", + "outputs": [ + { + "components": [ + { + "internalType": "PoolConfigBits", + "name": "poolConfigBits", + "type": "bytes32" + }, + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + }, + { + "components": [ + { + "internalType": "enum TokenType", + "name": "tokenType", + "type": "uint8" + }, + { + "internalType": "contract IRateProvider", + "name": "rateProvider", + "type": "address" + }, + { + "internalType": "bool", + "name": "paysYieldFees", + "type": "bool" + } + ], + "internalType": "struct TokenInfo[]", + "name": "tokenInfo", + "type": "tuple[]" + }, + { + "internalType": "uint256[]", + "name": "balancesRaw", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "balancesLiveScaled18", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "tokenRates", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "decimalScalingFactors", + "type": "uint256[]" + } + ], + "internalType": "struct PoolData", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "getPoolPausedState", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + }, + { + "internalType": "uint32", + "name": "", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "", + "type": "uint32" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "getPoolRoleAccounts", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "pauseManager", + "type": "address" + }, + { + "internalType": "address", + "name": "swapFeeManager", + "type": "address" + }, + { + "internalType": "address", + "name": "poolCreator", + "type": "address" + } + ], + "internalType": "struct PoolRoleAccounts", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "getPoolTokenInfo", + "outputs": [ + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + }, + { + "components": [ + { + "internalType": "enum TokenType", + "name": "tokenType", + "type": "uint8" + }, + { + "internalType": "contract IRateProvider", + "name": "rateProvider", + "type": "address" + }, + { + "internalType": "bool", + "name": "paysYieldFees", + "type": "bool" + } + ], + "internalType": "struct TokenInfo[]", + "name": "tokenInfo", + "type": "tuple[]" + }, + { + "internalType": "uint256[]", + "name": "balancesRaw", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "lastBalancesLiveScaled18", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "getPoolTokenRates", + "outputs": [ + { + "internalType": "uint256[]", + "name": "decimalScalingFactors", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "tokenRates", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "getPoolTokens", + "outputs": [ + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getProtocolFeeController", + "outputs": [ + { + "internalType": "contract IProtocolFeeController", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "name": "getReservesOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "getStaticSwapFeePercentage", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "name": "getTokenDelta", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getVaultAdmin", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "exactAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "minBptAmountOut", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "initialize", + "outputs": [ + { + "internalType": "uint256", + "name": "bptAmountOut", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC4626", + "name": "wrappedToken", + "type": "address" + } + ], + "name": "isERC4626BufferInitialized", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "isPoolInRecoveryMode", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "isPoolInitialized", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "isPoolPaused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "isPoolRegistered", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isQueryDisabled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isQueryDisabledPermanently", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isUnlocked", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "quote", + "outputs": [ + { + "internalType": "bytes", + "name": "result", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "quoteAndRevert", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "reentrancyGuardEntered", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "enum TokenType", + "name": "tokenType", + "type": "uint8" + }, + { + "internalType": "contract IRateProvider", + "name": "rateProvider", + "type": "address" + }, + { + "internalType": "bool", + "name": "paysYieldFees", + "type": "bool" + } + ], + "internalType": "struct TokenConfig[]", + "name": "tokenConfig", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "swapFeePercentage", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "pauseWindowEndTime", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "protocolFeeExempt", + "type": "bool" + }, + { + "components": [ + { + "internalType": "address", + "name": "pauseManager", + "type": "address" + }, + { + "internalType": "address", + "name": "swapFeeManager", + "type": "address" + }, + { + "internalType": "address", + "name": "poolCreator", + "type": "address" + } + ], + "internalType": "struct PoolRoleAccounts", + "name": "roleAccounts", + "type": "tuple" + }, + { + "internalType": "address", + "name": "poolHooksContract", + "type": "address" + }, + { + "components": [ + { + "internalType": "bool", + "name": "disableUnbalancedLiquidity", + "type": "bool" + }, + { + "internalType": "bool", + "name": "enableAddLiquidityCustom", + "type": "bool" + }, + { + "internalType": "bool", + "name": "enableRemoveLiquidityCustom", + "type": "bool" + }, + { + "internalType": "bool", + "name": "enableDonation", + "type": "bool" + } + ], + "internalType": "struct LiquidityManagement", + "name": "liquidityManagement", + "type": "tuple" + } + ], + "name": "registerPool", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "exactBptAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "minAmountsOut", + "type": "uint256[]" + } + ], + "name": "removeLiquidityRecovery", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amountsOutRaw", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vault", + "outputs": [ + { + "internalType": "contract IVault", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/src/abi/cables/CablesMainnetRFQ.json b/src/abi/cables/CablesMainnetRFQ.json new file mode 100644 index 000000000..11cd0fade --- /dev/null +++ b/src/abi/cables/CablesMainnetRFQ.json @@ -0,0 +1,1083 @@ +[ + { + "inputs": [], + "name": "AccessControlBadConfirmation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "neededRole", + "type": "bytes32" + } + ], + "name": "AccessControlUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "AddressInsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "EnforcedPause", + "type": "error" + }, + { + "inputs": [], + "name": "ExpectedPause", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { + "inputs": [], + "name": "addressCannotBeZero", + "type": "error" + }, + { + "inputs": [], + "name": "atLeastOneAdminNeeded", + "type": "error" + }, + { + "inputs": [], + "name": "batchArraysMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "expired", + "type": "error" + }, + { + "inputs": [], + "name": "expiredByOverride", + "type": "error" + }, + { + "inputs": [], + "name": "invalidMsgValue", + "type": "error" + }, + { + "inputs": [], + "name": "invalidNonce", + "type": "error" + }, + { + "inputs": [], + "name": "invalidSignature", + "type": "error" + }, + { + "inputs": [], + "name": "swapSignerCannotBeZero", + "type": "error" + }, + { + "inputs": [], + "name": "transferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "wethAddressCannotBeZero", + "type": "error" + }, + { + "inputs": [], + "name": "zeroAmount", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "actionName", + "type": "string" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAddress", + "type": "address" + } + ], + "name": "AddressSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "EIP712DomainChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "RebalancerWithdraw", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32" + } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "actionName", + "type": "string" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "updatedRole", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "address", + "name": "updatedAddress", + "type": "address" + } + ], + "name": "RoleUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "nonceAndMeta", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "taker", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "destTrader", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "destChainId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "srcAsset", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "destAsset", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "srcAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "destAmount", + "type": "uint256" + } + ], + "name": "SwapExecuted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "nonceAndMeta", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "SwapExpired", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "newSwapSigner", + "type": "address" + } + ], + "name": "SwapSignerUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "REBALANCER_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "VERSION", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "addAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "addRebalancer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_assets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_amounts", + "type": "uint256[]" + } + ], + "name": "batchClaimBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_asset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "claimBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "completedSwaps", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "eip712Domain", + "outputs": [ + { + "internalType": "bytes1", + "name": "fields", + "type": "bytes1" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "version", + "type": "string" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "verifyingContract", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "uint256[]", + "name": "extensions", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "expiredSwaps", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "getRoleMember", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleMemberCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "hasRole", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_swapSigner", + "type": "address" + }, + { + "internalType": "address", + "name": "_wethAddress", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "isAdmin", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "isRebalancer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_hash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "_signature", + "type": "bytes" + } + ], + "name": "isValidSignature", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "nonceAndMeta", + "type": "uint256" + }, + { + "internalType": "uint128", + "name": "expiry", + "type": "uint128" + }, + { + "internalType": "address", + "name": "makerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "takerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "taker", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makerAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takerAmount", + "type": "uint256" + } + ], + "internalType": "struct CablesRFQ.Order", + "name": "_order", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "_signature", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "_takerAmount", + "type": "uint256" + } + ], + "name": "partialSwap", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "removeAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "removeRebalancer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "callerConfirmation", + "type": "address" + } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_swapSigner", + "type": "address" + } + ], + "name": "setSwapSigner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "nonceAndMeta", + "type": "uint256" + }, + { + "internalType": "uint128", + "name": "expiry", + "type": "uint128" + }, + { + "internalType": "address", + "name": "makerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "takerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "taker", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makerAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takerAmount", + "type": "uint256" + } + ], + "internalType": "struct CablesRFQ.Order", + "name": "_order", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "_signature", + "type": "bytes" + } + ], + "name": "simpleSwap", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "swapSigner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_nonceAndMeta", + "type": "uint256" + } + ], + "name": "updateSwapExpiry", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "wethAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/src/abi/fluid-dex/fluid-dex.abi.json b/src/abi/fluid-dex/fluid-dex.abi.json index 14ada197a..cdd5e0f1a 100644 --- a/src/abi/fluid-dex/fluid-dex.abi.json +++ b/src/abi/fluid-dex/fluid-dex.abi.json @@ -1,9 +1,119 @@ [ + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "dexId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "liquidity", + "type": "address" + }, + { + "internalType": "address", + "name": "factory", + "type": "address" + }, + { + "components": [ + { + "internalType": "address", + "name": "shift", + "type": "address" + }, + { + "internalType": "address", + "name": "admin", + "type": "address" + }, + { + "internalType": "address", + "name": "colOperations", + "type": "address" + }, + { + "internalType": "address", + "name": "debtOperations", + "type": "address" + }, + { + "internalType": "address", + "name": "perfectOperationsAndSwapOut", + "type": "address" + } + ], + "internalType": "struct Structs.Implementations", + "name": "implementations", + "type": "tuple" + }, + { + "internalType": "address", + "name": "deployerContract", + "type": "address" + }, + { + "internalType": "address", + "name": "token0", + "type": "address" + }, + { + "internalType": "address", + "name": "token1", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "supplyToken0Slot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "borrowToken0Slot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "supplyToken1Slot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "borrowToken1Slot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "exchangePriceToken0Slot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "exchangePriceToken1Slot", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "oracleMapping", + "type": "uint256" + } + ], + "internalType": "struct Structs.ConstantViews", + "name": "constantViews_", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, { "inputs": [ { "internalType": "uint256", - "name": "errorId", + "name": "errorId_", "type": "uint256" } ], @@ -14,7 +124,18 @@ "inputs": [ { "internalType": "uint256", - "name": "shares", + "name": "errorId", + "type": "uint256" + } + ], + "name": "FluidDexFactoryError", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares_", "type": "uint256" } ], @@ -87,7 +208,7 @@ "type": "uint256" } ], - "internalType": "struct IFluidDexT1.PricesAndExchangePrice", + "internalType": "struct Structs.PricesAndExchangePrice", "name": "pex_", "type": "tuple" } @@ -117,6 +238,346 @@ "name": "FluidDexSwapResult", "type": "error" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "errorId_", + "type": "uint256" + } + ], + "name": "FluidLiquidityCalcsError", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "errorId_", + "type": "uint256" + } + ], + "name": "FluidSafeTransferError", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "int256", + "name": "routing", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amtOut", + "type": "uint256" + } + ], + "name": "LogArbitrage", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "LogBorrowDebtLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token0Amt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token1Amt", + "type": "uint256" + } + ], + "name": "LogBorrowPerfectDebtLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "LogDepositColLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token0Amt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token1Amt", + "type": "uint256" + } + ], + "name": "LogDepositPerfectColLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + ], + "name": "LogPauseSwapAndArbitrage", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + ], + "name": "LogUnpauseSwapAndArbitrage", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token0Amt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token1Amt", + "type": "uint256" + } + ], + "name": "LogPaybackDebtInOneToken", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "LogPaybackDebtLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token0Amt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token1Amt", + "type": "uint256" + } + ], + "name": "LogPaybackPerfectDebtLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token0Amt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token1Amt", + "type": "uint256" + } + ], + "name": "LogWithdrawColInOneToken", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "LogWithdrawColLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token0Amt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token1Amt", + "type": "uint256" + } + ], + "name": "LogWithdrawPerfectColLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "swap0to1", + "type": "bool" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "Swap", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, { "inputs": [], "name": "DEX_ID", @@ -148,9 +609,9 @@ "type": "uint256" }, { - "internalType": "bool", - "name": "estimate_", - "type": "bool" + "internalType": "address", + "name": "to_", + "type": "address" } ], "name": "borrow", @@ -182,9 +643,9 @@ "type": "uint256" }, { - "internalType": "bool", - "name": "estimate_", - "type": "bool" + "internalType": "address", + "name": "to_", + "type": "address" } ], "name": "borrowPerfect", @@ -248,11 +709,11 @@ }, { "internalType": "address", - "name": "perfectOperationsAndOracle", + "name": "perfectOperationsAndSwapOut", "type": "address" } ], - "internalType": "struct IFluidDexT1.Implementations", + "internalType": "struct Structs.Implementations", "name": "implementations", "type": "tuple" }, @@ -307,7 +768,7 @@ "type": "uint256" } ], - "internalType": "struct IFluidDexT1.ConstantViews", + "internalType": "struct Structs.ConstantViews", "name": "constantsView_", "type": "tuple" } @@ -342,7 +803,7 @@ "type": "uint256" } ], - "internalType": "struct IFluidDexT1.ConstantViews2", + "internalType": "struct Structs.ConstantViews2", "name": "constantsView2_", "type": "tuple" } @@ -476,7 +937,7 @@ "type": "uint256" } ], - "internalType": "struct IFluidDexT1.CollateralReserves", + "internalType": "struct Structs.CollateralReserves", "name": "c_", "type": "tuple" } @@ -547,7 +1008,7 @@ "type": "uint256" } ], - "internalType": "struct IFluidDexT1.DebtReserves", + "internalType": "struct Structs.DebtReserves", "name": "d_", "type": "tuple" } @@ -562,6 +1023,29 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "token_", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount_", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data_", + "type": "bytes" + } + ], + "name": "liquidityCallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -605,7 +1089,7 @@ "type": "uint256" } ], - "internalType": "struct IFluidDexT1.Oracle[]", + "internalType": "struct Structs.Oracle[]", "name": "twaps_", "type": "tuple[]" }, @@ -778,6 +1262,40 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bool", + "name": "swap0to1_", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "amountIn_", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin_", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to_", + "type": "address" + } + ], + "name": "swapInWithCallback", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut_", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { @@ -812,6 +1330,40 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bool", + "name": "swap0to1_", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "amountOut_", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountInMax_", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to_", + "type": "address" + } + ], + "name": "swapOutWithCallback", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn_", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { @@ -830,9 +1382,9 @@ "type": "uint256" }, { - "internalType": "bool", - "name": "estimate_", - "type": "bool" + "internalType": "address", + "name": "to_", + "type": "address" } ], "name": "withdraw", @@ -864,9 +1416,9 @@ "type": "uint256" }, { - "internalType": "bool", - "name": "estimate_", - "type": "bool" + "internalType": "address", + "name": "to_", + "type": "address" } ], "name": "withdrawPerfect", @@ -903,9 +1455,9 @@ "type": "uint256" }, { - "internalType": "bool", - "name": "estimate_", - "type": "bool" + "internalType": "address", + "name": "to_", + "type": "address" } ], "name": "withdrawPerfectInOneToken", @@ -918,5 +1470,9 @@ ], "stateMutability": "nonpayable", "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" } ] diff --git a/src/abi/permit2.json b/src/abi/permit2.json new file mode 100644 index 000000000..22201c714 --- /dev/null +++ b/src/abi/permit2.json @@ -0,0 +1,581 @@ +[ + { + "inputs": [ + { "internalType": "uint256", "name": "deadline", "type": "uint256" } + ], + "name": "AllowanceExpired", + "type": "error" + }, + { "inputs": [], "name": "ExcessiveInvalidation", "type": "error" }, + { + "inputs": [ + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "maxAmount", "type": "uint256" } + ], + "name": "InvalidAmount", + "type": "error" + }, + { "inputs": [], "name": "InvalidContractSignature", "type": "error" }, + { "inputs": [], "name": "InvalidNonce", "type": "error" }, + { "inputs": [], "name": "InvalidSignature", "type": "error" }, + { "inputs": [], "name": "InvalidSignatureLength", "type": "error" }, + { "inputs": [], "name": "InvalidSigner", "type": "error" }, + { "inputs": [], "name": "LengthMismatch", "type": "error" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "signatureDeadline", + "type": "uint256" + } + ], + "name": "SignatureExpired", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "indexed": false, + "internalType": "uint48", + "name": "expiration", + "type": "uint48" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "Lockdown", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint48", + "name": "newNonce", + "type": "uint48" + }, + { + "indexed": false, + "internalType": "uint48", + "name": "oldNonce", + "type": "uint48" + } + ], + "name": "NonceInvalidation", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "indexed": false, + "internalType": "uint48", + "name": "expiration", + "type": "uint48" + }, + { + "indexed": false, + "internalType": "uint48", + "name": "nonce", + "type": "uint48" + } + ], + "name": "Permit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "word", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mask", + "type": "uint256" + } + ], + "name": "UnorderedNonceInvalidation", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "address", "name": "", "type": "address" } + ], + "name": "allowance", + "outputs": [ + { "internalType": "uint160", "name": "amount", "type": "uint160" }, + { "internalType": "uint48", "name": "expiration", "type": "uint48" }, + { "internalType": "uint48", "name": "nonce", "type": "uint48" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint160", "name": "amount", "type": "uint160" }, + { "internalType": "uint48", "name": "expiration", "type": "uint48" } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint48", "name": "newNonce", "type": "uint48" } + ], + "name": "invalidateNonces", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "wordPos", "type": "uint256" }, + { "internalType": "uint256", "name": "mask", "type": "uint256" } + ], + "name": "invalidateUnorderedNonces", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" } + ], + "internalType": "struct IAllowanceTransfer.TokenSpenderPair[]", + "name": "approvals", + "type": "tuple[]" + } + ], + "name": "lockdown", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "nonceBitmap", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "token", "type": "address" }, + { + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "internalType": "uint48", + "name": "expiration", + "type": "uint48" + }, + { "internalType": "uint48", "name": "nonce", "type": "uint48" } + ], + "internalType": "struct IAllowanceTransfer.PermitDetails[]", + "name": "details", + "type": "tuple[]" + }, + { "internalType": "address", "name": "spender", "type": "address" }, + { + "internalType": "uint256", + "name": "sigDeadline", + "type": "uint256" + } + ], + "internalType": "struct IAllowanceTransfer.PermitBatch", + "name": "permitBatch", + "type": "tuple" + }, + { "internalType": "bytes", "name": "signature", "type": "bytes" } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "token", "type": "address" }, + { + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "internalType": "uint48", + "name": "expiration", + "type": "uint48" + }, + { "internalType": "uint48", "name": "nonce", "type": "uint48" } + ], + "internalType": "struct IAllowanceTransfer.PermitDetails", + "name": "details", + "type": "tuple" + }, + { "internalType": "address", "name": "spender", "type": "address" }, + { + "internalType": "uint256", + "name": "sigDeadline", + "type": "uint256" + } + ], + "internalType": "struct IAllowanceTransfer.PermitSingle", + "name": "permitSingle", + "type": "tuple" + }, + { "internalType": "bytes", "name": "signature", "type": "bytes" } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.TokenPermissions", + "name": "permitted", + "type": "tuple" + }, + { "internalType": "uint256", "name": "nonce", "type": "uint256" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.PermitTransferFrom", + "name": "permit", + "type": "tuple" + }, + { + "components": [ + { "internalType": "address", "name": "to", "type": "address" }, + { + "internalType": "uint256", + "name": "requestedAmount", + "type": "uint256" + } + ], + "internalType": "struct ISignatureTransfer.SignatureTransferDetails", + "name": "transferDetails", + "type": "tuple" + }, + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "bytes", "name": "signature", "type": "bytes" } + ], + "name": "permitTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.TokenPermissions[]", + "name": "permitted", + "type": "tuple[]" + }, + { "internalType": "uint256", "name": "nonce", "type": "uint256" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.PermitBatchTransferFrom", + "name": "permit", + "type": "tuple" + }, + { + "components": [ + { "internalType": "address", "name": "to", "type": "address" }, + { + "internalType": "uint256", + "name": "requestedAmount", + "type": "uint256" + } + ], + "internalType": "struct ISignatureTransfer.SignatureTransferDetails[]", + "name": "transferDetails", + "type": "tuple[]" + }, + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "bytes", "name": "signature", "type": "bytes" } + ], + "name": "permitTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.TokenPermissions", + "name": "permitted", + "type": "tuple" + }, + { "internalType": "uint256", "name": "nonce", "type": "uint256" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.PermitTransferFrom", + "name": "permit", + "type": "tuple" + }, + { + "components": [ + { "internalType": "address", "name": "to", "type": "address" }, + { + "internalType": "uint256", + "name": "requestedAmount", + "type": "uint256" + } + ], + "internalType": "struct ISignatureTransfer.SignatureTransferDetails", + "name": "transferDetails", + "type": "tuple" + }, + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "bytes32", "name": "witness", "type": "bytes32" }, + { + "internalType": "string", + "name": "witnessTypeString", + "type": "string" + }, + { "internalType": "bytes", "name": "signature", "type": "bytes" } + ], + "name": "permitWitnessTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.TokenPermissions[]", + "name": "permitted", + "type": "tuple[]" + }, + { "internalType": "uint256", "name": "nonce", "type": "uint256" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.PermitBatchTransferFrom", + "name": "permit", + "type": "tuple" + }, + { + "components": [ + { "internalType": "address", "name": "to", "type": "address" }, + { + "internalType": "uint256", + "name": "requestedAmount", + "type": "uint256" + } + ], + "internalType": "struct ISignatureTransfer.SignatureTransferDetails[]", + "name": "transferDetails", + "type": "tuple[]" + }, + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "bytes32", "name": "witness", "type": "bytes32" }, + { + "internalType": "string", + "name": "witnessTypeString", + "type": "string" + }, + { "internalType": "bytes", "name": "signature", "type": "bytes" } + ], + "name": "permitWitnessTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint160", "name": "amount", "type": "uint160" }, + { "internalType": "address", "name": "token", "type": "address" } + ], + "internalType": "struct IAllowanceTransfer.AllowanceTransferDetails[]", + "name": "transferDetails", + "type": "tuple[]" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint160", "name": "amount", "type": "uint160" }, + { "internalType": "address", "name": "token", "type": "address" } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/abi/usual-m-usd0/usualCollateralDao.abi.json b/src/abi/usual-m-usd0/usualCollateralDao.abi.json new file mode 100644 index 000000000..9fe20473a --- /dev/null +++ b/src/abi/usual-m-usd0/usualCollateralDao.abi.json @@ -0,0 +1,681 @@ +[ + { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, + { + "inputs": [ + { "internalType": "address", "name": "target", "type": "address" } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "AddressInsufficientBalance", + "type": "error" + }, + { "inputs": [], "name": "AmountIsZero", "type": "error" }, + { "inputs": [], "name": "AmountTooBig", "type": "error" }, + { "inputs": [], "name": "AmountTooLow", "type": "error" }, + { "inputs": [], "name": "ApprovalFailed", "type": "error" }, + { "inputs": [], "name": "CBRIsNull", "type": "error" }, + { "inputs": [], "name": "CBRIsTooHigh", "type": "error" }, + { "inputs": [], "name": "EnforcedPause", "type": "error" }, + { "inputs": [], "name": "ExpectedPause", "type": "error" }, + { + "inputs": [ + { "internalType": "uint256", "name": "deadline", "type": "uint256" } + ], + "name": "ExpiredSignature", + "type": "error" + }, + { "inputs": [], "name": "FailedInnerCall", "type": "error" }, + { + "inputs": [ + { "internalType": "address", "name": "account", "type": "address" }, + { "internalType": "uint256", "name": "currentNonce", "type": "uint256" } + ], + "name": "InvalidAccountNonce", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "approvalDeadline", + "type": "uint256" + }, + { "internalType": "uint256", "name": "intentDeadline", "type": "uint256" } + ], + "name": "InvalidDeadline", + "type": "error" + }, + { "inputs": [], "name": "InvalidInitialization", "type": "error" }, + { + "inputs": [ + { "internalType": "address", "name": "account", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "InvalidOrderAmount", + "type": "error" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "name": "InvalidSigner", + "type": "error" + }, + { "inputs": [], "name": "InvalidToken", "type": "error" }, + { "inputs": [], "name": "MathOverflowedMulDiv", "type": "error" }, + { "inputs": [], "name": "NoOrdersIdsProvided", "type": "error" }, + { "inputs": [], "name": "NotAuthorized", "type": "error" }, + { "inputs": [], "name": "NotInitializing", "type": "error" }, + { "inputs": [], "name": "NullContract", "type": "error" }, + { "inputs": [], "name": "RedeemFeeTooBig", "type": "error" }, + { "inputs": [], "name": "RedeemMustBePaused", "type": "error" }, + { "inputs": [], "name": "RedeemMustNotBePaused", "type": "error" }, + { "inputs": [], "name": "ReentrancyGuardReentrantCall", "type": "error" }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { "inputs": [], "name": "SameValue", "type": "error" }, + { "inputs": [], "name": "SwapMustBePaused", "type": "error" }, + { "inputs": [], "name": "SwapMustNotBePaused", "type": "error" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "cbrCoef", + "type": "uint256" + } + ], + "name": "CBRActivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "CBRDeactivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "EIP712DomainChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "tokenSwapped", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalAmountInTokenDecimals", + "type": "uint256" + } + ], + "name": "IntentConsumed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "tokenSwapped", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountInTokenDecimals", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountInUSD", + "type": "uint256" + } + ], + "name": "IntentMatched", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "nonceInvalidated", + "type": "uint256" + } + ], + "name": "NonceInvalidated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newThreshold", + "type": "uint256" + } + ], + "name": "NonceThresholdSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "redeemer", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "rwaToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountRedeemed", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "returnedRwaAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "stableFeeAmount", + "type": "uint256" + } + ], + "name": "Redeem", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "redeemFee", + "type": "uint256" + } + ], + "name": "RedeemFeeUpdated", + "type": "event" + }, + { "anonymous": false, "inputs": [], "name": "RedeemPaused", "type": "event" }, + { + "anonymous": false, + "inputs": [], + "name": "RedeemUnPaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "tokenSwapped", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountInUSD", + "type": "uint256" + } + ], + "name": "Swap", + "type": "event" + }, + { "anonymous": false, "inputs": [], "name": "SwapPaused", "type": "event" }, + { "anonymous": false, "inputs": [], "name": "SwapUnPaused", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DaoCollateralStorageV0Location", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "coefficient", "type": "uint256" } + ], + "name": "activateCBR", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "cbrCoef", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "deactivateCBR", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "eip712Domain", + "outputs": [ + { "internalType": "bytes1", "name": "fields", "type": "bytes1" }, + { "internalType": "string", "name": "name", "type": "string" }, + { "internalType": "string", "name": "version", "type": "string" }, + { "internalType": "uint256", "name": "chainId", "type": "uint256" }, + { + "internalType": "address", + "name": "verifyingContract", + "type": "address" + }, + { "internalType": "bytes32", "name": "salt", "type": "bytes32" }, + { "internalType": "uint256[]", "name": "extensions", "type": "uint256[]" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_registryContract", + "type": "address" + }, + { "internalType": "uint256", "name": "_redeemFee", "type": "uint256" } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_registryContract", + "type": "address" + } + ], + "name": "initializeV1", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "invalidateNonce", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "newNonce", "type": "uint256" } + ], + "name": "invalidateUpToNonce", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "isCBROn", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isRedeemPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isSwapPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nonceThreshold", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "name": "nonces", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "name": "orderAmountTakenCurrentNonce", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "pauseRedeem", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "pauseSwap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "rwaToken", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "uint256", "name": "minAmountOut", "type": "uint256" } + ], + "name": "redeem", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "rwaToken", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "redeemDao", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "redeemFee", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "threshold", "type": "uint256" } + ], + "name": "setNonceThreshold", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "_redeemFee", "type": "uint256" } + ], + "name": "setRedeemFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "rwaToken", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "uint256", "name": "minAmountOut", "type": "uint256" } + ], + "name": "swap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "rwaToken", "type": "address" }, + { + "internalType": "uint256", + "name": "amountInTokenDecimals", + "type": "uint256" + }, + { "internalType": "bool", "name": "partialMatching", "type": "bool" }, + { + "internalType": "uint256[]", + "name": "orderIdsToTake", + "type": "uint256[]" + }, + { + "components": [ + { "internalType": "uint256", "name": "deadline", "type": "uint256" }, + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "internalType": "struct Approval", + "name": "approval", + "type": "tuple" + } + ], + "name": "swapRWAtoStbc", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "orderIdsToTake", + "type": "uint256[]" + }, + { + "components": [ + { "internalType": "uint256", "name": "deadline", "type": "uint256" }, + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "internalType": "struct Approval", + "name": "approval", + "type": "tuple" + }, + { + "components": [ + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "address", "name": "rwaToken", "type": "address" }, + { + "internalType": "uint256", + "name": "amountInTokenDecimals", + "type": "uint256" + }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" }, + { "internalType": "bytes", "name": "signature", "type": "bytes" } + ], + "internalType": "struct Intent", + "name": "intent", + "type": "tuple" + }, + { "internalType": "bool", "name": "partialMatching", "type": "bool" } + ], + "name": "swapRWAtoStbcIntent", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "rwaToken", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "uint256", "name": "minAmountOut", "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": "swapWithPermit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpauseRedeem", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpauseSwap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/abi/usual-m-wrapped-m/usualM.abi.json b/src/abi/usual-m-wrapped-m/usualM.abi.json new file mode 100644 index 000000000..53ea08cd6 --- /dev/null +++ b/src/abi/usual-m-wrapped-m/usualM.abi.json @@ -0,0 +1,509 @@ +[ + { "type": "constructor", "inputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "DECIMALS_NUMBER", + "inputs": [], + "outputs": [{ "name": "", "type": "uint8", "internalType": "uint8" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "DOMAIN_SEPARATOR", + "inputs": [], + "outputs": [{ "name": "", "type": "bytes32", "internalType": "bytes32" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "UsualMStorageV0Location", + "inputs": [], + "outputs": [{ "name": "", "type": "bytes32", "internalType": "bytes32" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "allowance", + "inputs": [ + { "name": "owner", "type": "address", "internalType": "address" }, + { "name": "spender", "type": "address", "internalType": "address" } + ], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "approve", + "inputs": [ + { "name": "spender", "type": "address", "internalType": "address" }, + { "name": "value", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { "name": "account", "type": "address", "internalType": "address" } + ], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "blacklist", + "inputs": [ + { "name": "account", "type": "address", "internalType": "address" } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "decimals", + "inputs": [], + "outputs": [{ "name": "", "type": "uint8", "internalType": "uint8" }], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "eip712Domain", + "inputs": [], + "outputs": [ + { "name": "fields", "type": "bytes1", "internalType": "bytes1" }, + { "name": "name", "type": "string", "internalType": "string" }, + { "name": "version", "type": "string", "internalType": "string" }, + { "name": "chainId", "type": "uint256", "internalType": "uint256" }, + { + "name": "verifyingContract", + "type": "address", + "internalType": "address" + }, + { "name": "salt", "type": "bytes32", "internalType": "bytes32" }, + { "name": "extensions", "type": "uint256[]", "internalType": "uint256[]" } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getWrappableAmount", + "inputs": [ + { "name": "amount", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { "name": "wrappedM_", "type": "address", "internalType": "address" }, + { + "name": "registryAccess_", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "isBlacklisted", + "inputs": [ + { "name": "account", "type": "address", "internalType": "address" } + ], + "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "mintCap", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [{ "name": "", "type": "string", "internalType": "string" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "nonces", + "inputs": [ + { "name": "owner", "type": "address", "internalType": "address" } + ], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pause", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "paused", + "inputs": [], + "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "permit", + "inputs": [ + { "name": "owner", "type": "address", "internalType": "address" }, + { "name": "spender", "type": "address", "internalType": "address" }, + { "name": "value", "type": "uint256", "internalType": "uint256" }, + { "name": "deadline", "type": "uint256", "internalType": "uint256" }, + { "name": "v", "type": "uint8", "internalType": "uint8" }, + { "name": "r", "type": "bytes32", "internalType": "bytes32" }, + { "name": "s", "type": "bytes32", "internalType": "bytes32" } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "registryAccess", + "inputs": [], + "outputs": [{ "name": "", "type": "address", "internalType": "address" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setMintCap", + "inputs": [ + { "name": "newMintCap", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "symbol", + "inputs": [], + "outputs": [{ "name": "", "type": "string", "internalType": "string" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalSupply", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transfer", + "inputs": [ + { "name": "to", "type": "address", "internalType": "address" }, + { "name": "value", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferFrom", + "inputs": [ + { "name": "from", "type": "address", "internalType": "address" }, + { "name": "to", "type": "address", "internalType": "address" }, + { "name": "value", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "unBlacklist", + "inputs": [ + { "name": "account", "type": "address", "internalType": "address" } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "unpause", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "unwrap", + "inputs": [ + { "name": "recipient", "type": "address", "internalType": "address" }, + { "name": "amount", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "wrap", + "inputs": [ + { "name": "recipient", "type": "address", "internalType": "address" }, + { "name": "amount", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "wrapWithPermit", + "inputs": [ + { "name": "recipient", "type": "address", "internalType": "address" }, + { "name": "amount", "type": "uint256", "internalType": "uint256" }, + { "name": "deadline", "type": "uint256", "internalType": "uint256" }, + { "name": "v", "type": "uint8", "internalType": "uint8" }, + { "name": "r", "type": "bytes32", "internalType": "bytes32" }, + { "name": "s", "type": "bytes32", "internalType": "bytes32" } + ], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "wrappedM", + "inputs": [], + "outputs": [{ "name": "", "type": "address", "internalType": "address" }], + "stateMutability": "view" + }, + { + "type": "event", + "name": "Approval", + "inputs": [ + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "spender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Blacklist", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "EIP712DomainChanged", + "inputs": [], + "anonymous": false + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "MintCapSet", + "inputs": [ + { + "name": "newMintCap", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Paused", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Transfer", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "UnBlacklist", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Unpaused", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { "type": "error", "name": "Blacklisted", "inputs": [] }, + { "type": "error", "name": "ECDSAInvalidSignature", "inputs": [] }, + { + "type": "error", + "name": "ECDSAInvalidSignatureLength", + "inputs": [ + { "name": "length", "type": "uint256", "internalType": "uint256" } + ] + }, + { + "type": "error", + "name": "ECDSAInvalidSignatureS", + "inputs": [{ "name": "s", "type": "bytes32", "internalType": "bytes32" }] + }, + { + "type": "error", + "name": "ERC20InsufficientAllowance", + "inputs": [ + { "name": "spender", "type": "address", "internalType": "address" }, + { "name": "allowance", "type": "uint256", "internalType": "uint256" }, + { "name": "needed", "type": "uint256", "internalType": "uint256" } + ] + }, + { + "type": "error", + "name": "ERC20InsufficientBalance", + "inputs": [ + { "name": "sender", "type": "address", "internalType": "address" }, + { "name": "balance", "type": "uint256", "internalType": "uint256" }, + { "name": "needed", "type": "uint256", "internalType": "uint256" } + ] + }, + { + "type": "error", + "name": "ERC20InvalidApprover", + "inputs": [ + { "name": "approver", "type": "address", "internalType": "address" } + ] + }, + { + "type": "error", + "name": "ERC20InvalidReceiver", + "inputs": [ + { "name": "receiver", "type": "address", "internalType": "address" } + ] + }, + { + "type": "error", + "name": "ERC20InvalidSender", + "inputs": [ + { "name": "sender", "type": "address", "internalType": "address" } + ] + }, + { + "type": "error", + "name": "ERC20InvalidSpender", + "inputs": [ + { "name": "spender", "type": "address", "internalType": "address" } + ] + }, + { + "type": "error", + "name": "ERC2612ExpiredSignature", + "inputs": [ + { "name": "deadline", "type": "uint256", "internalType": "uint256" } + ] + }, + { + "type": "error", + "name": "ERC2612InvalidSigner", + "inputs": [ + { "name": "signer", "type": "address", "internalType": "address" }, + { "name": "owner", "type": "address", "internalType": "address" } + ] + }, + { "type": "error", "name": "EnforcedPause", "inputs": [] }, + { "type": "error", "name": "ExpectedPause", "inputs": [] }, + { + "type": "error", + "name": "InvalidAccountNonce", + "inputs": [ + { "name": "account", "type": "address", "internalType": "address" }, + { "name": "currentNonce", "type": "uint256", "internalType": "uint256" } + ] + }, + { "type": "error", "name": "InvalidAmount", "inputs": [] }, + { "type": "error", "name": "InvalidInitialization", "inputs": [] }, + { "type": "error", "name": "InvalidUInt96", "inputs": [] }, + { "type": "error", "name": "MintCapExceeded", "inputs": [] }, + { "type": "error", "name": "NotAuthorized", "inputs": [] }, + { "type": "error", "name": "NotInitializing", "inputs": [] }, + { "type": "error", "name": "SameValue", "inputs": [] }, + { "type": "error", "name": "ZeroAddress", "inputs": [] }, + { "type": "error", "name": "ZeroRegistryAccess", "inputs": [] }, + { "type": "error", "name": "ZeroWrappedM", "inputs": [] } +] diff --git a/src/bigint-constants.ts b/src/bigint-constants.ts index 08e714a74..33754b387 100644 --- a/src/bigint-constants.ts +++ b/src/bigint-constants.ts @@ -11,6 +11,7 @@ export const BI_MAX_INT = BigInt(MAX_INT); export const BI_MAX_UINT8 = 2n ** 8n - 1n; export const BI_MAX_UINT16 = 2n ** 16n - 1n; export const BI_MAX_UINT32 = 2n ** 32n - 1n; +export const BI_MAX_UINT48 = 2n ** 48n - 1n; export const BI_MAX_UINT64 = 2n ** 64n - 1n; export const BI_MAX_UINT96 = 2n ** 96n - 1n; export const BI_MAX_UINT128 = 2n ** 128n - 1n; diff --git a/src/config.ts b/src/config.ts index 9f5b26fe8..3b905b882 100644 --- a/src/config.ts +++ b/src/config.ts @@ -454,6 +454,35 @@ const baseConfigs: { [network: number]: BaseConfig } = { rpcPollingBlocksBackToTriggerUpdate: 3, forceRpcFallbackDexs: [], }, + [Network.SEPOLIA]: { + network: Network.SEPOLIA, + networkName: 'Sepolia', + isTestnet: false, + nativeTokenName: 'Ether', + nativeTokenSymbol: 'ETH', + wrappedNativeTokenAddress: '0x7b79995e5f793a07bc00c21412e50ecae098e7f9', + hasEIP1559: true, + augustusAddress: '0x0000000000000000000000000000000000000000', + augustusRFQAddress: '0xF6322953d6bFcEACf77D90BC9a01B055249D44fE', + tokenTransferProxyAddress: '0x0000000000000000000000000000000000000000', + multicallV2Address: '0xcA11bde05977b3631167028862bE2a173976CA11', + privateHttpProvider: process.env.HTTP_PROVIDER_11155111, + augustusV6Address: '0x6a000f20005980200259b80c5102003040001068', + adapterAddresses: {}, + rfqConfigs: {}, + hashFlowDisabledMMs: [], + executorsAddresses: { + Executor01: '0x000010036c0190e009a000d0fc3541100a07380a', + Executor02: '0x00c600b30fb0400701010f4b080409018b9006e0', + Executor03: '0xe009f00e200a090090fc70e02d70b232000c0802', + }, + uniswapV2ExchangeRouterAddress: + '0x0000000000000000000000000000000000000000', + rpcPollingMaxAllowedStateDelayInBlocks: 0, + rpcPollingBlocksBackToTriggerUpdate: 0, + uniswapV3EventLoggingSampleRate: 0, + forceRpcFallbackDexs: [], + }, }; // Should not be used, except by internal test code diff --git a/src/constants.ts b/src/constants.ts index 4bdfb1679..6ddad8bde 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,6 +7,9 @@ export const PORT_TEST_SERVER = process.env.TEST_PORT; export const ETHER_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'.toLowerCase(); +// address is the same on all chains +export const PERMIT2_ADDRESS = '0x000000000022d473030f116ddee9f6b43ac78ba3'; + export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; export const CACHE_PREFIX = 'dl'; @@ -41,6 +44,7 @@ export enum Network { ARBITRUM = 42161, OPTIMISM = 10, BASE = 8453, + SEPOLIA = 11155111, GNOSIS = 100, } export const SUBGRAPH_TIMEOUT = 20 * 1000; diff --git a/src/dex/algebra/algebra.ts b/src/dex/algebra/algebra.ts index 559d31092..daa4bc17d 100644 --- a/src/dex/algebra/algebra.ts +++ b/src/dex/algebra/algebra.ts @@ -69,10 +69,6 @@ const ALGEBRA_TICK_BASE_OVERHEAD = 75_000; const ALGEBRA_POOL_SEARCH_OVERHEAD = 10_000; const ALGEBRA_QUOTE_GASLIMIT = 2_000_000; -const MAX_STALE_STATE_BLOCK_AGE = { - [Network.ZKEVM]: 150, // approximately 3min -}; - type IAlgebraEventPool = | AlgebraEventPoolV1_1 | AlgebraEventPoolV1_9 @@ -240,20 +236,6 @@ export class Algebra extends SimpleExchange implements IDex { if (pool) { if (!pool.initFailed) { - if (this.network !== Network.ZKEVM) return pool; - - if ( - pool.getStaleState() === null || - (pool.getState(blockNumber) === null && - blockNumber - pool.getStateBlockNumber() > - MAX_STALE_STATE_BLOCK_AGE[this.network]) - ) { - /* reload state, on zkEVM this would most likely timeout during request life - * but would allow to rely on staleState for couple of min for next requests - */ - await pool.initialize(blockNumber, { forceRegenerate: true }); - } - return pool; } else { // if init failed then prefer to early return pool with empty state to fallback to rpc call @@ -615,37 +597,16 @@ export class Algebra extends SimpleExchange implements IDex { let state = pool.getState(blockNumber); if (state === null) { - if (this.network === Network.ZKEVM) { - if (pool.initFailed) return null; - - if ( - blockNumber - pool.getStateBlockNumber() <= - MAX_STALE_STATE_BLOCK_AGE[this.network] - ) { - this.logger.warn( - `${_srcAddress}_${_destAddress}_${pool.name}_${ - pool.poolAddress - } state fallback to latest early enough state. Current blockNumber=${blockNumber}, stateBlockNumber=${pool.getStateBlockNumber()}`, - ); - state = pool.getStaleState(); - } else { - this.logger.warn( - `${_srcAddress}_${_destAddress}_${pool.name}_${pool.poolAddress} state is unhealthy, cannot compute price (no fallback on this chain)`, - ); - return null; // never fallback as takes more time - } - } else { - const rpcPrice = await this.getPricingFromRpc( - _srcToken, - _destToken, - amounts, - side, - pool, - transferFees, - ); + const rpcPrice = await this.getPricingFromRpc( + _srcToken, + _destToken, + amounts, + side, + pool, + transferFees, + ); - return rpcPrice; - } + return rpcPrice; } if (!state) return null; diff --git a/src/dex/augustus-approvals.ts b/src/dex/augustus-approvals.ts index 81a6ea3db..fe47d1e01 100644 --- a/src/dex/augustus-approvals.ts +++ b/src/dex/augustus-approvals.ts @@ -1,11 +1,13 @@ -import { Address, ParaSwapVersion } from '@paraswap/core'; -import { CACHE_PREFIX, ETHER_ADDRESS } from '../constants'; -import { ICache, IDexHelper } from '../dex-helper'; +import { Address } from '@paraswap/core'; +import { CACHE_PREFIX, ETHER_ADDRESS, PERMIT2_ADDRESS } from '../constants'; +import { ICache } from '../dex-helper'; import { Interface } from '@ethersproject/abi'; import ERC20ABI from '../abi/erc20.json'; +import Permit2Abi from '../abi/permit2.json'; import { uint256ToBigInt } from '../lib/decoders'; import { MultiCallParams, MultiWrapper } from '../lib/multi-wrapper'; import { ConfigHelper } from '../config'; +import { BigNumber } from 'ethers'; const DEFAULT_APPROVE_CACHE_KEY_VALUE = 'true'; @@ -14,6 +16,7 @@ type ApprovalsMapping = Record; export class AugustusApprovals { erc20Interface: Interface; + permit2Interface: Interface; private cache: ICache; @@ -32,6 +35,7 @@ export class AugustusApprovals { this.augustusAddress = config.data.augustusAddress; this.augustusV6Address = config.data.augustusV6Address; this.erc20Interface = new Interface(ERC20ABI); + this.permit2Interface = new Interface(Permit2Abi); this.cache = cache; this.cacheApprovesKey = `${CACHE_PREFIX}_${this.network}_generic_approves`; @@ -41,19 +45,22 @@ export class AugustusApprovals { spender: Address, token: Address, target: Address, + permit2 = false, ): Promise { - const approvals = await this.hasApprovals(spender, [[token, target]]); + const approvals = await this.hasApprovals(spender, [ + [token, target, permit2], + ]); return approvals[0]; } async hasApprovals( spender: Address, - tokenTargetMapping: [token: Address, target: Address][], + tokenTargetMapping: [token: Address, target: Address, permit2: boolean][], ): Promise { let approvalsMapping: Record = {}; - tokenTargetMapping.forEach(([token, target]) => { - const key = this.createCacheKey(spender, token, target); + tokenTargetMapping.forEach(([token, target, permit2]) => { + const key = this.createCacheKey(spender, token, target, permit2); // set approved 'true' for ETH approvalsMapping[key] = token.toLowerCase() === ETHER_ADDRESS; }); @@ -68,7 +75,9 @@ export class AugustusApprovals { // to keep same order and length as input return tokenTargetMapping - .map(([token, target]) => this.createCacheKey(spender, token, target)) + .map(([token, target, permit2]) => + this.createCacheKey(spender, token, target, permit2), + ) .map(key => approvalsMapping[key]); } @@ -107,17 +116,37 @@ export class AugustusApprovals { spender: Address, token: Address, target: Address, + permit2: boolean, ][], ): Promise { const allowanceCalldata: MultiCallParams[] = - spenderTokenTargetMapping.map(([spender, token, target]) => ({ - target: token, - callData: this.erc20Interface.encodeFunctionData('allowance', [ - spender, - target, - ]), - decodeFunction: uint256ToBigInt, - })); + spenderTokenTargetMapping.map(([spender, token, target, permit2]) => + permit2 + ? { + target: PERMIT2_ADDRESS, + callData: this.permit2Interface.encodeFunctionData('allowance', [ + spender, + token, + target, + ]), + decodeFunction: value => { + const [amount, expiration, nonce] = + this.permit2Interface.decodeFunctionResult( + 'allowance', + value.toString(), + ) as [BigNumber, BigNumber, BigNumber]; + return amount.toBigInt(); + }, + } + : { + target: token, + callData: this.erc20Interface.encodeFunctionData('allowance', [ + spender, + target, + ]), + decodeFunction: uint256ToBigInt, + }, + ); const allowances = await this.multiWrapper.tryAggregate( false, @@ -157,14 +186,18 @@ export class AugustusApprovals { spender: Address, token: Address, target: Address, + permit2 = false, ): string { - return `${spender}_${token}_${target}`; + return `${spender}_${token}_${target}${ + permit2 ? '_permit2' : '' + }`.toLowerCase(); } private splitCacheKey( key: string, - ): [spender: Address, token: Address, target: Address] { - return key.split('_') as [Address, Address, Address]; + ): [spender: Address, token: Address, target: Address, permit2: boolean] { + const [spender, token, target, permit2] = key.split('_'); + return [spender, token, target, permit2 === 'permit2']; } private filterKeys(tokenTargetMapping: ApprovalsMapping, approved = false) { diff --git a/src/dex/balancer-v3/balancer-v3-e2e.test.ts b/src/dex/balancer-v3/balancer-v3-e2e.test.ts new file mode 100644 index 000000000..df084f902 --- /dev/null +++ b/src/dex/balancer-v3/balancer-v3-e2e.test.ts @@ -0,0 +1,299 @@ +/* eslint-disable no-console */ +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'; + +/* + README + ====== + + This test script should add e2e tests for BalancerV3. The tests + should cover as many cases as possible. Most of the DEXes follow + the following test structure: + - DexName + - ForkName + Network + - ContractMethod + - ETH -> Token swap + - Token -> ETH swap + - Token -> Token swap + + The template already enumerates the basic structure which involves + testing simpleSwap, multiSwap, megaSwap contract methods for + ETH <> TOKEN and TOKEN <> TOKEN swaps. You should replace tokenA and + tokenB with any two highly liquid tokens on BalancerV3 for the tests + to work. If the tokens that you would like to use are not defined in + Tokens or Holders map, you can update the './tests/constants-e2e' + + Other than the standard cases that are already added by the template + it is highly recommended to add test cases which could be specific + to testing BalancerV3 (Eg. Tests based on poolType, special tokens, + etc). + + You can run this individual test script by running: + `npx jest src/dex//-e2e.test.ts` + + e2e tests use the Tenderly fork api. Please add the following to your + .env file: + TENDERLY_TOKEN=Find this under Account>Settings>Authorization. + TENDERLY_ACCOUNT_ID=Your Tenderly account name. + TENDERLY_PROJECT=Name of a Tenderly project you have created in your + dashboard. + + (This comment should be removed from the final implementation) +*/ + +function testForNetwork( + network: Network, + dexKey: string, + tokenASymbol: string, + tokenBSymbol: string, + tokenAAmount: string, + tokenBAmount: string, + nativeTokenAmount: string, + testNative: boolean, +) { + const provider = new StaticJsonRpcProvider( + generateConfig(network).privateHttpProvider, + network, + ); + const tokens = Tokens[network]; + const holders = Holders[network]; + const nativeTokenSymbol = NativeTokenSymbols[network]; + + // TODO: Add any direct swap contractMethod name if it exists + const sideToContractMethods = new Map([ + [SwapSide.SELL, [ContractMethod.swapExactAmountIn]], + [SwapSide.BUY, [ContractMethod.swapExactAmountOut]], + ]); + + describe(`${network}`, () => { + sideToContractMethods.forEach((contractMethods, side) => + describe(`${side}`, () => { + contractMethods.forEach((contractMethod: ContractMethod) => { + describe(`${contractMethod}`, () => { + if (testNative) { + it(`${nativeTokenSymbol} -> ${tokenASymbol}`, async () => { + await testE2E( + tokens[nativeTokenSymbol], + tokens[tokenASymbol], + holders[nativeTokenSymbol], + side === SwapSide.SELL ? nativeTokenAmount : tokenAAmount, + side, + dexKey, + contractMethod, + network, + provider, + ); + }); + it(`${tokenASymbol} -> ${nativeTokenSymbol}`, async () => { + await testE2E( + tokens[tokenASymbol], + tokens[nativeTokenSymbol], + holders[tokenASymbol], + side === SwapSide.SELL ? tokenAAmount : nativeTokenAmount, + side, + dexKey, + contractMethod, + network, + provider, + ); + }); + } + it(`${tokenASymbol} -> ${tokenBSymbol}`, async () => { + await testE2E( + tokens[tokenASymbol], + tokens[tokenBSymbol], + holders[tokenASymbol], + side === SwapSide.SELL ? tokenAAmount : tokenBAmount, + side, + dexKey, + contractMethod, + network, + provider, + ); + }); + it(`${tokenBSymbol} -> ${tokenASymbol}`, async () => { + await testE2E( + tokens[tokenBSymbol], + tokens[tokenASymbol], + holders[tokenBSymbol], + side === SwapSide.SELL ? tokenBAmount : tokenAAmount, + side, + dexKey, + contractMethod, + network, + provider, + ); + }); + }); + }); + }), + ); + }); +} + +// TODO - these tests dont currently run without full PS setup on Sepolia +describe('BalancerV3 E2E', () => { + const dexKey = 'BalancerV3'; + + describe('Sepolia', () => { + const network = Network.SEPOLIA; + + describe('Weighted Path', () => { + const tokenASymbol: string = 'bal'; + const tokenBSymbol: string = 'daiAave'; + + const tokenAAmount: string = '1000000000000000000'; + const tokenBAmount: string = '1000000000000000000'; + const nativeTokenAmount = '100000000000000'; + + testForNetwork( + network, + dexKey, + tokenASymbol, + tokenBSymbol, + tokenAAmount, + tokenBAmount, + nativeTokenAmount, + true, + ); + }); + + describe('Stable Path', () => { + const tokenASymbol: string = 'stataUSDT'; + const tokenBSymbol: string = 'stataUSDC'; + + const tokenAAmount: string = '10000000'; + const tokenBAmount: string = '10000000'; + const nativeTokenAmount = '0'; + + testForNetwork( + network, + dexKey, + tokenASymbol, + tokenBSymbol, + tokenAAmount, + tokenBAmount, + nativeTokenAmount, + false, + ); + }); + + describe('Boosted Path', () => { + const tokenASymbol: string = 'usdcAave'; + const tokenBSymbol: string = 'daiAave'; + + const tokenAAmount: string = '10000000'; + const tokenBAmount: string = '1000000000000000000'; + const nativeTokenAmount = '0'; + + testForNetwork( + network, + dexKey, + tokenASymbol, + tokenBSymbol, + tokenAAmount, + tokenBAmount, + nativeTokenAmount, + false, + ); + }); + }); + + describe('Gnosis', () => { + const network = Network.GNOSIS; + + describe('Stable Path', () => { + const tokenASymbol: string = 'WXDAI'; + const tokenBSymbol: string = 'COW'; + + const tokenAAmount: string = '100000000000000000'; + const tokenBAmount: string = '100000000000000000'; + const nativeTokenAmount = '100000000000000'; + + testForNetwork( + network, + dexKey, + tokenASymbol, + tokenBSymbol, + tokenAAmount, + tokenBAmount, + nativeTokenAmount, + false, + ); + }); + + describe('Weighed Path', () => { + const tokenASymbol: string = 'sDAI'; + const tokenBSymbol: string = 'XDAI'; + + const tokenAAmount: string = '100000000000000000'; + const tokenBAmount: string = '100000000000000000'; + const nativeTokenAmount = '100000000000000000'; + + testForNetwork( + network, + dexKey, + tokenASymbol, + tokenBSymbol, + tokenAAmount, + tokenBAmount, + nativeTokenAmount, + false, + ); + }); + + describe('Boosted Path', () => { + const tokenASymbol: string = 'waGnoWETH'; + const tokenBSymbol: string = 'waGnowstETH'; + + const tokenAAmount: string = '1000000000000000'; + const tokenBAmount: string = '1000000000000000'; + const nativeTokenAmount = '0'; + + testForNetwork( + network, + dexKey, + tokenASymbol, + tokenBSymbol, + tokenAAmount, + tokenBAmount, + nativeTokenAmount, + false, + ); + }); + }); + + describe('Mainnet', () => { + const network = Network.MAINNET; + + describe('Stable Path', () => { + const tokenASymbol: string = 'wUSDL'; + const tokenBSymbol: string = 'USDC'; + + const tokenAAmount: string = '1000000000000000000'; + const tokenBAmount: string = '10000000'; + const nativeTokenAmount = '0'; + + testForNetwork( + network, + dexKey, + tokenASymbol, + tokenBSymbol, + tokenAAmount, + tokenBAmount, + nativeTokenAmount, + false, + ); + }); + }); +}); diff --git a/src/dex/balancer-v3/balancer-v3-events.test.ts b/src/dex/balancer-v3/balancer-v3-events.test.ts new file mode 100644 index 000000000..f6ccdbab5 --- /dev/null +++ b/src/dex/balancer-v3/balancer-v3-events.test.ts @@ -0,0 +1,223 @@ +/* eslint-disable no-console */ +import dotenv from 'dotenv'; +dotenv.config(); + +import { BalancerV3EventPool } from './balancer-v3-pool'; +import { Network } from '../../constants'; +import { Address } from '../../types'; +import { DummyDexHelper } from '../../dex-helper/index'; +import { testEventSubscriber } from '../../../tests/utils-events'; +import { PoolStateMap, StableMutableState } from './types'; +import { BalancerV3Config } from './config'; +import _ from 'lodash'; + +/* + README + ====== + + This test script adds unit tests for BalancerV3 event based + system. This is done by fetching the state on-chain before the + event block, manually pushing the block logs to the event-subscriber, + comparing the local state with on-chain state. + + Most of the logic for testing is abstracted by `testEventSubscriber`. + You need to do two things to make the tests work: + + 1. Fetch the block numbers where certain events were released. You + can modify the `./scripts/fetch-event-blocknumber.ts` to get the + block numbers for different events. Make sure to get sufficient + number of blockNumbers to cover all possible cases for the event + mutations. + + 2. Complete the implementation for fetchPoolState function. The + function should fetch the on-chain state of the event subscriber + using just the blocknumber. + + The template tests only include the test for a single event + subscriber. There can be cases where multiple event subscribers + exist for a single DEX. In such cases additional tests should be + added. + + You can run this individual test script by running: + `npx jest src/dex/balancer-v3/balancer-v3-events.test.ts` + + (This comment should be removed from the final implementation) +*/ + +jest.setTimeout(50 * 1000); + +async function fetchPoolState( + balancerV3Pools: BalancerV3EventPool, + blockNumber: number, + poolAddress: string, +): Promise { + const pools = await balancerV3Pools.generateState(blockNumber); + // Filter to pool of interest + return Object.entries(_.cloneDeep(pools) as PoolStateMap) + .filter(([address]) => { + return address.toLowerCase() === poolAddress.toLowerCase(); + }) + .reduce((acc, [address, pool]) => { + acc[address] = pool; + return acc; + }, {} as PoolStateMap); +} + +function stateCompare(state: PoolStateMap, expectedState: PoolStateMap) { + if (state['0xd63db0b88dca565633fb8d70a70b9b8093d34a7e']) { + if ( + ( + expectedState[ + '0xd63db0b88dca565633fb8d70a70b9b8093d34a7e' + ] as StableMutableState + ).ampIsUpdating + ) { + compareAmpUpdating(state, expectedState); + } else compareAmpStopped(state, expectedState); + } else expect(state).toEqual(expectedState); +} + +function compareAmpUpdating(state: PoolStateMap, expectedState: PoolStateMap) { + // tokenRates and balancesLiveScaled18 are gradually increasing between blocks due to tokenRate (and can't be tracked via event) so will be ignored + const compare = { + ...state, + ['0xd63db0b88dca565633fb8d70a70b9b8093d34a7e']: _.omit( + state['0xd63db0b88dca565633fb8d70a70b9b8093d34a7e'], + ['balancesLiveScaled18', 'tokenRates'], + ), + }; + const expectedCompare = { + ...expectedState, + ['0xd63db0b88dca565633fb8d70a70b9b8093d34a7e']: _.omit( + expectedState['0xd63db0b88dca565633fb8d70a70b9b8093d34a7e'], + ['balancesLiveScaled18', 'tokenRates'], + ), + }; + expect(compare).toEqual(expectedCompare); +} + +function compareAmpStopped(state: PoolStateMap, expectedState: PoolStateMap) { + if (state['0xd63db0b88dca565633fb8d70a70b9b8093d34a7e']) { + // tokenRates and balancesLiveScaled18 are gradually increasing between blocks due to tokenRate (and can't be tracked via event) so will be ignored + // In Contract ampStartTime & ampStopTime are updated to timestamp event is called. + // There doesn't appear to be a way to easily get timestamp non-async so we default to 0n which should have no effect. + const compare = { + ...state, + ['0xd63db0b88dca565633fb8d70a70b9b8093d34a7e']: _.omit( + state['0xd63db0b88dca565633fb8d70a70b9b8093d34a7e'], + ['balancesLiveScaled18', 'tokenRates'], + ), + }; + const expectedCompare = { + ...expectedState, + ['0xd63db0b88dca565633fb8d70a70b9b8093d34a7e']: { + ..._.omit(expectedState['0xd63db0b88dca565633fb8d70a70b9b8093d34a7e'], [ + 'balancesLiveScaled18', + 'tokenRates', + ]), + ampStartTime: 0n, + ampStopTime: 0n, + }, + }; + expect(compare).toEqual(expectedCompare); + } else expect(state).toEqual(expectedState); +} + +// eventName -> blockNumbers +type EventMappings = Record; +type EventData = { blockNumbers: number[]; poolAddress: string[] }; + +describe('BalancerV3 EventPool', function () { + const dexKey = 'BalancerV3'; + const network = Network.SEPOLIA; + const dexHelper = new DummyDexHelper(network); + const logger = dexHelper.getLogger(dexKey); + let balancerV3Pool: BalancerV3EventPool; + + // vault -> EventMappings + // TODO once we have a new test deployment add tests for: AggregateSwapFeePercentageChanged, SwapFeePercentageChanged, PoolPausedStateChanged + const eventsToTest: Record = { + [BalancerV3Config.BalancerV3[network].vaultAddress]: { + // https://eth-sepolia.blockscout.com/tx/0xc417d38ad6e21250c9ddded37680b40f0991cfd3f8ae2d8b5800507a58d48c44 + LiquidityAdded: { + blockNumbers: [7170937], + poolAddress: ['0x2ff3b96e0057a1f25f1d62ab800554ccdb268ab8'], + }, + // https://sepolia.etherscan.io/tx/0xc1596e26d51104b9236a0debc3e1946b30b82f92b8331639ad6f6aea2ff2decc + LiquidityRemoved: { + blockNumbers: [7170957], + poolAddress: ['0x2ff3b96e0057a1f25f1d62ab800554ccdb268ab8'], + }, + // https://sepolia.etherscan.io/tx/0x78d18503c2dd4458c94ec916b10c088bb4e8b90059676d00dbcec83f763d8c0e + Swap: { + blockNumbers: [7175721], + poolAddress: ['0x2ff3b96e0057a1f25f1d62ab800554ccdb268ab8'], + }, + // 7170034, AmpUpdateStarted, https://eth-sepolia.blockscout.com/tx/0xfbe2e53d9cede1dc900100a1d6e809a89d909746ac7b8cc011e93227af8dda8b?tab=logs + // 7170069, AmpUpdateStopped, https://eth-sepolia.blockscout.com/tx/0x2ee2e1d5980013fdf1cf9c3789e0321fb598c2412db8ee8057fcfaffd1c792ab?tab=logs + VaultAuxiliary: { + blockNumbers: [7170034, 7170069], + poolAddress: [ + '0xD63dB0B88dca565633fB8d70a70b9b8093d34A7E', + '0xD63dB0B88dca565633fB8d70a70b9b8093d34A7E', + ], + }, + // https://sepolia.etherscan.io/tx/0x71f9879485f4e4cf97aa42381988ffe277f05a872d6b507cfa007cec1239a3f8#eventlog + SwapFeePercentageChanged: { + blockNumbers: [7206571], + poolAddress: ['0xe69b70a86A4e1fD33dA95693A1aE12Be1c26C8ea'], + }, + // https://sepolia.etherscan.io/tx/0xaf232ca2df59ba5fad38d74e9b54ec2ae1ad2e6abca8348f107cf3fd94c787c7#eventlog + // Should remove pool from state as its paused and no longer supports swaps + PoolPausedStateChanged: { + blockNumbers: [7206586], + poolAddress: ['0xe69b70a86A4e1fD33dA95693A1aE12Be1c26C8ea'], + }, + // https://sepolia.etherscan.io/tx/0xa23dc10bd0fbed7ffffe867766d9b0d7670ca4a0bc352669f547bc7775644349#eventlog + AggregateSwapFeePercentageChanged: { + blockNumbers: [7206701], + poolAddress: ['0xe69b70a86A4e1fD33dA95693A1aE12Be1c26C8ea'], + }, + }, + }; + + beforeEach(async () => { + balancerV3Pool = new BalancerV3EventPool( + dexKey, + network, + dexHelper, + logger, + ); + }); + + Object.entries(eventsToTest).forEach( + ([vaultAddress, events]: [string, EventMappings]) => { + describe(`Events for Vault: ${vaultAddress}`, () => { + Object.entries(events).forEach( + ([eventName, eventData]: [string, EventData]) => { + describe(`${eventName}`, () => { + eventData.blockNumbers.forEach((blockNumber: number, i) => { + it(`Pool: ${eventData.poolAddress[i]} State after ${blockNumber}`, async function () { + await testEventSubscriber( + balancerV3Pool, + balancerV3Pool.addressesSubscribed, + (_blockNumber: number) => + fetchPoolState( + balancerV3Pool, + _blockNumber, + eventData.poolAddress[i], + ), + blockNumber, + `${dexKey}_${vaultAddress}`, + dexHelper.provider, + stateCompare, + ); + }); + }); + }); + }, + ); + }); + }, + ); +}); diff --git a/src/dex/balancer-v3/balancer-v3-integration.test.ts b/src/dex/balancer-v3/balancer-v3-integration.test.ts new file mode 100644 index 000000000..0a2b71014 --- /dev/null +++ b/src/dex/balancer-v3/balancer-v3-integration.test.ts @@ -0,0 +1,1107 @@ +/* eslint-disable no-console */ +import dotenv from 'dotenv'; +dotenv.config(); + +import { Interface, Result } from '@ethersproject/abi'; +import { DummyDexHelper } from '../../dex-helper/index'; +import { Network, NULL_ADDRESS, SwapSide } from '../../constants'; +import { BI_POWS } from '../../bigint-constants'; +import { BalancerV3 } from './balancer-v3'; +import { + checkPoolPrices, + checkConstantPoolPrices, + checkPoolsLiquidity, +} from '../../../tests/utils'; +import { Tokens } from '../../../tests/constants-e2e'; +import { BalancerV3Config } from './config'; +import { BalancerV3Data, Step } from './types'; +import { Address, ExchangePrices, PoolPrices } from '../../types'; +import balancerBatchRouterAbi from '../../abi/balancer-v3/batch-router.json'; +import balancerRouterAbi from '../../abi/balancer-v3/router.json'; + +function getQuerySwapSingleTokenCalldata( + routerAddress: Address, + routerInterface: Interface, + amounts: bigint[], + step: Step, + side: SwapSide, +) { + return amounts + .filter(amount => amount !== 0n) + .map(amount => { + return { + target: routerAddress, + callData: routerInterface.encodeFunctionData( + side === SwapSide.SELL + ? `querySwapSingleTokenExactIn` + : `querySwapSingleTokenExactOut`, + [ + step.pool, + step.swapInput.tokenIn, + step.swapInput.tokenOut, + amount, + NULL_ADDRESS, + '0x', + ], + ), + }; + }); +} + +function getQuerySwapMultiTokenCalldata( + routerAddress: Address, + routerInterface: Interface, + amounts: bigint[], + steps: Step[], + side: SwapSide, +) { + const tokenIn = steps[0].swapInput.tokenIn; + const stepsNew = steps.map(s => ({ + pool: s.pool, + tokenOut: s.swapInput.tokenOut, + isBuffer: s.isBuffer, + })); + return amounts + .filter(amount => amount !== 0n) + .map(amount => { + let args: any[] = []; + if (side === SwapSide.SELL) + args = [ + { + tokenIn, + steps: stepsNew, + exactAmountIn: amount, + minAmountOut: 0n, + }, + ]; + else + args = [ + { + tokenIn, + steps: stepsNew, + exactAmountOut: amount, + maxAmountIn: 0n, + }, + ]; + return { + target: routerAddress, + callData: routerInterface.encodeFunctionData( + side === SwapSide.SELL ? `querySwapExactIn` : `querySwapExactOut`, + [args, NULL_ADDRESS, '0x'], + ), + }; + }); +} + +async function querySinglePathPrices( + network: number, + side: SwapSide, + balancerV3: BalancerV3, + blockNumber: number, + price: PoolPrices, + amounts: bigint[], +) { + const balancerRouter = new Interface(balancerRouterAbi); + const readerCallData = getQuerySwapSingleTokenCalldata( + BalancerV3Config.BalancerV3[network].balancerRouterAddress, + balancerRouter, + amounts, + price.data.steps[0], + side, + ); + + const expectedPrices = [0n]; + for (const call of readerCallData) { + try { + const result = await balancerV3.dexHelper.provider.call( + { + to: call.target, + data: call.callData, + }, + blockNumber, + ); + const parsed = balancerRouter.decodeFunctionResult( + side === SwapSide.SELL + ? `querySwapSingleTokenExactIn` + : `querySwapSingleTokenExactOut`, + result, + ); + expectedPrices.push(BigInt(parsed[0]._hex)); + } catch (error) { + console.log('Error in querySinglePathPrices', error); + expectedPrices.push(0n); + } + } + return expectedPrices; +} + +async function queryMultiPathPrices( + network: number, + side: SwapSide, + balancerV3: BalancerV3, + blockNumber: number, + price: PoolPrices, + amounts: bigint[], +) { + const balancerBatchRouter = new Interface(balancerBatchRouterAbi); + const readerCallData = getQuerySwapMultiTokenCalldata( + BalancerV3Config.BalancerV3[network].balancerBatchRouterAddress, + balancerBatchRouter, + amounts, + price.data.steps, + side, + ); + + const expectedPrices = [0n]; + for (const call of readerCallData) { + try { + const result = await balancerV3.dexHelper.provider.call( + { + to: call.target, + data: call.callData, + }, + blockNumber, + ); + const parsed = balancerBatchRouter.decodeFunctionResult( + side === SwapSide.SELL ? `querySwapExactIn` : `querySwapExactOut`, + result, + ); + expectedPrices.push(BigInt(parsed[2][0]._hex)); + } catch (error) { + console.log('Error in queryMultiPathPrices', error); + expectedPrices.push(0n); + } + } + return expectedPrices; +} + +// Note - this is currently needed because queries won't work with multicall but should be updated in future +async function checkOnChainPricingNonMulti( + network: number, + side: SwapSide, + balancerV3: BalancerV3, + blockNumber: number, + prices: ExchangePrices, + amounts: bigint[], +) { + // test match for each returned price + for (const price of prices) { + let expectedPrices: bigint[] = []; + if (price.data.steps.length === 1 && !price.data.steps[0].isBuffer) + expectedPrices = await querySinglePathPrices( + network, + side, + balancerV3, + blockNumber, + price, + amounts, + ); + else + expectedPrices = await queryMultiPathPrices( + network, + side, + balancerV3, + blockNumber, + price, + amounts, + ); + expect(price.prices).toEqual(expectedPrices); + } +} + +async function testPricingOnNetwork( + balancerV3: BalancerV3, + network: Network, + dexKey: string, + blockNumber: number, + srcTokenSymbol: string, + destTokenSymbol: string, + side: SwapSide, + amounts: bigint[], +) { + const networkTokens = Tokens[network]; + + const pools = await balancerV3.getPoolIdentifiers( + networkTokens[srcTokenSymbol], + networkTokens[destTokenSymbol], + side, + blockNumber, + ); + console.log( + `${srcTokenSymbol} <> ${destTokenSymbol} Pool Identifiers: `, + pools, + ); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await balancerV3.getPricesVolume( + networkTokens[srcTokenSymbol], + networkTokens[destTokenSymbol], + amounts, + side, + blockNumber, + pools, + ); + console.log( + `${srcTokenSymbol} <> ${destTokenSymbol} Pool Prices: `, + poolPrices, + ); + + expect(poolPrices).not.toBeNull(); + if (balancerV3.hasConstantPriceLargeAmounts) { + checkConstantPoolPrices(poolPrices!, amounts, dexKey); + } else { + checkPoolPrices(poolPrices!, amounts, side, dexKey, false); + } + + // Check if onchain pricing equals to calculated ones + await checkOnChainPricingNonMulti( + network, + side, + balancerV3, + blockNumber, + poolPrices!, + amounts, + ); +} + +describe('BalancerV3', function () { + const dexKey = 'BalancerV3'; + let blockNumber: number; + let balancerV3: BalancerV3; + + describe('Sepolia', () => { + const network = Network.SEPOLIA; + + describe('Weighted Pool', () => { + const dexHelper = new DummyDexHelper(network); + + const tokens = Tokens[network]; + const srcTokenSymbol = 'bal'; + const destTokenSymbol = 'daiAave'; + + const amountsForSell = [ + 0n, + 1n * BI_POWS[tokens[srcTokenSymbol].decimals], + 2n * BI_POWS[tokens[srcTokenSymbol].decimals], + 3n * BI_POWS[tokens[srcTokenSymbol].decimals], + 4n * BI_POWS[tokens[srcTokenSymbol].decimals], + 5n * BI_POWS[tokens[srcTokenSymbol].decimals], + 6n * BI_POWS[tokens[srcTokenSymbol].decimals], + 7n * BI_POWS[tokens[srcTokenSymbol].decimals], + 8n * BI_POWS[tokens[srcTokenSymbol].decimals], + 9n * BI_POWS[tokens[srcTokenSymbol].decimals], + 10n * BI_POWS[tokens[srcTokenSymbol].decimals], + ]; + + const amountsForBuy = [ + 0n, + 1n * BI_POWS[tokens[destTokenSymbol].decimals], + 2n * BI_POWS[tokens[destTokenSymbol].decimals], + 3n * BI_POWS[tokens[destTokenSymbol].decimals], + 4n * BI_POWS[tokens[destTokenSymbol].decimals], + 5n * BI_POWS[tokens[destTokenSymbol].decimals], + 6n * BI_POWS[tokens[destTokenSymbol].decimals], + 7n * BI_POWS[tokens[destTokenSymbol].decimals], + 8n * BI_POWS[tokens[destTokenSymbol].decimals], + 9n * BI_POWS[tokens[destTokenSymbol].decimals], + 10n * BI_POWS[tokens[destTokenSymbol].decimals], + ]; + + beforeAll(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + balancerV3 = new BalancerV3(network, dexKey, dexHelper); + if (balancerV3.initializePricing) { + await balancerV3.initializePricing(blockNumber); + } + }); + + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.SELL, + amountsForSell, + ); + }); + + it('getPoolIdentifiers and getPricesVolume BUY', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.BUY, + amountsForBuy, + ); + }); + + it('getTopPoolsForToken', async function () { + // We have to check without calling initializePricing, because + // pool-tracker is not calling that function + const newBalancerV3 = new BalancerV3(network, dexKey, dexHelper); + if (newBalancerV3.updatePoolState) { + await newBalancerV3.updatePoolState(); + } + const poolLiquidity = await newBalancerV3.getTopPoolsForToken( + tokens[srcTokenSymbol].address, + 10, + ); + console.log(`${srcTokenSymbol} Top Pools:`, poolLiquidity); + + if (!newBalancerV3.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity( + poolLiquidity, + Tokens[network][srcTokenSymbol].address, + dexKey, + ); + } + }); + }); + + describe('Stable Pool', () => { + const dexHelper = new DummyDexHelper(network); + + const tokens = Tokens[network]; + const srcTokenSymbol = 'stataUSDC'; + const destTokenSymbol = 'stataUSDT'; + + const amountsForSell = [ + 0n, + 1n * BI_POWS[tokens[srcTokenSymbol].decimals], + 2n * BI_POWS[tokens[srcTokenSymbol].decimals], + 3n * BI_POWS[tokens[srcTokenSymbol].decimals], + 4n * BI_POWS[tokens[srcTokenSymbol].decimals], + 5n * BI_POWS[tokens[srcTokenSymbol].decimals], + 6n * BI_POWS[tokens[srcTokenSymbol].decimals], + 7n * BI_POWS[tokens[srcTokenSymbol].decimals], + 8n * BI_POWS[tokens[srcTokenSymbol].decimals], + 9n * BI_POWS[tokens[srcTokenSymbol].decimals], + 10n * BI_POWS[tokens[srcTokenSymbol].decimals], + ]; + + const amountsForBuy = [ + 0n, + 1n * BI_POWS[tokens[destTokenSymbol].decimals], + 2n * BI_POWS[tokens[destTokenSymbol].decimals], + 3n * BI_POWS[tokens[destTokenSymbol].decimals], + 4n * BI_POWS[tokens[destTokenSymbol].decimals], + 5n * BI_POWS[tokens[destTokenSymbol].decimals], + 6n * BI_POWS[tokens[destTokenSymbol].decimals], + 7n * BI_POWS[tokens[destTokenSymbol].decimals], + 8n * BI_POWS[tokens[destTokenSymbol].decimals], + 9n * BI_POWS[tokens[destTokenSymbol].decimals], + 10n * BI_POWS[tokens[destTokenSymbol].decimals], + ]; + + beforeAll(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + balancerV3 = new BalancerV3(network, dexKey, dexHelper); + if (balancerV3.initializePricing) { + await balancerV3.initializePricing(blockNumber); + } + }); + + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.SELL, + amountsForSell, + ); + }); + + it('getPoolIdentifiers and getPricesVolume BUY', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.BUY, + amountsForBuy, + ); + }); + + it('getTopPoolsForToken', async function () { + // We have to check without calling initializePricing, because + // pool-tracker is not calling that function + const newBalancerV3 = new BalancerV3(network, dexKey, dexHelper); + if (newBalancerV3.updatePoolState) { + await newBalancerV3.updatePoolState(); + } + const poolLiquidity = await newBalancerV3.getTopPoolsForToken( + tokens[srcTokenSymbol].address, + 10, + ); + console.log(`${srcTokenSymbol} Top Pools:`, poolLiquidity); + + if (!newBalancerV3.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity( + poolLiquidity, + Tokens[network][srcTokenSymbol].address, + dexKey, + ); + } + }); + }); + + describe('Boosted Path', () => { + const dexHelper = new DummyDexHelper(network); + + const tokens = Tokens[network]; + const srcTokenSymbol = 'usdcAave'; + const destTokenSymbol = 'usdtAave'; + + const amountsForSell = [ + 0n, + 1n * BI_POWS[tokens[srcTokenSymbol].decimals], + 2n * BI_POWS[tokens[srcTokenSymbol].decimals], + 3n * BI_POWS[tokens[srcTokenSymbol].decimals], + 4n * BI_POWS[tokens[srcTokenSymbol].decimals], + 5n * BI_POWS[tokens[srcTokenSymbol].decimals], + 6n * BI_POWS[tokens[srcTokenSymbol].decimals], + 7n * BI_POWS[tokens[srcTokenSymbol].decimals], + 8n * BI_POWS[tokens[srcTokenSymbol].decimals], + 9n * BI_POWS[tokens[srcTokenSymbol].decimals], + 10n * BI_POWS[tokens[srcTokenSymbol].decimals], + ]; + + const amountsForBuy = [ + 0n, + 1n * BI_POWS[tokens[destTokenSymbol].decimals], + 2n * BI_POWS[tokens[destTokenSymbol].decimals], + 3n * BI_POWS[tokens[destTokenSymbol].decimals], + 4n * BI_POWS[tokens[destTokenSymbol].decimals], + 5n * BI_POWS[tokens[destTokenSymbol].decimals], + 6n * BI_POWS[tokens[destTokenSymbol].decimals], + 7n * BI_POWS[tokens[destTokenSymbol].decimals], + 8n * BI_POWS[tokens[destTokenSymbol].decimals], + 9n * BI_POWS[tokens[destTokenSymbol].decimals], + 10n * BI_POWS[tokens[destTokenSymbol].decimals], + ]; + + beforeAll(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + balancerV3 = new BalancerV3(network, dexKey, dexHelper); + if (balancerV3.initializePricing) { + await balancerV3.initializePricing(blockNumber); + } + }); + + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.SELL, + amountsForSell, + ); + }); + + // TODO 1 WEI rounding issue in maths - investigating + it('getPoolIdentifiers and getPricesVolume BUY', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.BUY, + amountsForBuy, + ); + }); + + it('getTopPoolsForToken', async function () { + // We have to check without calling initializePricing, because + // pool-tracker is not calling that function + const newBalancerV3 = new BalancerV3(network, dexKey, dexHelper); + if (newBalancerV3.updatePoolState) { + await newBalancerV3.updatePoolState(); + } + const poolLiquidity = await newBalancerV3.getTopPoolsForToken( + tokens[srcTokenSymbol].address, + 10, + ); + console.log(`${srcTokenSymbol} Top Pools:`, poolLiquidity); + + if (!newBalancerV3.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity( + poolLiquidity, + Tokens[network][srcTokenSymbol].address, + dexKey, + ); + } + }); + }); + }); + + describe('Gnosis', () => { + const network = Network.GNOSIS; + + describe('Weighted Pool', () => { + const dexHelper = new DummyDexHelper(network); + + const tokens = Tokens[network]; + const srcTokenSymbol = 'USDCe'; + const destTokenSymbol = 'sDAI'; + + const amountsForSell = [ + 0n, + 1n * BI_POWS[tokens[srcTokenSymbol].decimals], + 2n * BI_POWS[tokens[srcTokenSymbol].decimals], + 3n * BI_POWS[tokens[srcTokenSymbol].decimals], + 4n * BI_POWS[tokens[srcTokenSymbol].decimals], + 5n * BI_POWS[tokens[srcTokenSymbol].decimals], + 6n * BI_POWS[tokens[srcTokenSymbol].decimals], + 7n * BI_POWS[tokens[srcTokenSymbol].decimals], + 8n * BI_POWS[tokens[srcTokenSymbol].decimals], + 9n * BI_POWS[tokens[srcTokenSymbol].decimals], + 10n * BI_POWS[tokens[srcTokenSymbol].decimals], + ]; + + const amountsForBuy = [ + 0n, + 1n * BI_POWS[tokens[destTokenSymbol].decimals], + 2n * BI_POWS[tokens[destTokenSymbol].decimals], + 3n * BI_POWS[tokens[destTokenSymbol].decimals], + 4n * BI_POWS[tokens[destTokenSymbol].decimals], + 5n * BI_POWS[tokens[destTokenSymbol].decimals], + 6n * BI_POWS[tokens[destTokenSymbol].decimals], + 7n * BI_POWS[tokens[destTokenSymbol].decimals], + 8n * BI_POWS[tokens[destTokenSymbol].decimals], + 9n * BI_POWS[tokens[destTokenSymbol].decimals], + 10n * BI_POWS[tokens[destTokenSymbol].decimals], + ]; + + beforeAll(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + balancerV3 = new BalancerV3(network, dexKey, dexHelper); + if (balancerV3.initializePricing) { + await balancerV3.initializePricing(blockNumber); + } + }); + + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.SELL, + amountsForSell, + ); + }); + + it('getPoolIdentifiers and getPricesVolume BUY', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.BUY, + amountsForBuy, + ); + }); + + it('getTopPoolsForToken', async function () { + // We have to check without calling initializePricing, because + // pool-tracker is not calling that function + const newBalancerV3 = new BalancerV3(network, dexKey, dexHelper); + if (newBalancerV3.updatePoolState) { + await newBalancerV3.updatePoolState(); + } + const poolLiquidity = await newBalancerV3.getTopPoolsForToken( + tokens[srcTokenSymbol].address, + 10, + ); + console.log(`${srcTokenSymbol} Top Pools:`, poolLiquidity); + + if (!newBalancerV3.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity( + poolLiquidity, + Tokens[network][srcTokenSymbol].address, + dexKey, + ); + } + }); + }); + + describe('Stable Pool', () => { + const dexHelper = new DummyDexHelper(network); + + const tokens = Tokens[network]; + const srcTokenSymbol = 'WXDAI'; + const destTokenSymbol = 'COW'; + + const amountsForSell = [ + 0n, + 1n * BI_POWS[tokens[srcTokenSymbol].decimals], + 2n * BI_POWS[tokens[srcTokenSymbol].decimals], + 3n * BI_POWS[tokens[srcTokenSymbol].decimals], + 4n * BI_POWS[tokens[srcTokenSymbol].decimals], + 5n * BI_POWS[tokens[srcTokenSymbol].decimals], + 6n * BI_POWS[tokens[srcTokenSymbol].decimals], + 7n * BI_POWS[tokens[srcTokenSymbol].decimals], + 8n * BI_POWS[tokens[srcTokenSymbol].decimals], + 9n * BI_POWS[tokens[srcTokenSymbol].decimals], + 10n * BI_POWS[tokens[srcTokenSymbol].decimals], + ]; + + const amountsForBuy = [ + 0n, + 1n * BI_POWS[tokens[destTokenSymbol].decimals], + 2n * BI_POWS[tokens[destTokenSymbol].decimals], + 3n * BI_POWS[tokens[destTokenSymbol].decimals], + 4n * BI_POWS[tokens[destTokenSymbol].decimals], + 5n * BI_POWS[tokens[destTokenSymbol].decimals], + 6n * BI_POWS[tokens[destTokenSymbol].decimals], + 7n * BI_POWS[tokens[destTokenSymbol].decimals], + 8n * BI_POWS[tokens[destTokenSymbol].decimals], + 9n * BI_POWS[tokens[destTokenSymbol].decimals], + 10n * BI_POWS[tokens[destTokenSymbol].decimals], + ]; + + beforeAll(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + balancerV3 = new BalancerV3(network, dexKey, dexHelper); + if (balancerV3.initializePricing) { + await balancerV3.initializePricing(blockNumber); + } + }); + + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.SELL, + amountsForSell, + ); + }); + + it('getPoolIdentifiers and getPricesVolume BUY', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.BUY, + amountsForBuy, + ); + }); + + it('getTopPoolsForToken', async function () { + // We have to check without calling initializePricing, because + // pool-tracker is not calling that function + const newBalancerV3 = new BalancerV3(network, dexKey, dexHelper); + if (newBalancerV3.updatePoolState) { + await newBalancerV3.updatePoolState(); + } + const poolLiquidity = await newBalancerV3.getTopPoolsForToken( + tokens[srcTokenSymbol].address, + 10, + ); + console.log(`${srcTokenSymbol} Top Pools:`, poolLiquidity); + + if (!newBalancerV3.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity( + poolLiquidity, + Tokens[network][srcTokenSymbol].address, + dexKey, + ); + } + }); + }); + + describe('Boosted Path', () => { + const dexHelper = new DummyDexHelper(network); + + const tokens = Tokens[network]; + const srcTokenSymbol = 'waGnoWETH'; + const destTokenSymbol = 'waGnowstETH'; + + const amountsForSell = [ + 0n, + (1n * BI_POWS[tokens[srcTokenSymbol].decimals]) / 1000n, + (2n * BI_POWS[tokens[srcTokenSymbol].decimals]) / 1000n, + (3n * BI_POWS[tokens[srcTokenSymbol].decimals]) / 1000n, + (4n * BI_POWS[tokens[srcTokenSymbol].decimals]) / 1000n, + (5n * BI_POWS[tokens[srcTokenSymbol].decimals]) / 1000n, + (6n * BI_POWS[tokens[srcTokenSymbol].decimals]) / 1000n, + (7n * BI_POWS[tokens[srcTokenSymbol].decimals]) / 1000n, + (8n * BI_POWS[tokens[srcTokenSymbol].decimals]) / 1000n, + (9n * BI_POWS[tokens[srcTokenSymbol].decimals]) / 1000n, + (10n * BI_POWS[tokens[srcTokenSymbol].decimals]) / 1000n, + ]; + + const amountsForBuy = [ + 0n, + (1n * BI_POWS[tokens[destTokenSymbol].decimals]) / 1000n, + (2n * BI_POWS[tokens[destTokenSymbol].decimals]) / 1000n, + (3n * BI_POWS[tokens[destTokenSymbol].decimals]) / 1000n, + (4n * BI_POWS[tokens[destTokenSymbol].decimals]) / 1000n, + (5n * BI_POWS[tokens[destTokenSymbol].decimals]) / 1000n, + (6n * BI_POWS[tokens[destTokenSymbol].decimals]) / 1000n, + (7n * BI_POWS[tokens[destTokenSymbol].decimals]) / 1000n, + (8n * BI_POWS[tokens[destTokenSymbol].decimals]) / 1000n, + (9n * BI_POWS[tokens[destTokenSymbol].decimals]) / 1000n, + (10n * BI_POWS[tokens[destTokenSymbol].decimals]) / 1000n, + ]; + + beforeAll(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + balancerV3 = new BalancerV3(network, dexKey, dexHelper); + if (balancerV3.initializePricing) { + await balancerV3.initializePricing(blockNumber); + } + }); + + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.SELL, + amountsForSell, + ); + }); + + // TODO 1 WEI rounding issue in maths - investigating + it('getPoolIdentifiers and getPricesVolume BUY', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.BUY, + amountsForBuy, + ); + }); + + it('getTopPoolsForToken', async function () { + // We have to check without calling initializePricing, because + // pool-tracker is not calling that function + const newBalancerV3 = new BalancerV3(network, dexKey, dexHelper); + if (newBalancerV3.updatePoolState) { + await newBalancerV3.updatePoolState(); + } + const poolLiquidity = await newBalancerV3.getTopPoolsForToken( + tokens[srcTokenSymbol].address, + 10, + ); + + console.log(`${srcTokenSymbol} Top Pools:`, poolLiquidity); + + if (!newBalancerV3.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity( + poolLiquidity, + Tokens[network][srcTokenSymbol].address, + dexKey, + ); + } + }); + }); + + describe('Buffer, Nested Rate', () => { + /* + The Gnosis pool, 0x272d6be442e30d7c87390edeb9b96f1e84cecd8d uses a rate provider that is nested. + So unwrap rate does not equal rate between aave wsteth and eth. + This particular case the rate provider accounts for growth of wsteth in terms of weth and the additional aave yield. + This highlighted that rateProvider can not be used for buffer wrap/unwrap which instead should use erc4626 rate. + */ + const dexHelper = new DummyDexHelper(network); + + const tokens = Tokens[network]; + const srcTokenSymbol = 'wstETH'; + const destTokenSymbol = 'waGnowstETH'; + + const amountsForSell = [ + 0n, + 1n * BI_POWS[tokens[srcTokenSymbol].decimals], + 100n * BI_POWS[tokens[srcTokenSymbol].decimals], + ]; + + const amountsForBuy = [ + 0n, + 1n * BI_POWS[tokens[destTokenSymbol].decimals], + 100n * BI_POWS[tokens[destTokenSymbol].decimals], + ]; + + beforeAll(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + balancerV3 = new BalancerV3(network, dexKey, dexHelper); + if (balancerV3.initializePricing) { + await balancerV3.initializePricing(blockNumber); + } + }); + + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.SELL, + amountsForSell, + ); + }); + + it('getPoolIdentifiers and getPricesVolume BUY', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.BUY, + amountsForBuy, + ); + }); + }); + }); + + describe('Mainnet', () => { + const network = Network.MAINNET; + describe('Token/Underlying With Different Decimals', () => { + /* + Mainnet Pool, 0x5dd88b3aa3143173eb26552923922bdf33f50949 has an ERC4626 token with 18 decimals that uses a 6 decimal underlying. + Note for maths: Instead of manually adding support for each ERC4626 implementation (e.g. stata with Ray maths) we always use an + 18 decimal scaled rate and do 18 decimal maths to convert. We may end up loosing 100% accuracy but thats deemed acceptable. + */ + describe.only('Buffer wrap 6decimal>18decimal', () => { + const dexHelper = new DummyDexHelper(network); + + const tokens = Tokens[network]; + const srcTokenSymbol = 'USDC'; + const destTokenSymbol = 'steakUSDC'; + + const amountsForSell = [ + 0n, + 1n * BI_POWS[tokens[srcTokenSymbol].decimals], + 10n * BI_POWS[tokens[srcTokenSymbol].decimals], + ]; + + const amountsForBuy = [ + 0n, + 1n * BI_POWS[tokens[destTokenSymbol].decimals], + 10n * BI_POWS[tokens[destTokenSymbol].decimals], + ]; + + beforeAll(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + balancerV3 = new BalancerV3(network, dexKey, dexHelper); + if (balancerV3.initializePricing) { + await balancerV3.initializePricing(blockNumber); + } + }); + + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.SELL, + amountsForSell, + ); + }); + + it('getPoolIdentifiers and getPricesVolume BUY', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.BUY, + amountsForBuy, + ); + }); + }); + describe('Buffer unwrap 18decimal>6decimal', () => { + const dexHelper = new DummyDexHelper(network); + + const tokens = Tokens[network]; + const srcTokenSymbol = 'steakUSDC'; + const destTokenSymbol = 'USDC'; + + const amountsForSell = [ + 0n, + 1n * BI_POWS[tokens[srcTokenSymbol].decimals], + 10n * BI_POWS[tokens[srcTokenSymbol].decimals], + ]; + + const amountsForBuy = [ + 0n, + 1n * BI_POWS[tokens[destTokenSymbol].decimals], + 10n * BI_POWS[tokens[destTokenSymbol].decimals], + ]; + + beforeAll(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + balancerV3 = new BalancerV3(network, dexKey, dexHelper); + if (balancerV3.initializePricing) { + await balancerV3.initializePricing(blockNumber); + } + }); + + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.SELL, + amountsForSell, + ); + }); + + it('getPoolIdentifiers and getPricesVolume BUY', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.BUY, + amountsForBuy, + ); + }); + }); + describe('Full boosted path', () => { + const dexHelper = new DummyDexHelper(network); + + const tokens = Tokens[network]; + const srcTokenSymbol = 'wUSDL'; + const destTokenSymbol = 'USDC'; + + const amountsForSell = [ + 0n, + 1n * BI_POWS[tokens[srcTokenSymbol].decimals], + ]; + + const amountsForBuy = [ + 0n, + 1n * BI_POWS[tokens[destTokenSymbol].decimals], + ]; + + beforeAll(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + balancerV3 = new BalancerV3(network, dexKey, dexHelper); + if (balancerV3.initializePricing) { + await balancerV3.initializePricing(blockNumber); + } + }); + + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.SELL, + amountsForSell, + ); + }); + + it('getPoolIdentifiers and getPricesVolume BUY', async function () { + await testPricingOnNetwork( + balancerV3, + network, + dexKey, + blockNumber, + srcTokenSymbol, + destTokenSymbol, + SwapSide.BUY, + amountsForBuy, + ); + }); + }); + }); + }); +}); + +// Add back once multicall queries are working +/* +function decodeQuerySwapSingleTokenResult(results: Result, side: SwapSide) { + const balancerRouter = new Interface(balancerRouterAbi); + return results.map(result => { + const parsed = balancerRouter.decodeFunctionResult( + side === SwapSide.SELL + ? `querySwapSingleTokenExactIn` + : `querySwapSingleTokenExactOut`, + result, + ); + return BigInt(parsed[0]._hex); + }); +} + +async function checkOnChainPricing( + network: number, + side: SwapSide, + balancerV3: BalancerV3, + blockNumber: number, + prices: ExchangePrices, + amounts: bigint[], +) { + // test match for each returned price + for (const price of prices) { + const readerCallData = getQuerySwapSingleTokenCalldata( + network, + amounts, + price.data.steps[0], + side, + ); + const readerResult = ( + await balancerV3.dexHelper.multiContract.methods + .aggregate(readerCallData) + .call({}, blockNumber) + ).returnData; + const expectedPrices = [0n].concat( + decodeQuerySwapSingleTokenResult(readerResult, side), + ); + expect(price.prices).toEqual(expectedPrices); + } +} + */ diff --git a/src/dex/balancer-v3/balancer-v3-pool.ts b/src/dex/balancer-v3/balancer-v3-pool.ts new file mode 100644 index 000000000..783520f7b --- /dev/null +++ b/src/dex/balancer-v3/balancer-v3-pool.ts @@ -0,0 +1,702 @@ +import _ from 'lodash'; +import { Interface, defaultAbiCoder } from '@ethersproject/abi'; +import { DeepReadonly } from 'ts-essentials'; +import { Log, Logger } from '../../types'; +import { catchParseLogError } from '../../utils'; +import { StatefulEventSubscriber } from '../../stateful-event-subscriber'; +import { IDexHelper } from '../../dex-helper/idex-helper'; +import { + PoolState, + PoolStateMap, + StableMutableState, + Step, + TokenInfo, +} from './types'; +import { getPoolsApi } from './getPoolsApi'; +import vaultExtensionAbi_V3 from '../../abi/balancer-v3/vault-extension.json'; +import { + decodeErc4626MultiCallData, + decodeThrowError, + getErc4626MultiCallData, + getOnChainState, +} from './getOnChainState'; +import { BalancerV3Config } from './config'; +import { SwapKind, Vault } from '@balancer-labs/balancer-maths'; +import { + ampUpdateStartedEvent, + ampUpdateStoppedEvent, + getAmplificationParameter, + isStableMutableState, +} from './stablePool'; +import { BI_POWS } from '../../bigint-constants'; + +export const WAD = BI_POWS[18]; + +export class BalancerV3EventPool extends StatefulEventSubscriber { + handlers: { + [event: string]: ( + event: any, + state: DeepReadonly, + log: Readonly, + ) => DeepReadonly | null; + } = {}; + + logDecoder: (log: Log) => any; + + addressesSubscribed: string[]; + + interfaces: { + [name: string]: Interface; + }; + + vault: Vault; + + constructor( + readonly parentName: string, + protected network: number, + protected dexHelper: IDexHelper, + logger: Logger, + ) { + super( + parentName, + BalancerV3Config.BalancerV3[network].vaultAddress, + dexHelper, + logger, + ); + + this.interfaces = { + ['VAULT']: new Interface(vaultExtensionAbi_V3), + ['STABLE']: new Interface([ + 'function getAmplificationParameter() external view returns (uint256 value, bool isUpdating, uint256 precision)', + 'function getAmplificationState() external view returns (tuple(uint64 startValue, uint64 endValue, uint32 startTime, uint32 endTime) amplificationState, uint256 precision)', + ]), + ['ERC4626']: new Interface([ + 'function convertToAssets(uint256 shares) external view returns (uint256 assets)', + ]), + }; + + this.logDecoder = (log: Log) => this.interfaces['VAULT'].parseLog(log); + this.addressesSubscribed = [ + BalancerV3Config.BalancerV3[network].vaultAddress, + ]; + + // Add handlers + this.handlers['LiquidityAdded'] = this.liquidityAddedEvent.bind(this); + this.handlers['LiquidityRemoved'] = this.liquidityRemovedEvent.bind(this); + this.handlers['Swap'] = this.swapEvent.bind(this); + this.handlers['VaultAuxiliary'] = this.vaultAuxiliaryEvent.bind(this); + this.handlers['AggregateSwapFeePercentageChanged'] = + this.poolAggregateSwapFeePercentageEvent.bind(this); + this.handlers['SwapFeePercentageChanged'] = + this.poolSwapFeePercentageChangedEvent.bind(this); + this.handlers['PoolPausedStateChanged'] = + this.poolPausedStateChanged.bind(this); + + // replicates V3 maths with fees, pool and hook logic + this.vault = new Vault(); + } + + /** + * 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 processLog( + state: DeepReadonly, + log: Readonly, + ): DeepReadonly | null { + try { + const event = this.logDecoder(log); + if (event.name in this.handlers) { + return this.handlers[event.name](event, state, log); + } + } catch (e) { + catchParseLogError(e, this.logger); + } + + 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 block = await this.dexHelper.provider.getBlock(blockNumber); + const apiPoolStateMap = await getPoolsApi(this.network, block.timestamp); + const allOnChainPools = await getOnChainState( + this.network, + apiPoolStateMap, + this.dexHelper, + this.interfaces, + blockNumber, + ); + + // Filter out all paused pools + const filteredPools = Object.entries(allOnChainPools) + .filter(([address, pool]) => { + return !pool.isPoolPaused; + }) + .reduce((acc, [address, pool]) => { + acc[address] = pool; + return acc; + }, {} as PoolStateMap); + + return filteredPools; + } + + async getUpdatedPoolState( + existingPoolState: DeepReadonly, + ): Promise | null> { + // Get all latest pools from API + const apiPoolStateMap = await getPoolsApi(this.network); + + // Filter out pools that already exist in existing state + const newApiPools = Object.entries(apiPoolStateMap).reduce( + (acc, [address, pool]) => { + if (!existingPoolState[address]) { + acc[address] = pool; + } + return acc; + }, + {} as typeof apiPoolStateMap, + ); + + // If no new pools return + if (Object.keys(newApiPools).length === 0) { + return null; + } + + // Only get on-chain state for new pools + const newOnChainPools = await getOnChainState( + this.network, + newApiPools, + this.dexHelper, + this.interfaces, + ); + + // Filter out pools with hooks and paused pools from new state + // TODO this won't be necessary once API has this filter option + const filteredNewPools = Object.entries(newOnChainPools) + .filter(([_, pool]) => !(pool.hasHook || pool.isPoolPaused)) + .reduce((acc, [address, pool]) => { + acc[address] = pool; + return acc; + }, {} as PoolStateMap); + + // Merge existing pools with new pools + return { + ...existingPoolState, + ...filteredNewPools, + }; + } + + liquidityAddedEvent( + event: any, + state: DeepReadonly, + log: Readonly, + ): DeepReadonly | null { + const poolAddress = event.args.pool.toLowerCase(); + // Vault will send events from all pools, some of which are not officially supported by Balancer + if (!state[poolAddress]) { + return null; + } + const newState = _.cloneDeep(state) as PoolStateMap; + for ( + let i = 0; + i < newState[poolAddress].balancesLiveScaled18.length; + i++ + ) { + newState[poolAddress].balancesLiveScaled18[i] += this.toScaled18( + BigInt(event.args.amountsAddedRaw[i]), + newState[poolAddress].scalingFactors[i], + ); + } + newState[poolAddress].totalSupply = BigInt(event.args.totalSupply); + + return newState; + } + + liquidityRemovedEvent( + event: any, + state: DeepReadonly, + log: Readonly, + ): DeepReadonly | null { + const poolAddress = event.args.pool.toLowerCase(); + // Vault will send events from all pools, some of which are not officially supported by Balancer + if (!state[poolAddress]) { + return null; + } + const newState = _.cloneDeep(state) as PoolStateMap; + for ( + let i = 0; + i < newState[poolAddress].balancesLiveScaled18.length; + i++ + ) { + newState[poolAddress].balancesLiveScaled18[i] -= this.toScaled18( + BigInt(event.args.amountsRemovedRaw[i]), + newState[poolAddress].scalingFactors[i], + ); + } + newState[poolAddress].totalSupply = BigInt(event.args.totalSupply); + + return newState; + } + + swapEvent( + event: any, + state: DeepReadonly, + log: Readonly, + ): DeepReadonly | null { + const poolAddress = event.args.pool.toLowerCase(); + // Vault will send events from all pools, some of which are not officially supported by Balancer + if (!state[poolAddress]) { + return null; + } + const newState = _.cloneDeep(state) as PoolStateMap; + const tokenInIndex = newState[poolAddress].tokens.findIndex( + address => address.toLowerCase() === event.args.tokenIn.toLowerCase(), + ); + const tokenOutIndex = newState[poolAddress].tokens.findIndex( + address => address.toLowerCase() === event.args.tokenOut.toLowerCase(), + ); + if (tokenInIndex === -1 || tokenOutIndex === -1) { + this.logger.error(`swapEvent - token index not found in pool state`); + return null; + } + newState[poolAddress].balancesLiveScaled18[tokenInIndex] += this.toScaled18( + BigInt(event.args.amountIn), + newState[poolAddress].scalingFactors[tokenInIndex], + ); + newState[poolAddress].balancesLiveScaled18[tokenOutIndex] -= + this.toScaled18( + BigInt(event.args.amountOut), + newState[poolAddress].scalingFactors[tokenOutIndex], + ); + + return newState; + } + + vaultAuxiliaryEvent( + event: any, + state: DeepReadonly, + log: Readonly, + ): DeepReadonly | null { + // In SC Pools can use this event to emit event data from the Vault. + // Allows us to track pool specific events using only the Vault subscription. + // https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/interfaces/contracts/vault/IVaultExtension.sol + const poolAddress = event.args.pool.toLowerCase(); + // Vault will send events from all pools, some of which are not officially supported by Balancer + if (!state[poolAddress]) { + return null; + } + + const newState = _.cloneDeep(state) as PoolStateMap; + switch (event.args.eventKey) { + case 'AmpUpdateStarted': + ampUpdateStartedEvent(newState[poolAddress], event.args.eventData); + return newState; + case 'AmpUpdateStopped': + ampUpdateStoppedEvent(newState[poolAddress], event.args.eventData); + return newState; + default: + return null; + } + } + + poolAggregateSwapFeePercentageEvent( + event: any, + state: DeepReadonly, + log: Readonly, + ): DeepReadonly | null { + const poolAddress = event.args.pool.toLowerCase(); + // Vault will send events from all pools, some of which are not officially supported by Balancer + if (!state[poolAddress]) { + return null; + } + const newState = _.cloneDeep(state) as PoolStateMap; + newState[poolAddress].aggregateSwapFee = BigInt( + event.args.aggregateSwapFeePercentage, + ); + return newState; + } + + poolSwapFeePercentageChangedEvent( + event: any, + state: DeepReadonly, + log: Readonly, + ): DeepReadonly | null { + const poolAddress = event.args.pool.toLowerCase(); + // Vault will send events from all pools, some of which are not officially supported by Balancer + if (!state[poolAddress]) { + return null; + } + const newState = _.cloneDeep(state) as PoolStateMap; + newState[poolAddress].swapFee = BigInt(event.args.swapFeePercentage); + return newState; + } + + poolPausedStateChanged( + event: any, + state: DeepReadonly, + log: Readonly, + ): DeepReadonly | null { + // Unpaused pools will be added with correct state during updateStatePools + if (event.args.paused === false) return null; + const poolAddress = event.args.pool.toLowerCase(); + // Vault will send events from all pools, some of which are not officially supported by Balancer + if (!state[poolAddress]) { + return null; + } + // Remove paused pool from state as it can't be swapped against + const newState = _.cloneDeep(state) as PoolStateMap; + delete newState[poolAddress]; + return newState; + } + + getMaxSwapAmount( + pool: PoolState, + tokenIn: TokenInfo, + tokenOut: TokenInfo, + swapKind: SwapKind, + ): bigint { + // Find the maximum swap amount the pool will support + const maxSwapAmount = this.vault.getMaxSwapAmount( + { + swapKind, + balancesLiveScaled18: pool.balancesLiveScaled18, + tokenRates: pool.tokenRates, + scalingFactors: pool.scalingFactors, + indexIn: tokenIn.index, + indexOut: tokenOut.index, + }, + pool, + ); + return maxSwapAmount; + } + + getSwapResult( + steps: Step[], + amountRaw: bigint, + swapKind: SwapKind, + timestamp: number, + ): bigint { + if (amountRaw === 0n) return 0n; + + // A GivenOut needs to use steps in reverse during calculation + const indices = + swapKind === SwapKind.GivenIn + ? steps.keys() + : Array.from(steps.keys()).reverse(); + + let amount = amountRaw; + let outputAmountRaw = 0n; + for (const i of indices) { + const step = steps[i]; + // If its a Stable Pool with an updating Amp factor calculate current Amp value + if ( + step.poolState.poolType === 'STABLE' && + isStableMutableState(step.poolState) + ) { + if (step.poolState.ampIsUpdating) { + step.poolState.amp = getAmplificationParameter( + step.poolState.ampStartValue, + step.poolState.ampEndValue, + step.poolState.ampStartTime, + step.poolState.ampStopTime, + BigInt(timestamp), + ); + } + } + outputAmountRaw = this.vault.swap( + { + ...step.swapInput, + amountRaw: amount, + swapKind, + }, + step.poolState, + ); + // Next step uses output from previous step as input + amount = outputAmountRaw; + } + return outputAmountRaw; + } + + /** + * Retrieves any new pools via API/multicall and adds to state + */ + async updateStatePools(): Promise { + const blockNumber = await this.dexHelper.provider.getBlockNumber(); + // We just want the current saved state + const currentState = this.getStaleState() || {}; + const updatedPoolState = await this.getUpdatedPoolState(currentState); + if (updatedPoolState) this.setState(updatedPoolState, blockNumber); + } + + /** + * Uses multicall to get onchain token rate for each pool then updates pool state + */ + async updateStatePoolRates(): Promise { + // Get existing state + const poolState = _.cloneDeep(this.getStaleState()) as PoolStateMap; + if (!poolState) return; + + // Fetch onchain pool rates + const poolRates = await this.getPoolRates(poolState); + + // Update each pools rate + poolRates.forEach(({ poolAddress, tokenRates, erc4626Rates }, i) => { + poolState[poolAddress].tokenRates = tokenRates; + poolState[poolAddress].erc4626Rates = erc4626Rates; + }); + + // Update state + const blockNumber = await this.dexHelper.provider.getBlockNumber(); + this.setState(poolState, blockNumber); + } + + private async getPoolRates(poolState: PoolStateMap) { + const erc4626MultiCallData = getErc4626MultiCallData( + this.interfaces['ERC4626'], + poolState, + ); + + const poolAddresses = Object.keys(poolState); + // For each pool make the getPoolTokenRates call + const poolsMultiCallData = poolAddresses.map(address => { + return { + target: BalancerV3Config.BalancerV3[this.network].vaultAddress, + callData: this.interfaces['VAULT'].encodeFunctionData( + 'getPoolTokenRates', + [address], + ), + }; + }); + // 500 is an arbitrary number chosen based on the blockGasLimit + const slicedMultiCallData = _.chunk( + [...erc4626MultiCallData, ...poolsMultiCallData], + 500, + ); + + // Make the multicall + const multicallDataResult = ( + await Promise.all( + slicedMultiCallData.map(async _multiCallData => + this.dexHelper.multiContract.methods + .tryAggregate(false, _multiCallData) + .call({}), + ), + ) + ).flat(); + + const dataResultErc4626 = multicallDataResult.slice( + 0, + erc4626MultiCallData.length, + ); + const dataResultPools = multicallDataResult.slice( + erc4626MultiCallData.length, + ); + + const tokensWithRates = decodeErc4626MultiCallData( + this.interfaces['ERC4626'], + erc4626MultiCallData, + dataResultErc4626, + ); + + return poolAddresses.map((address, i) => { + const tokenRateResult = decodeThrowError( + this.interfaces['VAULT'], + 'getPoolTokenRates', + dataResultPools[i], + address, + ); + return { + poolAddress: address, + tokenRates: tokenRateResult.tokenRates.map((r: string) => BigInt(r)), + erc4626Rates: poolState[address].tokens.map(t => { + if (!tokensWithRates[t]) return null; + return tokensWithRates[t]; + }), + }; + }); + } + + // If a token is "boosted" it can be auto wrapped/unwrapped by Vault, e.g. aDAI<>DAI + // mainToken is the actual token the pool would contain, e.g. in a bbausd type setup it would be aDAI/aUSDC/aUSDT + // underlyingToken would be the unwrapped, e.g. DAI/USDC/USDT + // need rate info to calculate wrap/unwrap + getTokenInfo(poolState: PoolState, tokenAddress: string): TokenInfo | null { + // Check in main tokens + let tokenIndex = poolState.tokens.findIndex( + address => address.toLowerCase() === tokenAddress.toLowerCase(), + ); + if (tokenIndex !== -1) { + return { + isBoosted: false, + mainToken: tokenAddress, + underlyingToken: null, + index: tokenIndex, + rate: poolState.tokenRates[tokenIndex], + }; + } + + // Check in underlying tokens if available + if (poolState.tokensUnderlying) { + tokenIndex = poolState.tokensUnderlying.findIndex( + address => + address && address.toLowerCase() === tokenAddress.toLowerCase(), + ); + if (tokenIndex !== -1) { + if (poolState.erc4626Rates[tokenIndex] === null) { + this.logger.error( + `missing erc4626 token rate ${poolState.tokens[tokenIndex]}`, + ); + return null; + } + return { + isBoosted: true, + mainToken: poolState.tokens[tokenIndex], + underlyingToken: tokenAddress, + index: tokenIndex, + rate: poolState.erc4626Rates[tokenIndex]!, + }; + } + } + + // Token not found + this.logger.error(`getTokenInfo token not found`); + return null; + } + + /** + * Prepares all the step data required to simulate maths and construct swap transaction. + * Balancer V3 has the concepts of Boosted Pools and ERC4626 Liquidity Buffers. + * These enable highly capital efficient pools and gas efficient swaps. + * To swap via a buffer we must provide the correct ""steps" to the router transaction. + * Wrap: e.g. USDC>aUSDC + * Unwrap: e.g. aUSDC>USDC + * A full swap between USDC>DAI for an example bbausd pool consisting of aDAI/aUSDC/aUSDT would look like: + * USDC[wrap-buffer]aUSDC[swap-pool]aDAI[unwrap-buffer]USDC + * See docs for further info: + * https://docs-v3.balancer.fi/concepts/explore-available-balancer-pools/boosted-pool.html + * https://docs-v3.balancer.fi/concepts/vault/buffer.html + */ + getSteps(pool: PoolState, tokenIn: TokenInfo, tokenOut: TokenInfo): Step[] { + if (tokenIn.isBoosted && tokenOut.isBoosted) { + return [ + // Wrap tokenIn underlying to main token + this.getWrapStep(tokenIn), + // Swap main > main + this.getSwapStep(pool, tokenIn, tokenOut), + // Unwrap tokenOut main to underlying token + this.getUnwrapStep(tokenOut), + ]; + } else if (tokenIn.isBoosted) { + if ( + tokenIn.mainToken.toLowerCase() === tokenOut.mainToken.toLowerCase() + ) { + // wrap, token > erc4626 + // tokenIn is boosted, e.g. isn't pool token and must be wrapped + return [this.getWrapStep(tokenIn)]; + } + return [ + // Wrap tokenIn underlying to main token + this.getWrapStep(tokenIn), + // Swap main > main + this.getSwapStep(pool, tokenIn, tokenOut), + ]; + } else if (tokenOut.isBoosted) { + if ( + tokenIn.mainToken.toLowerCase() === tokenOut.mainToken.toLowerCase() + ) { + // unwrap, stata > token + // token out is boosted, e.g. isn't pool token + return [this.getUnwrapStep(tokenOut)]; + } + return [ + // Swap main > main + this.getSwapStep(pool, tokenIn, tokenOut), + // Unwrap tokenOut main to underlying token + this.getUnwrapStep(tokenOut), + ]; + } else { + return [ + // Swap main > main + this.getSwapStep(pool, tokenIn, tokenOut), + ]; + } + } + + getWrapStep(token: TokenInfo): Step { + if (!token.underlyingToken) + throw new Error( + `Buffer wrap: token has no underlying. ${token.mainToken}`, + ); + // Vault expects pool to be the ERC4626 wrapped token, e.g. aUSDC + return { + pool: token.mainToken, + isBuffer: true, + swapInput: { + tokenIn: token.underlyingToken, + tokenOut: token.mainToken, + }, + poolState: { + poolType: 'Buffer', + rate: token.rate, + poolAddress: token.mainToken, + tokens: [token.mainToken, token.underlyingToken], // staticToken & underlying + }, + }; + } + + getUnwrapStep(token: TokenInfo): Step { + if (!token.underlyingToken) + throw new Error( + `Buffer unwrap: token has no underlying. ${token.mainToken}`, + ); + // Vault expects pool to be the ERC4626 wrapped token, e.g. aUSDC + return { + pool: token.mainToken, + isBuffer: true, + swapInput: { + tokenIn: token.mainToken, + tokenOut: token.underlyingToken, + }, + poolState: { + poolType: 'Buffer', + // TODO: for ERC4626 fetch the wrap/unwrap rate + rate: token.rate, + poolAddress: token.mainToken, + tokens: [token.mainToken, token.underlyingToken], // staticToken & underlying + }, + }; + } + + getSwapStep(pool: PoolState, tokenIn: TokenInfo, tokenOut: TokenInfo): Step { + // A normal swap between two tokens in a pool + return { + pool: pool.poolAddress, + isBuffer: false, + swapInput: { + tokenIn: tokenIn.mainToken, + tokenOut: tokenOut.mainToken, + }, + poolState: pool, + }; + } + + toScaled18(amount: bigint, scalingFactor: bigint): bigint { + return (amount * scalingFactor * WAD) / WAD; + } +} diff --git a/src/dex/balancer-v3/balancer-v3.ts b/src/dex/balancer-v3/balancer-v3.ts new file mode 100644 index 000000000..fb1ecfbb3 --- /dev/null +++ b/src/dex/balancer-v3/balancer-v3.ts @@ -0,0 +1,614 @@ +import { AsyncOrSync, DeepReadonly } from 'ts-essentials'; +import { + Token, + Address, + ExchangePrices, + PoolPrices, + AdapterExchangeParam, + PoolLiquidity, + Logger, + DexExchangeParam, +} from '../../types'; +import { SwapSide, Network } from '../../constants'; +import * as CALLDATA_GAS_COST from '../../calldata-gas-cost'; +import { getBigIntPow, getDexKeysWithNetwork, isETHAddress } from '../../utils'; +import { IDex } from '../../dex/idex'; +import { IDexHelper } from '../../dex-helper/idex-helper'; +import { BalancerV3Data, PoolState, PoolStateMap } from './types'; +import { SimpleExchange } from '../simple-exchange'; +import { BalancerV3Config } from './config'; +import { BalancerV3EventPool } from './balancer-v3-pool'; +import { NumberAsString } from '@paraswap/core'; +import { SwapKind } from '@balancer-labs/balancer-maths'; +import { Interface } from '@ethersproject/abi'; +import { extractReturnAmountPosition } from '../../executor/utils'; +import { getTopPoolsApi } from './getTopPoolsApi'; +import balancerRouterAbi from '../../abi/balancer-v3/router.json'; +import balancerBatchRouterAbi from '../../abi/balancer-v3/batch-router.json'; +import { getGasCost } from './getGasCost'; +import { Block } from '@ethersproject/abstract-provider'; + +const MAX_UINT256 = + '115792089237316195423570985008687907853269984665640564039457584007913129639935'; +const POOL_UPDATE_TTL = 5 * 60; // 5mins +const RATE_UPDATE_TTL = 1 * 60; // 1min + +type DeepMutable = { + -readonly [P in keyof T]: T[P] extends object ? DeepMutable : T[P]; +}; + +export class BalancerV3 extends SimpleExchange implements IDex { + protected eventPools: BalancerV3EventPool; + + readonly hasConstantPriceLargeAmounts = false; + // Vault can handle native + readonly needWrapNative = false; + + readonly isFeeOnTransferSupported = false; + + public static dexKeysWithNetwork: { key: string; networks: Network[] }[] = + getDexKeysWithNetwork(BalancerV3Config); + + logger: Logger; + balancerRouter: Interface; + balancerBatchRouter: Interface; + updateNewPoolsTimer?: NodeJS.Timeout; + updateRatesTimer?: NodeJS.Timeout; + + latestBlock?: Block; + + constructor( + readonly network: Network, + readonly dexKey: string, + readonly dexHelper: IDexHelper, + ) { + super(dexHelper, dexKey); + this.logger = dexHelper.getLogger(dexKey); + this.eventPools = new BalancerV3EventPool( + dexKey, + network, + dexHelper, + this.logger, + ); + this.balancerRouter = new Interface(balancerRouterAbi); + this.balancerBatchRouter = new Interface(balancerBatchRouterAbi); + } + + // Initialize pricing is called once in the start of + // pricing service. It is intended to setup the integration + // for pricing requests. It is optional for a DEX to + // implement this function + async initializePricing(blockNumber: number) { + await this.eventPools.initialize(blockNumber); + + // This will periodically query API and add any new pools to pool state + if (!this.updateNewPoolsTimer) { + this.updateNewPoolsTimer = setInterval(async () => { + try { + await this.updatePoolState(); + } catch (e) { + this.logger.error(`${this.dexKey}: Failed to update pool state:`, e); + } + }, POOL_UPDATE_TTL * 1000); + } + + // This will periodically refresh tokenRates with onchain state + if (!this.updateRatesTimer) { + this.updateRatesTimer = setInterval(async () => { + try { + await this.updateStatePoolRates(); + } catch (e) { + this.logger.error(`${this.dexKey}: Failed to update pool rates:`, e); + } + }, RATE_UPDATE_TTL * 1000); + } + } + + getAdapters(side: SwapSide): null { + return null; + } + + // Returns list of pool identifiers that can be used + // for a given swap. poolIdentifiers must be unique + // across DEXes. It is recommended to use + // ${dexKey}_${poolAddress} as a poolIdentifier + async getPoolIdentifiers( + srcToken: Token, + destToken: Token, + side: SwapSide, + blockNumber: number, + ): Promise { + const _from = this.dexHelper.config.wrapETH(srcToken); + const _to = this.dexHelper.config.wrapETH(destToken); + const poolState = this.eventPools.getState(blockNumber); + if (poolState === null) return []; + return this.findPoolAddressesWithTokens( + poolState, + _from.address.toLowerCase(), + _to.address.toLowerCase(), + ); + } + + async getBlock(blockNumber: number): Promise { + if (this.latestBlock && this.latestBlock.number === blockNumber) { + return this.latestBlock; + } + + const block = await this.dexHelper.provider.getBlock(blockNumber); + this.latestBlock = block; + return block; + } + + findPoolAddressesWithTokens( + pools: DeepReadonly, + tokenA: string, + tokenB: string, + ): string[] { + return Object.entries(pools) + .filter(([, poolState]) => { + return this.hasTokens(poolState, [tokenA, tokenB]); + }) + .map(([address]) => address); + } + + /** + * Filter pools that have tokens from/to and are in limitPool list + * @param pools + * @param from + * @param to + * @param limitPools + * @returns Array of PoolState + */ + filterPools( + pools: DeepReadonly, + from: string, + to: string, + limitPools?: string[], + ): PoolState[] { + return Object.entries(pools) + .filter(([address, poolState]) => { + const hasRequiredTokens = this.hasTokens(poolState, [from, to]); + const isAllowedPool = !limitPools || limitPools.includes(address); + return hasRequiredTokens && isAllowedPool; + }) + .map(([_, poolState]) => poolState as DeepMutable); + } + + hasTokens(pool: DeepReadonly, tokens: string[]): boolean { + return tokens.every( + token => + pool.tokens.includes(token) || pool.tokensUnderlying.includes(token), + ); + } + + // 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> { + try { + const _from = this.dexHelper.config.wrapETH(srcToken); + const _to = this.dexHelper.config.wrapETH(destToken); + if (_from.address === _to.address) { + return null; + } + + // This is used to get block timestamp which is needed to calculate Amp if it is updating + const block = await this.getBlock(blockNumber); + + // get up to date pools and state + const allPoolState = this.eventPools.getState(blockNumber); + if (allPoolState === null) { + this.logger.error(`getState returned null`); + return null; + } + + // filter for pools with tokens and to only use limit pools + const allowedPools = this.filterPools( + allPoolState, + _from.address.toLowerCase(), + _to.address.toLowerCase(), + limitPools, + ); + + if (!allowedPools.length) return null; + + const swapKind = + side === SwapSide.SELL ? SwapKind.GivenIn : SwapKind.GivenOut; + const tokenIn = _from.address; + const tokenOut = _to.address; + + // Gets the single unit amount based off token decimals, e.g. for USDC its 1e6 + const unitAmount = getBigIntPow( + (side === SwapSide.SELL ? _from : _to).decimals, + ); + + const poolPrices: ExchangePrices = []; + // For each pool we calculate swap result using balancer maths + for (let i = 0; i < allowedPools.length; i++) { + const pool = { + ...allowedPools[i], + }; + + const tokenInInfo = this.eventPools.getTokenInfo(pool, tokenIn); + const tokenOutInfo = this.eventPools.getTokenInfo(pool, tokenOut); + if (!tokenInInfo || !tokenOutInfo) { + continue; + } + + const steps = this.eventPools.getSteps(pool, tokenInInfo, tokenOutInfo); + + try { + // This is the max amount the pool can swap + const maxSwapAmount = this.eventPools.getMaxSwapAmount( + pool, + tokenInInfo, + tokenOutInfo, + swapKind, + ); + + let unit = 0n; + if (unitAmount < maxSwapAmount) + unit = this.eventPools.getSwapResult( + steps, + unitAmount, + swapKind, + block.timestamp, + ); + + const poolExchangePrice: PoolPrices = { + prices: new Array(amounts.length).fill(0n), + unit, + data: { + steps: steps, + }, + exchange: this.dexKey, + gasCost: getGasCost(steps), + poolAddresses: [pool.poolAddress], + poolIdentifier: `${this.dexKey}_${pool.poolAddress}`, + }; + + for (let j = 0; j < amounts.length; j++) { + if (amounts[j] < maxSwapAmount) { + // Uses balancer maths to calculate swap + poolExchangePrice.prices[j] = this.eventPools.getSwapResult( + steps, + amounts[j], + swapKind, + block.timestamp, + ); + } + } + poolPrices.push(poolExchangePrice); + } catch (err) { + this.logger.error( + `error fetching prices for pool: ${pool.poolAddress}`, + ); + this.logger.error(err); + } + } + + return poolPrices; + } catch (err) {} + return null; + } + + // Returns estimated gas cost of calldata for this DEX in multiSwap + getCalldataGasCost( + poolPrices: PoolPrices, + ): number | number[] { + if ( + poolPrices.data.steps.length === 1 && + !poolPrices.data.steps[0].isBuffer + ) { + return ( + CALLDATA_GAS_COST.DEX_OVERHEAD + + // pool + CALLDATA_GAS_COST.ADDRESS + + // tokenIn + CALLDATA_GAS_COST.ADDRESS + + // tokenOut + CALLDATA_GAS_COST.ADDRESS + + // exactAmountOut + CALLDATA_GAS_COST.AMOUNT + + // maxAmountIn + CALLDATA_GAS_COST.AMOUNT + + // deadline + CALLDATA_GAS_COST.TIMESTAMP + + // wethIsEth + CALLDATA_GAS_COST.BOOL + + // userData + CALLDATA_GAS_COST.FULL_WORD + ); + } + + return ( + CALLDATA_GAS_COST.DEX_OVERHEAD + + CALLDATA_GAS_COST.LENGTH_LARGE + + // ParentStruct header + CALLDATA_GAS_COST.OFFSET_SMALL + + // ParentStruct -> paths[] header + CALLDATA_GAS_COST.OFFSET_LARGE + + // ParentStruct -> paths[] + CALLDATA_GAS_COST.LENGTH_SMALL + + // ParentStruct -> paths header + CALLDATA_GAS_COST.OFFSET_SMALL + + // ParentStruct -> paths -> exactAmountIn + CALLDATA_GAS_COST.AMOUNT + + // ParentStruct -> paths -> minAmountOut + CALLDATA_GAS_COST.AMOUNT + + // ParentStruct -> paths -> tokenIn + CALLDATA_GAS_COST.ADDRESS + + poolPrices.data.steps.reduce(step => { + return ( + // ParentStruct -> paths -> step header + CALLDATA_GAS_COST.OFFSET_SMALL + + // ParentStruct -> paths -> step -> isBuffer + CALLDATA_GAS_COST.BOOL + + // ParentStruct -> paths -> step -> pool + CALLDATA_GAS_COST.ADDRESS + + // ParentStruct -> paths -> step -> tokenOut + CALLDATA_GAS_COST.ADDRESS + ); + }, 0) + + // deadline + CALLDATA_GAS_COST.TIMESTAMP + + // wethIsEth + CALLDATA_GAS_COST.BOOL + + // userData + CALLDATA_GAS_COST.FULL_WORD + ); + } + + // Not used for V6 + getAdapterParam(): AdapterExchangeParam { + return { + targetExchange: '0x', + payload: '0x', + networkFee: '0', + }; + } + + getDexParam( + srcToken: Address, + destToken: Address, + srcAmount: NumberAsString, + destAmount: NumberAsString, + recipient: Address, + data: BalancerV3Data, + side: SwapSide, + ): DexExchangeParam { + if (side === SwapSide.SELL) { + return this.getExactInParam(srcToken, destToken, srcAmount, data); + } else { + return this.getExactOutParam( + srcToken, + destToken, + srcAmount, + destAmount, + data, + ); + } + } + + getExactInParam( + srcToken: Address, + destToken: Address, + srcAmount: NumberAsString, + data: BalancerV3Data, + ): DexExchangeParam { + if (data.steps.length === 1 && !data.steps[0].isBuffer) { + const exchangeData = this.balancerRouter.encodeFunctionData( + 'swapSingleTokenExactIn', + [ + data.steps[0].pool, + this.dexHelper.config.wrapETH(srcToken), + this.dexHelper.config.wrapETH(destToken), + srcAmount, + '0', // This should be limit for min amount out. Assume this is set elsewhere via Paraswap contract. + MAX_UINT256, // Deadline + isETHAddress(srcToken) || isETHAddress(destToken), + '0x', + ], + ); + return { + sendEthButSupportsInsertFromAmount: true, + permit2Approval: true, + needWrapNative: this.needWrapNative, + dexFuncHasRecipient: false, + exchangeData, + // This router handles single swaps + targetExchange: + BalancerV3Config.BalancerV3[this.network].balancerRouterAddress, + returnAmountPos: extractReturnAmountPosition( + this.balancerRouter, + 'swapSingleTokenExactIn', + ), + }; + } else { + // for each step: + // if tokenIn == pool router uses removeLiquidity SINGLE_TOKEN_EXACT_IN + // if tokenOut == pool router uses addLiquidity UNBALANCED + const exchangeData = this.balancerBatchRouter.encodeFunctionData( + 'swapExactIn', + [ + [ + { + tokenIn: this.dexHelper.config.wrapETH(srcToken), + steps: data.steps.map(step => ({ + pool: step.pool, + tokenOut: step.swapInput.tokenOut, + isBuffer: step.isBuffer, + })), + exactAmountIn: srcAmount, + minAmountOut: '0', // This should be limit for min amount out. Assume this is set elsewhere via Paraswap contract. + }, + ], + MAX_UINT256, // Deadline + isETHAddress(srcToken) || isETHAddress(destToken), + '0x', + ], + ); + + return { + sendEthButSupportsInsertFromAmount: true, + permit2Approval: true, + needWrapNative: this.needWrapNative, + dexFuncHasRecipient: false, + exchangeData, + // This router handles batch swaps + targetExchange: + BalancerV3Config.BalancerV3[this.network].balancerBatchRouterAddress, + returnAmountPos: undefined, + }; + } + } + + getExactOutParam( + srcToken: Address, + destToken: Address, + srcAmount: NumberAsString, + destAmount: NumberAsString, + data: BalancerV3Data, + ): DexExchangeParam { + if (data.steps.length === 1 && !data.steps[0].isBuffer) { + const exchangeData = this.balancerRouter.encodeFunctionData( + 'swapSingleTokenExactOut', + [ + data.steps[0].pool, + this.dexHelper.config.wrapETH(srcToken), + this.dexHelper.config.wrapETH(destToken), + destAmount, + MAX_UINT256, // This should be limit for max amount in. Assume this is set elsewhere via Paraswap contract. + MAX_UINT256, // Deadline + isETHAddress(srcToken) || isETHAddress(destToken), + '0x', + ], + ); + + return { + sendEthButSupportsInsertFromAmount: true, + permit2Approval: true, + needWrapNative: this.needWrapNative, + dexFuncHasRecipient: false, + exchangeData, + // Single swaps are submitted via Balancer Router + targetExchange: + BalancerV3Config.BalancerV3[this.network].balancerRouterAddress, + returnAmountPos: undefined, + }; + } else { + // for each step: + // if tokenIn == pool use removeLiquidity SINGLE_TOKEN_EXACT_OUT + // if tokenOut == pool use addLiquidity SINGLE_TOKEN_EXACT_OUT + const exchangeData = this.balancerBatchRouter.encodeFunctionData( + 'swapExactOut', + [ + [ + { + tokenIn: this.dexHelper.config.wrapETH(srcToken), + steps: data.steps.map(step => ({ + pool: step.pool, + tokenOut: step.swapInput.tokenOut, + isBuffer: step.isBuffer, + })), + exactAmountOut: destAmount, + maxAmountIn: srcAmount, + }, + ], + MAX_UINT256, // Deadline + isETHAddress(srcToken) || isETHAddress(destToken), + '0x', + ], + ); + + return { + sendEthButSupportsInsertFromAmount: true, + permit2Approval: true, + needWrapNative: this.needWrapNative, + dexFuncHasRecipient: false, + exchangeData, + // This router handles batch swaps + targetExchange: + BalancerV3Config.BalancerV3[this.network].balancerBatchRouterAddress, + returnAmountPos: undefined, + }; + } + } + + /** + * Uses multicall to get onchain token rate for each pool then updates pool state + */ + async updateStatePoolRates(): Promise { + await this.eventPools.updateStatePoolRates(); + } + + // This is called once before getTopPoolsForToken is + // called for multiple tokens. This can be helpful to + // update common state required for calculating + // getTopPoolsForToken. It is optional for a DEX + // to implement this + async updatePoolState(): Promise { + await this.eventPools.updateStatePools(); + } + + // Returns list of top pools based on liquidity. Max + // limit number pools should be returned. + async getTopPoolsForToken( + tokenAddress: Address, + count: number, + ): Promise { + const poolsWithToken = Object.entries(this.eventPools.getStaleState() || {}) + .filter(([, poolState]) => { + return this.hasTokens(poolState, [tokenAddress.toLowerCase()]); + }) + .map(([address]) => address); + + const topPools = await getTopPoolsApi(this.network, poolsWithToken, count); + + return topPools.map(pool => { + const tokens = pool.poolTokens + .filter(t => t.address !== tokenAddress) + .map(t => ({ + address: t.address, + decimals: t.decimals, + })); + + const underlyingTokens = pool.poolTokens + .map(t => t.underlyingToken) + .filter(t => !!t) + .filter(t => t?.address !== tokenAddress) as Token[]; + + return { + exchange: this.dexKey, + address: pool.address, + liquidityUSD: parseFloat(pool.dynamicData.totalLiquidity), + connectorTokens: tokens.concat(underlyingTokens), + }; + }); + } + + releaseResources(): AsyncOrSync { + if (this.updateNewPoolsTimer) { + clearInterval(this.updateNewPoolsTimer); + this.updateNewPoolsTimer = undefined; + this.logger.info( + `${this.dexKey}: cleared updateNewPoolsTimer before shutting down`, + ); + } + if (this.updateRatesTimer) { + clearInterval(this.updateRatesTimer); + this.updateRatesTimer = undefined; + this.logger.info( + `${this.dexKey}: cleared updateRatesTimer before shutting down`, + ); + } + } +} diff --git a/src/dex/balancer-v3/config.ts b/src/dex/balancer-v3/config.ts new file mode 100644 index 000000000..7df1ad9ea --- /dev/null +++ b/src/dex/balancer-v3/config.ts @@ -0,0 +1,42 @@ +import { DexParams } from './types'; +import { DexConfigMap, AdapterMappings } from '../../types'; +import { Network, SwapSide } from '../../constants'; + +// These map to the Balancer API poolType. Only Balancer supported pools will be added +export enum SUPPORTED_POOLS { + WEIGHTED = 'WEIGHTED', + STABLE = 'STABLE', +} + +export const disabledPoolIds: Record> = { + BalancerV3: { + [Network.GNOSIS]: [], + }, +}; + +// Balancer API - aggregatorSpecific query serves all useful static pool data +export const apiUrl = 'https://test-api-v3.balancer.fi/'; + +// TODO Full config added after V3 release +export const BalancerV3Config: DexConfigMap = { + BalancerV3: { + [Network.SEPOLIA]: { + vaultAddress: '0xbA1333333333a1BA1108E8412f11850A5C319bA9', + apiNetworkName: 'SEPOLIA', + balancerRouterAddress: '0x0BF61f706105EA44694f2e92986bD01C39930280', + balancerBatchRouterAddress: '0xC85b652685567C1B074e8c0D4389f83a2E458b1C', + }, + [Network.GNOSIS]: { + vaultAddress: '0xbA1333333333a1BA1108E8412f11850A5C319bA9', + apiNetworkName: 'GNOSIS', + balancerRouterAddress: '0x84813aA3e079A665C0B80F944427eE83cBA63617', + balancerBatchRouterAddress: '0xe2fa4e1d17725e72dcdAfe943Ecf45dF4B9E285b', + }, + [Network.MAINNET]: { + vaultAddress: '0xbA1333333333a1BA1108E8412f11850A5C319bA9', + apiNetworkName: 'MAINNET', + balancerRouterAddress: '0x5C6fb490BDFD3246EB0bB062c168DeCAF4bD9FDd', + balancerBatchRouterAddress: '0x136f1EFcC3f8f88516B9E94110D56FDBfB1778d1', + }, + }, +}; diff --git a/src/dex/balancer-v3/getGasCost.ts b/src/dex/balancer-v3/getGasCost.ts new file mode 100644 index 000000000..1dc97b786 --- /dev/null +++ b/src/dex/balancer-v3/getGasCost.ts @@ -0,0 +1,37 @@ +import { STABLE_GAS_COST } from './stablePool'; +import { Step } from './types'; +import { WEIGHTED_GAS_COST } from './weightedPool'; + +// https://sepolia.etherscan.io/tx/0x8c2c5ec7fc2855ed2ffab3467ee434f4e374e0ecf791e8d2b93c8d74e3f5b1fe +// 0x7ec61d6dcf0ea412327c30f013e60578c0ff4d83c334a95fb3a68739860e6f59 +// 0xb45331fe60091bdf4ba4c201b7fdfefc1ca8087fa1b6bc28877ef84d167809f4 +const FULL_BOOSTED_SWAP_GAS_COST = 283070; +// 0xb47bcae19ba4693d18f2148073d0d469ff59223e90d6f75eb25e06b0063f1556 +// 0x4730b0c1dce820f14747698446865364b98d96d3ed926e8e74bbead6482b8f8b +const PARTIAL_BOOSTED_SWAP_GAS_COST = 259815; +// 0xef9037f992645a9ecf26ddbdf65690a71acbffce2cc68445c869bb9707cb706a +// 0x77aa06350df079a077147f1051b10e0612542eb3aff3ae1b3d4004341cb64690 +const BUFFER_WRAP_UNWRAP_GAS_COST = 155921; + +export function getGasCost(steps: Step[]): number { + if (steps.length === 2) { + // Partial boosted/buffer swap: + // token[wrap]wrappedToken[swap]wrappedToken or + // wrappedToken[swap]wrappedToken[unwrap]token + return PARTIAL_BOOSTED_SWAP_GAS_COST; + } else if (steps.length === 3) { + // Full boosted/buffer swap: token[wrap]wrappedToken[swap]wrappedToken[unwrap]token + return FULL_BOOSTED_SWAP_GAS_COST; + } else { + switch (steps[0].poolState.poolType) { + case 'WEIGHTED': + return WEIGHTED_GAS_COST; + case 'STABLE': + return STABLE_GAS_COST; + case 'BUFFER': + return BUFFER_WRAP_UNWRAP_GAS_COST; + default: + return WEIGHTED_GAS_COST; + } + } +} diff --git a/src/dex/balancer-v3/getOnChainState.ts b/src/dex/balancer-v3/getOnChainState.ts new file mode 100644 index 000000000..025936621 --- /dev/null +++ b/src/dex/balancer-v3/getOnChainState.ts @@ -0,0 +1,400 @@ +import _ from 'lodash'; +import { + ImmutablePoolStateMap, + CommonMutableState, + PoolStateMap, + StableMutableState, +} from './types'; +import { BalancerV3Config } from './config'; +import { Interface, Result } from '@ethersproject/abi'; +import { IDexHelper } from '../../dex-helper'; +import { WAD } from './balancer-v3-pool'; + +interface callData { + target: string; + callData: string; +} + +// Encoding & Decoding for onchain calls to fetch mutable pool data +// Each supported pool type should have its own specific calls if needed +const poolOnChain: Record< + string, + { + count: number; + encode: ( + network: number, + contractInterface: Interface, + address: string, + ) => callData[]; + decode: ( + contractInterface: Interface, + poolAddress: string, + data: any, + startIndex: number, + ) => {} | CommonMutableState | StableMutableState; + } +> = { + ['COMMON']: { + count: 6, + ['encode']: ( + network: number, + contractInterface: Interface, + address: string, + ): callData[] => { + return [ + { + target: BalancerV3Config.BalancerV3[network].vaultAddress, + callData: contractInterface.encodeFunctionData('getPoolTokenRates', [ + address, + ]), + }, + { + target: BalancerV3Config.BalancerV3[network].vaultAddress, + callData: contractInterface.encodeFunctionData( + 'getCurrentLiveBalances', + [address], + ), + }, + { + target: BalancerV3Config.BalancerV3[network].vaultAddress, + callData: contractInterface.encodeFunctionData('getPoolConfig', [ + address, + ]), + }, + { + target: BalancerV3Config.BalancerV3[network].vaultAddress, + callData: contractInterface.encodeFunctionData('totalSupply', [ + address, + ]), + }, + { + target: BalancerV3Config.BalancerV3[network].vaultAddress, + callData: contractInterface.encodeFunctionData('isPoolPaused', [ + address, + ]), + }, + { + target: BalancerV3Config.BalancerV3[network].vaultAddress, + callData: contractInterface.encodeFunctionData('getHooksConfig', [ + address, + ]), + }, + ]; + }, + ['decode']: ( + contractInterface: Interface, + poolAddress: string, + data: any, + startIndex: number, + ): Omit => { + const resultTokenRates = decodeThrowError( + contractInterface, + 'getPoolTokenRates', + data[startIndex++], + poolAddress, + ); + if (!resultTokenRates) + throw new Error( + `Failed to get result for getPoolTokenRates for ${poolAddress}`, + ); + const resultLiveBalances = decodeThrowError( + contractInterface, + 'getCurrentLiveBalances', + data[startIndex++], + poolAddress, + ); + if (!resultLiveBalances) + throw new Error( + `Failed to get result for getCurrentLiveBalances for ${poolAddress}`, + ); + const resultGetPoolConfig = decodeThrowError( + contractInterface, + 'getPoolConfig', + data[startIndex++], + poolAddress, + ); + if (!resultGetPoolConfig) + throw new Error( + `Failed to get result for getPoolConfig for ${poolAddress}`, + ); + const resultTotalSupply = decodeThrowError( + contractInterface, + 'totalSupply', + data[startIndex++], + poolAddress, + ); + if (!resultTotalSupply) + throw new Error( + `Failed to get result for totalSupply for ${poolAddress}`, + ); + const resultIsPoolPaused = decodeThrowError( + contractInterface, + 'isPoolPaused', + data[startIndex++], + poolAddress, + ); + if (!resultIsPoolPaused) + throw new Error( + `Failed to get result for isPoolPaused for ${poolAddress}`, + ); + const resultHooksConfig = decodeThrowError( + contractInterface, + 'getHooksConfig', + data[startIndex++], + poolAddress, + ); + if (!resultHooksConfig) + throw new Error( + `Failed to get result for resultHooksConfig for ${poolAddress}`, + ); + return { + tokenRates: resultTokenRates.tokenRates.map((r: string) => BigInt(r)), + balancesLiveScaled18: resultLiveBalances.balancesLiveScaled18.map( + (b: string) => BigInt(b), + ), + swapFee: BigInt(resultGetPoolConfig[0].staticSwapFeePercentage), + aggregateSwapFee: BigInt( + resultGetPoolConfig[0].aggregateSwapFeePercentage, + ), + totalSupply: BigInt(resultTotalSupply[0]), + scalingFactors: resultTokenRates.decimalScalingFactors.map( + (r: string) => BigInt(r), + ), + isPoolPaused: resultIsPoolPaused[0], + hasHook: + resultHooksConfig[0].hooksContract !== + '0x0000000000000000000000000000000000000000', + }; + }, + }, + ['WEIGHTED']: { + count: 0, + ['encode']: ( + network: number, + contractInterface: Interface, + address: string, + ): callData[] => { + return []; + }, + ['decode']: ( + contractInterface: Interface, + poolAddress: string, + data: any, + startIndex: number, + ) => { + return {}; + }, + }, + ['STABLE']: { + count: 2, + ['encode']: ( + network: number, + contractInterface: Interface, + address: string, + ): callData[] => { + return [ + { + target: address, + callData: contractInterface.encodeFunctionData( + 'getAmplificationParameter', + ), + }, + { + target: address, + callData: contractInterface.encodeFunctionData( + 'getAmplificationState', + ), + }, + ]; + }, + ['decode']: ( + contractInterface: Interface, + poolAddress: string, + data: any, + startIndex: number, + ): StableMutableState => { + const resultAmp = decodeThrowError( + contractInterface, + 'getAmplificationParameter', + data[startIndex++], + poolAddress, + ); + if (!resultAmp) + throw new Error( + `Failed to get result for getAmplificationParameter for ${poolAddress}`, + ); + const resultAmpState = decodeThrowError( + contractInterface, + 'getAmplificationState', + data[startIndex++], + poolAddress, + ); + if (!resultAmpState) + throw new Error( + `Failed to get result for getAmplificationState for ${poolAddress}`, + ); + + return { + amp: resultAmp[0].toBigInt(), + ampIsUpdating: !!resultAmp[1], + ampStartValue: resultAmpState[0][0].toBigInt(), + ampEndValue: resultAmpState[0][1].toBigInt(), + ampStartTime: BigInt(resultAmpState[0][2]), + ampStopTime: BigInt(resultAmpState[0][3]), + }; + }, + }, +}; + +export function decodeThrowError( + contractInterface: Interface, + functionName: string, + resultEntry: { success: boolean; returnData: any }, + poolAddress: string, +): Result { + if (!resultEntry.success) + throw new Error(`Failed to execute ${functionName} for ${poolAddress}`); + return contractInterface.decodeFunctionResult( + functionName, + resultEntry.returnData, + ); +} + +export function getErc4626MultiCallData( + erc4626Interface: Interface, + immutablePoolStateMap: ImmutablePoolStateMap, +): callData[] { + // We want to query rate for each unique ERC4626 token + const uniqueErc4626Tokens = Array.from( + new Set( + Object.values(immutablePoolStateMap).flatMap(pool => + pool.tokens.filter((_, index) => pool.tokensUnderlying[index] !== null), + ), + ), + ); + + // query result for 1e18 (this maintains correct scaling for different token decimals in maths) + const erc4626MultiCallData: callData[] = uniqueErc4626Tokens.map(token => { + return { + target: token, + callData: erc4626Interface.encodeFunctionData('convertToAssets', [WAD]), + }; + }); + return erc4626MultiCallData; +} + +export function decodeErc4626MultiCallData( + erc4626Interface: Interface, + erc4626MultiCallData: callData[], + dataResultErc4626: any[], +) { + return Object.fromEntries( + erc4626MultiCallData.map((multiCallData, i) => { + const rate = decodeThrowError( + erc4626Interface, + 'convertToAssets', + dataResultErc4626[i], + multiCallData.target, + ); + if (!rate) + throw new Error( + `Failed to get result for convertToAssets for ${multiCallData.target}`, + ); + + return [multiCallData.target, BigInt(rate[0])]; + }), + ); +} + +// Any data from API will be immutable. Mutable data such as balances, etc will be fetched via onchain/event state. +export async function getOnChainState( + network: number, + immutablePoolStateMap: ImmutablePoolStateMap, + dexHelper: IDexHelper, + interfaces: { + [name: string]: Interface; + }, + blockNumber?: number, +): Promise { + const erc4626MultiCallData = getErc4626MultiCallData( + interfaces['ERC4626'], + immutablePoolStateMap, + ); + + // query pool specific onchain data, e.g. totalSupply, etc + const poolsMultiCallData = Object.entries(immutablePoolStateMap) + .map(([address, pool]) => { + return [ + ...poolOnChain['COMMON'].encode(network, interfaces['VAULT'], address), + ...poolOnChain[pool.poolType].encode( + network, + interfaces[pool.poolType], + address, + ), + ]; + }) + .flat(); + + // 500 is an arbitrary number chosen based on the blockGasLimit + const slicedMultiCallData = _.chunk( + [...erc4626MultiCallData, ...poolsMultiCallData], + 500, + ); + + const multicallDataResult = ( + await Promise.all( + slicedMultiCallData.map(async _multiCallData => + dexHelper.multiContract.methods + .tryAggregate(false, _multiCallData) + .call({}, blockNumber), + ), + ) + ).flat(); + + const dataResultErc4626 = multicallDataResult.slice( + 0, + erc4626MultiCallData.length, + ); + const dataResultPools = multicallDataResult.slice( + erc4626MultiCallData.length, + ); + + const tokensWithRates = decodeErc4626MultiCallData( + interfaces['ERC4626'], + erc4626MultiCallData, + dataResultErc4626, + ); + + let i = 0; + const poolStateMap = Object.fromEntries( + Object.entries(immutablePoolStateMap).map(([address, pool]) => { + const commonMutableData = poolOnChain['COMMON'].decode( + interfaces['VAULT'], + address, + dataResultPools, + i, + ) as CommonMutableState; + i = i + poolOnChain['COMMON'].count; + const poolMutableData = poolOnChain[pool.poolType].decode( + interfaces[pool.poolType], + address, + dataResultPools, + i, + ); + i = i + poolOnChain[pool.poolType].count; + return [ + address, + { + ...pool, + ...commonMutableData, + ...poolMutableData, + erc4626Rates: pool.tokens.map(t => { + if (!tokensWithRates[t]) return null; + return tokensWithRates[t]; + }), + }, + ]; + }), + ); + return poolStateMap; +} diff --git a/src/dex/balancer-v3/getPoolsApi.ts b/src/dex/balancer-v3/getPoolsApi.ts new file mode 100644 index 000000000..4056bf802 --- /dev/null +++ b/src/dex/balancer-v3/getPoolsApi.ts @@ -0,0 +1,129 @@ +import axios from 'axios'; +import { + apiUrl, + BalancerV3Config, + disabledPoolIds, + SUPPORTED_POOLS, +} from './config'; +import { CommonImmutablePoolState, ImmutablePoolStateMap } from './types'; +import { parseUnits } from 'ethers/lib/utils'; + +interface PoolToken { + address: string; + weight: string | null; + isErc4626: boolean; + underlyingToken: { + address: string; + } | null; +} + +interface Pool { + id: string; + type: string; + poolTokens: PoolToken[]; +} + +interface QueryResponse { + data: { + poolGetAggregatorPools: Pool[]; + }; +} + +function createQuery( + networkId: number, + poolTypes: SUPPORTED_POOLS[], + timestamp?: number, +): string { + const poolTypesString = poolTypes.map(type => `${type}`).join(', '); + const networkString = BalancerV3Config.BalancerV3[networkId].apiNetworkName; + const disabledPoolIdsString = disabledPoolIds.BalancerV3[networkId] + ?.map(p => `"${p}"`) + .join(', '); + + // Build the where clause conditionally + const whereClause = { + chainIn: networkString, + protocolVersionIn: 3, + hasHook: false, + poolTypeIn: `[${poolTypesString}]`, + ...(timestamp && { createTime: `{lt: ${timestamp}}` }), + ...(disabledPoolIdsString && { idNotIn: `[${disabledPoolIdsString}]` }), + }; + + // Convert where clause to string, filtering out undefined values + const whereString = Object.entries(whereClause) + .map(([key, value]) => `${key}: ${value}`) + .join(', '); + return ` + query MyQuery { + poolGetAggregatorPools( + where: {${whereString}} + ) { + id + type + poolTokens { + address + weight + isErc4626 + underlyingToken { + address + } + } + } + } + `; +} + +function toImmutablePoolStateMap(pools: Pool[]): ImmutablePoolStateMap { + return pools.reduce((map, pool) => { + const immutablePoolState: CommonImmutablePoolState = { + poolAddress: pool.id, + tokens: pool.poolTokens.map(t => t.address), + tokensUnderlying: pool.poolTokens.map(t => + t.underlyingToken ? t.underlyingToken.address : null, + ), + weights: pool.poolTokens.map(t => + t.weight ? parseUnits(t.weight, 18).toBigInt() : 0n, + ), + poolType: pool.type, + // TODO add scalingFactors once API provides them + // scalingFactors: pool.poolTokens.map(t => parseUnits('1', 18).toBigInt()), + // TODO Hook support will be added in future PR + hookType: undefined, + }; + + map[pool.id] = immutablePoolState; + return map; + }, {} as ImmutablePoolStateMap); +} + +// Any data from API will be immutable. Mutable data such as balances, etc will be fetched via onchain/event state. +export async function getPoolsApi( + network: number, + timestamp?: number, +): Promise { + try { + const query = createQuery( + network, + Object.values(SUPPORTED_POOLS), + timestamp, + ); + const response = await axios.post( + apiUrl, + { + query, + }, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + const pools = response.data.data.poolGetAggregatorPools; + return toImmutablePoolStateMap(pools); + } catch (error) { + // console.error('Error executing GraphQL query:', error); + throw error; + } +} diff --git a/src/dex/balancer-v3/getTopPoolsApi.ts b/src/dex/balancer-v3/getTopPoolsApi.ts new file mode 100644 index 000000000..26db2d0ba --- /dev/null +++ b/src/dex/balancer-v3/getTopPoolsApi.ts @@ -0,0 +1,101 @@ +import axios from 'axios'; +import { apiUrl, BalancerV3Config, disabledPoolIds } from './config'; + +interface PoolToken { + address: string; + decimals: number; + underlyingToken?: { + address: string; + decimals: number; + }; +} + +export interface Pool { + address: string; + poolTokens: PoolToken[]; + dynamicData: { + totalLiquidity: string; + }; +} + +interface QueryResponse { + data: { + poolGetAggregatorPools: Pool[]; + }; +} + +function createQuery( + networkId: number, + poolsFilter: string[], + count: number, +): string { + const disabledPoolIdsString = disabledPoolIds.BalancerV3[networkId] + ?.map(p => `"${p}"`) + .join(', '); + + const networkString = BalancerV3Config.BalancerV3[networkId].apiNetworkName; + const poolIdString = poolsFilter.map(a => `"${a}"`).join(', '); + // Build the where clause conditionally + const whereClause = { + chainIn: networkString, + protocolVersionIn: 3, + hasHook: false, + idIn: `[${poolIdString}]`, + ...(disabledPoolIdsString && { idNotIn: `[${disabledPoolIdsString}]` }), + }; + + // Convert where clause to string, filtering out undefined values + const whereString = Object.entries(whereClause) + .map(([key, value]) => `${key}: ${value}`) + .join(', '); + return ` + query MyQuery { + poolGetAggregatorPools( + where: {${whereString}} + first: ${count} + orderBy: totalLiquidity + orderDirection: desc + ) { + address + poolTokens { + address + decimals + underlyingToken { + address + decimals + } + } + dynamicData { + totalLiquidity + } + } + } + `; +} + +export async function getTopPoolsApi( + networkId: number, + poolsFilter: string[], + count: number, +): Promise { + try { + const query = createQuery(networkId, poolsFilter, count); + const response = await axios.post( + apiUrl, + { + query, + }, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + const pools = response.data.data.poolGetAggregatorPools; + return pools; + } catch (error) { + // console.error('Error executing GraphQL query:', error); + throw error; + } +} diff --git a/src/dex/balancer-v3/optimizer.ts b/src/dex/balancer-v3/optimizer.ts new file mode 100644 index 000000000..3925a25a6 --- /dev/null +++ b/src/dex/balancer-v3/optimizer.ts @@ -0,0 +1,66 @@ +import { UnoptimizedRate, OptimalSwap } from '../../types'; +import _ from 'lodash'; + +export function balancerV3Merge(or: UnoptimizedRate): UnoptimizedRate { + const fixRoute = (rawRate: OptimalSwap[]): OptimalSwap[] => { + let lastExchange: false | OptimalSwap = false; + + let optimizedRate = new Array(); + + rawRate.forEach((s: OptimalSwap) => { + if ( + s.swapExchanges.length !== 1 || + s.swapExchanges[0].exchange.toLowerCase() !== 'balancerv3' + ) { + lastExchange = false; + optimizedRate.push(s); + return; + } + + if ( + lastExchange && + lastExchange.swapExchanges[0].exchange.toLowerCase() === + s.swapExchanges[0].exchange.toLowerCase() && + _.last( + lastExchange.swapExchanges[0].data.steps, + )!.swapInput.tokenOut.toLowerCase() === + s.swapExchanges[0].data.steps[0].swapInput.tokenIn.toLowerCase() + ) { + lastExchange.swapExchanges[0].data.steps = + lastExchange.swapExchanges[0].data.steps.concat( + s.swapExchanges[0].data.steps, + ); + + lastExchange.swapExchanges[0].poolAddresses = + lastExchange.swapExchanges[0].poolAddresses!.concat( + s.swapExchanges[0].poolAddresses!, + ); + + lastExchange.swapExchanges[0].data.gasUSD = ( + parseFloat(lastExchange.swapExchanges[0].data.gasUSD) + + parseFloat(s.swapExchanges[0].data.gasUSD) + ).toFixed(6); + + lastExchange.destToken = s.destToken; + lastExchange.destDecimals = s.destDecimals; + + lastExchange.swapExchanges[0].destAmount = + s.swapExchanges[0].destAmount; + + return; + } + + lastExchange = _.cloneDeep(s); + optimizedRate.push(lastExchange); + }); + + return optimizedRate; + }; + + or.bestRoute = or.bestRoute.map(r => ({ + ...r, + swaps: fixRoute(r.swaps), + })); + + return or; +} diff --git a/src/dex/balancer-v3/stablePool.ts b/src/dex/balancer-v3/stablePool.ts new file mode 100644 index 000000000..1800d38b2 --- /dev/null +++ b/src/dex/balancer-v3/stablePool.ts @@ -0,0 +1,78 @@ +import { defaultAbiCoder } from '@ethersproject/abi'; +import { PoolState } from '@balancer-labs/balancer-maths'; +import { StableMutableState } from './types'; + +// TODO - Update with more accurate +export const STABLE_GAS_COST = 155000; + +export function isStableMutableState( + poolState: any, +): poolState is StableMutableState { + return ( + poolState && + typeof poolState === 'object' && + 'amp' in poolState && + 'ampIsUpdating' in poolState && + 'ampStartValue' in poolState && + 'ampEndValue' in poolState && + 'ampStartTime' in poolState && + 'ampStopTime' in poolState + ); +} + +// https://github.com/balancer/balancer-v3-monorepo/blob/009f2793abda248b150ccd15c1db25930c96ca82/pkg/pool-stable/contracts/StablePool.sol#L274 +export function getAmplificationParameter( + startValue: bigint, + endValue: bigint, + startTime: bigint, + endTime: bigint, + timestamp: bigint, +): bigint { + let value: bigint; + if (timestamp < endTime) { + if (endValue > startValue) { + value = + startValue + + ((endValue - startValue) * (timestamp - startTime)) / + (endTime - startTime); + } else { + value = + startValue - + ((startValue - endValue) * (timestamp - startTime)) / + (endTime - startTime); + } + } else { + value = endValue; + } + return value; +} + +export function ampUpdateStartedEvent(poolState: PoolState, eventData: any) { + // abi.encode(currentValueUint64, endValueUint64, startTimeUint32, endTimeUint32) + const decodedParams = defaultAbiCoder.decode( + ['uint64', 'uint64', 'uint32', 'uint32'], + eventData, + ); + if (isStableMutableState(poolState)) { + if (decodedParams[3] > decodedParams[2]) poolState.ampIsUpdating = true; + poolState.ampStartValue = decodedParams[0].toBigInt(); + poolState.ampEndValue = decodedParams[1].toBigInt(); + poolState.ampStartTime = BigInt(decodedParams[2]); + poolState.ampStopTime = BigInt(decodedParams[3]); + } else throw new Error("Can't update amp on non-stable pool"); +} + +export function ampUpdateStoppedEvent(poolState: PoolState, eventData: any) { + // abi.encode(currentValue) + const decodedParams = defaultAbiCoder.decode(['uint256'], eventData); + if (isStableMutableState(poolState)) { + poolState.ampIsUpdating = false; + poolState.amp = decodedParams[0].toBigInt(); + poolState.ampStartValue = decodedParams[0].toBigInt(); + poolState.ampEndValue = decodedParams[0].toBigInt(); + // In Contract these are update to timestamp event is called. + // There doesn't appear to be a way to easily get timestamp non-async so default to 0n which should have no effect + poolState.ampStartTime = BigInt(0n); + poolState.ampStopTime = BigInt(0n); + } else throw new Error("Can't update amp on non-stable pool"); +} diff --git a/src/dex/balancer-v3/types.ts b/src/dex/balancer-v3/types.ts new file mode 100644 index 000000000..f573c48dc --- /dev/null +++ b/src/dex/balancer-v3/types.ts @@ -0,0 +1,87 @@ +import { BufferState } from '@balancer-labs/balancer-maths'; +import { Address } from '../../types'; + +// Immutable data types available on all pools (Available from API) +export type CommonImmutablePoolState = { + poolAddress: string; + poolType: string; + // For boosted pools tokens is the actual pool token wrapped, e.g. aUSDC/aDAI + tokens: string[]; + // For boosted pools underlying is the unwrapped token, e.g. USDC/DAI + tokensUnderlying: (string | null)[]; + weights: bigint[]; + // TODO re-introduce this once added to API + // scalingFactors: bigint[]; + hookType: string | undefined; +}; + +// Mutable data types available on all pools (Available via onchain calls/events) +export interface CommonMutableState { + tokenRates: bigint[]; + erc4626Rates: (bigint | null)[]; + balancesLiveScaled18: bigint[]; + swapFee: bigint; + aggregateSwapFee: bigint; + totalSupply: bigint; + isPoolPaused: boolean; + // TODO remove this once API provides it + scalingFactors: bigint[]; + // TODO remove this once API provides it + hasHook: boolean; +} + +type CommonPoolState = CommonImmutablePoolState & CommonMutableState; + +export type PoolState = + | CommonPoolState + | (CommonPoolState & StableMutableState); + +// Stable Pool specific mutable data +export interface StableMutableState { + amp: bigint; + ampIsUpdating: boolean; + ampStartValue: bigint; + ampEndValue: bigint; + ampStartTime: bigint; + ampStopTime: bigint; +} + +export type PoolStateMap = { + [address: string]: PoolState; +}; + +export type ImmutablePoolStateMap = { + [address: string]: CommonImmutablePoolState; +}; + +export type Step = { + pool: Address; + isBuffer: boolean; + swapInput: { + tokenIn: Address; + tokenOut: Address; + }; + poolState: PoolState | BufferState; +}; + +export type BalancerV3Data = { + steps: Step[]; +}; + +export type DexParams = { + // Used to map network > API Name, e.g. 11155111>SEPOLIA + apiNetworkName: string; + vaultAddress: string; + // This router handles single swaps + // https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/interfaces/contracts/vault/IRouter.sol + balancerRouterAddress: string; + balancerBatchRouterAddress: string; +}; + +export type TokenInfo = { + isBoosted: boolean; + underlyingToken: string | null; + mainToken: string; + index: number; + rate: bigint; +}; diff --git a/src/dex/balancer-v3/weightedPool.ts b/src/dex/balancer-v3/weightedPool.ts new file mode 100644 index 000000000..bbb30c753 --- /dev/null +++ b/src/dex/balancer-v3/weightedPool.ts @@ -0,0 +1,2 @@ +// https://sepolia.etherscan.io/tx/0xe07958ff341aab57ab96a10347b65979b5b041f6408336d7e160e59bd02f9a40 +export const WEIGHTED_GAS_COST = 154000; diff --git a/src/dex/bebop/bebop.ts b/src/dex/bebop/bebop.ts index 46e0bb08e..fe69a4f19 100644 --- a/src/dex/bebop/bebop.ts +++ b/src/dex/bebop/bebop.ts @@ -723,7 +723,7 @@ export class Bebop extends SimpleExchange implements IDex { } } - if (side == SwapSide.SELL) { + if (side === SwapSide.SELL) { const requiredAmount = BigInt(optimalSwapExchange.destAmount); const quoteAmount = BigInt( response.buyTokens[utils.getAddress(destToken.address)].amount, diff --git a/src/dex/cables/cables-e2e.test.ts b/src/dex/cables/cables-e2e.test.ts new file mode 100644 index 000000000..f1cdf62dd --- /dev/null +++ b/src/dex/cables/cables-e2e.test.ts @@ -0,0 +1,207 @@ +import dotenv from 'dotenv'; +dotenv.config(); +import { testE2E } from '../../../tests/utils-e2e'; +import { Tokens, Holders } from '../../../tests/constants-e2e'; +import { Network, ContractMethod, SwapSide } from '../../constants'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { generateConfig } from '../../config'; + +const sleepMs: number = 10000; +const slippage: number = 100; + +describe('Cables E2E', () => { + const dexKey = 'Cables'; + + const sideToContractMethods = new Map([ + [SwapSide.SELL, [ContractMethod.swapExactAmountIn]], + [SwapSide.BUY, [ContractMethod.swapExactAmountOut]], + ]); + + describe('Arbitrum', () => { + const network = Network.ARBITRUM; + const provider = new StaticJsonRpcProvider( + generateConfig(network).privateHttpProvider, + network, + ); + const tokens = Tokens[network]; + const holders = Holders[network]; + + const pairs: { name: string; sellAmount: string; buyAmount: string }[][] = [ + [ + { + name: 'USDC', + sellAmount: '500000', + buyAmount: '700000', + }, + { + name: 'USDT', + sellAmount: '600000', + buyAmount: '850000', + }, + ], + [ + { + name: 'WETH', + sellAmount: '100000000000000000', + buyAmount: '200000000000000000', + }, + { + name: 'USDT', + sellAmount: '6000000', + buyAmount: '8000000', + }, + ], + ]; + + sideToContractMethods.forEach((contractMethods, side) => + describe(`${side}`, () => { + contractMethods.forEach((contractMethod: ContractMethod) => { + pairs.forEach(pair => { + describe(`${contractMethod}`, () => { + it(`${pair[0].name} -> ${pair[1].name}`, async () => { + await testE2E( + side === SwapSide.SELL + ? tokens[pair[0].name] + : tokens[pair[1].name], + side === SwapSide.SELL + ? tokens[pair[1].name] + : tokens[pair[0].name], + side === SwapSide.SELL + ? holders[pair[0].name] + : holders[pair[1].name], + side === SwapSide.SELL + ? pair[0].sellAmount + : pair[0].buyAmount, + side, + dexKey, + contractMethod, + network, + provider, + undefined, + undefined, + undefined, + slippage, + sleepMs, + ); + }); + it(`${pair[1].name} -> ${pair[0].name}`, async () => { + await testE2E( + side === SwapSide.SELL + ? tokens[pair[1].name] + : tokens[pair[0].name], + side === SwapSide.SELL + ? tokens[pair[0].name] + : tokens[pair[1].name], + side === SwapSide.SELL + ? holders[pair[1].name] + : holders[pair[0].name], + side === SwapSide.SELL + ? pair[1].sellAmount + : pair[1].buyAmount, + side, + dexKey, + contractMethod, + network, + provider, + undefined, + undefined, + undefined, + slippage, + sleepMs, + ); + }); + }); + }); + }); + }), + ); + }); + + describe('Avalanche', () => { + const network = Network.AVALANCHE; + const provider = new StaticJsonRpcProvider( + generateConfig(network).privateHttpProvider, + network, + ); + const tokens = Tokens[network]; + const holders = Holders[network]; + + const pairs: { name: string; sellAmount: string; buyAmount: string }[][] = [ + [ + { + name: 'USDC', + sellAmount: '1000000', + buyAmount: '700000', + }, + { + name: 'USDT', + sellAmount: '1000000', + buyAmount: '850000', + }, + ], + ]; + + sideToContractMethods.forEach((contractMethods, side) => + describe(`${side}`, () => { + contractMethods.forEach((contractMethod: ContractMethod) => { + pairs.forEach(pair => { + describe(`${contractMethod}`, () => { + it(`${pair[0].name} -> ${pair[1].name}`, async () => { + await testE2E( + side === SwapSide.SELL + ? tokens[pair[0].name] + : tokens[pair[1].name], + side === SwapSide.SELL + ? tokens[pair[1].name] + : tokens[pair[0].name], + side === SwapSide.SELL + ? holders[pair[0].name] + : holders[pair[1].name], + side === SwapSide.SELL + ? pair[0].sellAmount + : pair[0].buyAmount, + side, + dexKey, + contractMethod, + network, + provider, + undefined, + undefined, + undefined, + slippage, + sleepMs, + ); + }); + it(`${pair[1].name} -> ${pair[0].name}`, async () => { + await testE2E( + side === SwapSide.SELL + ? tokens[pair[1].name] + : tokens[pair[0].name], + side === SwapSide.SELL + ? tokens[pair[0].name] + : tokens[pair[1].name], + side === SwapSide.SELL + ? holders[pair[1].name] + : holders[pair[0].name], + side === SwapSide.SELL + ? pair[1].sellAmount + : pair[1].buyAmount, + side, + dexKey, + contractMethod, + network, + provider, + undefined, + undefined, + undefined, + slippage, + sleepMs, + ); + }); + }); + }); + }); + }), + ); + }); +}); diff --git a/src/dex/cables/cables-integration.test.ts b/src/dex/cables/cables-integration.test.ts new file mode 100644 index 000000000..62dc90c58 --- /dev/null +++ b/src/dex/cables/cables-integration.test.ts @@ -0,0 +1,391 @@ +/* eslint-disable no-console */ +import dotenv from 'dotenv'; +dotenv.config(); + +import { DummyDexHelper } from '../../dex-helper/index'; +import { Network, SwapSide } from '../../constants'; +import { BI_POWS } from '../../bigint-constants'; +import { Cables } from './cables'; +import { + checkPoolPrices, + checkPoolsLiquidity, + checkConstantPoolPrices, + sleep, +} from '../../../tests/utils'; +import { Tokens } from '../../../tests/constants-e2e'; + +async function testPricingOnNetwork( + cables: Cables, + network: Network, + dexKey: string, + blockNumber: number, + srcTokenSymbol: string, + destTokenSymbol: string, + side: SwapSide, + amounts: bigint[], +) { + const networkTokens = Tokens[network]; + + const pools = await cables.getPoolIdentifiers( + networkTokens[srcTokenSymbol], + networkTokens[destTokenSymbol], + side, + blockNumber, + ); + console.log( + `${srcTokenSymbol} <> ${destTokenSymbol} Pool Identifiers: `, + pools, + ); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await cables.getPricesVolume( + networkTokens[srcTokenSymbol], + networkTokens[destTokenSymbol], + amounts, + side, + blockNumber, + pools, + ); + console.log( + `${srcTokenSymbol} <> ${destTokenSymbol} Pool Prices: `, + poolPrices, + ); + + expect(poolPrices).not.toBeNull(); + if (cables.hasConstantPriceLargeAmounts) { + checkConstantPoolPrices(poolPrices!, amounts, dexKey); + } else { + checkPoolPrices(poolPrices!, amounts, side, dexKey); + } +} + +describe('Cables', function () { + const dexKey = 'Cables'; + let blockNumber: number; + let cables: Cables; + + describe('Avalanche', () => { + const network = Network.AVALANCHE; + const dexHelper = new DummyDexHelper(network); + + const tokens = Tokens[network]; + + const tokenASymbol = 'USDC'; + const tokenBSymbol = 'USDT'; + + const amountsForTokenA = [ + 0n, + 1n * BI_POWS[tokens[tokenASymbol].decimals], + 2n * BI_POWS[tokens[tokenASymbol].decimals], + 3n * BI_POWS[tokens[tokenASymbol].decimals], + 4n * BI_POWS[tokens[tokenASymbol].decimals], + 5n * BI_POWS[tokens[tokenASymbol].decimals], + 6n * BI_POWS[tokens[tokenASymbol].decimals], + 7n * BI_POWS[tokens[tokenASymbol].decimals], + 8n * BI_POWS[tokens[tokenASymbol].decimals], + 9n * BI_POWS[tokens[tokenASymbol].decimals], + 10n * BI_POWS[tokens[tokenASymbol].decimals], + ]; + + const amountsForTokenB = [ + 0n, + 1n * BI_POWS[tokens[tokenBSymbol].decimals], + 2n * BI_POWS[tokens[tokenBSymbol].decimals], + 3n * BI_POWS[tokens[tokenBSymbol].decimals], + 4n * BI_POWS[tokens[tokenBSymbol].decimals], + 5n * BI_POWS[tokens[tokenBSymbol].decimals], + 6n * BI_POWS[tokens[tokenBSymbol].decimals], + 7n * BI_POWS[tokens[tokenBSymbol].decimals], + 8n * BI_POWS[tokens[tokenBSymbol].decimals], + 9n * BI_POWS[tokens[tokenBSymbol].decimals], + 10n * BI_POWS[tokens[tokenBSymbol].decimals], + ]; + + beforeEach(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + cables = new Cables(network, dexKey, dexHelper); + await cables.initializePricing(blockNumber); + await sleep(5000); + }); + + afterEach(async () => { + if (cables.releaseResources) cables.releaseResources(); + await sleep(5000); + }); + + it(`getPoolIdentifiers and getPricesVolume SELL ${tokenASymbol} ${tokenBSymbol}`, async function () { + await testPricingOnNetwork( + cables, + network, + dexKey, + blockNumber, + tokenASymbol, + tokenBSymbol, + SwapSide.SELL, + amountsForTokenA, + ); + }); + + it(`getPoolIdentifiers and getPricesVolume BUY ${tokenASymbol} ${tokenBSymbol}`, async function () { + await testPricingOnNetwork( + cables, + network, + dexKey, + blockNumber, + tokenASymbol, + tokenBSymbol, + SwapSide.BUY, + amountsForTokenB, + ); + }); + + it(`getPoolIdentifiers and getPricesVolume SELL ${tokenBSymbol} ${tokenASymbol}`, async function () { + await testPricingOnNetwork( + cables, + network, + dexKey, + blockNumber, + tokenBSymbol, + tokenASymbol, + SwapSide.SELL, + amountsForTokenB, + ); + }); + + it(`getPoolIdentifiers and getPricesVolume BUY ${tokenBSymbol} ${tokenASymbol}`, async function () { + await testPricingOnNetwork( + cables, + network, + dexKey, + blockNumber, + tokenBSymbol, + tokenASymbol, + SwapSide.BUY, + amountsForTokenA, + ); + }); + + it.skip('getTopPoolsForToken', async function () { + // We have to check without calling initializePricing, because + // pool-tracker is not calling that function + const cables = new Cables(network, dexKey, dexHelper); + const poolLiquidity = await cables.getTopPoolsForToken( + tokens[tokenASymbol].address, + 10, + ); + console.log( + `${tokenASymbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + console.log( + `${tokenASymbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + + if (!cables.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity( + poolLiquidity, + Tokens[network][tokenASymbol].address, + dexKey, + ); + } + }); + }); + + describe('Arbitrum', () => { + const network = Network.ARBITRUM; + const dexHelper = new DummyDexHelper(network); + + const tokens = Tokens[network]; + + const tokenASymbol = 'USDC'; + const tokenBSymbol = 'USDT'; + + const amountsForTokenA = [ + 0n, + 1n * BI_POWS[tokens[tokenASymbol].decimals], + 2n * BI_POWS[tokens[tokenASymbol].decimals], + 3n * BI_POWS[tokens[tokenASymbol].decimals], + 4n * BI_POWS[tokens[tokenASymbol].decimals], + 5n * BI_POWS[tokens[tokenASymbol].decimals], + 6n * BI_POWS[tokens[tokenASymbol].decimals], + 7n * BI_POWS[tokens[tokenASymbol].decimals], + 8n * BI_POWS[tokens[tokenASymbol].decimals], + 9n * BI_POWS[tokens[tokenASymbol].decimals], + 10n * BI_POWS[tokens[tokenASymbol].decimals], + ]; + + const amountsForTokenB = [ + 0n, + 1n * BI_POWS[tokens[tokenBSymbol].decimals], + 2n * BI_POWS[tokens[tokenBSymbol].decimals], + 3n * BI_POWS[tokens[tokenBSymbol].decimals], + 4n * BI_POWS[tokens[tokenBSymbol].decimals], + 5n * BI_POWS[tokens[tokenBSymbol].decimals], + 6n * BI_POWS[tokens[tokenBSymbol].decimals], + 7n * BI_POWS[tokens[tokenBSymbol].decimals], + 8n * BI_POWS[tokens[tokenBSymbol].decimals], + 9n * BI_POWS[tokens[tokenBSymbol].decimals], + 10n * BI_POWS[tokens[tokenBSymbol].decimals], + ]; + + beforeEach(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + cables = new Cables(network, dexKey, dexHelper); + await cables.initializePricing(blockNumber); + await sleep(5000); + }); + + afterEach(async () => { + if (cables.releaseResources) cables.releaseResources(); + await sleep(5000); + }); + + it(`getPoolIdentifiers and getPricesVolume SELL ${tokenASymbol} ${tokenBSymbol}`, async function () { + await testPricingOnNetwork( + cables, + network, + dexKey, + blockNumber, + tokenASymbol, + tokenBSymbol, + SwapSide.SELL, + amountsForTokenA, + ); + }); + + it(`getPoolIdentifiers and getPricesVolume BUY ${tokenASymbol} ${tokenBSymbol}`, async function () { + await testPricingOnNetwork( + cables, + network, + dexKey, + blockNumber, + tokenASymbol, + tokenBSymbol, + SwapSide.BUY, + amountsForTokenB, + ); + }); + + it(`getPoolIdentifiers and getPricesVolume SELL ${tokenBSymbol} ${tokenASymbol}`, async function () { + await testPricingOnNetwork( + cables, + network, + dexKey, + blockNumber, + tokenBSymbol, + tokenASymbol, + SwapSide.SELL, + amountsForTokenB, + ); + }); + + it(`getPoolIdentifiers and getPricesVolume BUY ${tokenBSymbol} ${tokenASymbol}`, async function () { + await testPricingOnNetwork( + cables, + network, + dexKey, + blockNumber, + tokenBSymbol, + tokenASymbol, + SwapSide.BUY, + amountsForTokenA, + ); + }); + + describe.skip('getTopPoolsForToken', () => { + it('USDC getTopPoolsForToken', async function () { + // We have to check without calling initializePricing, because + // pool-tracker is not calling that function + const tokenSymbol = 'USDC'; + const cables = new Cables(network, dexKey, dexHelper); + const poolLiquidity = await cables.getTopPoolsForToken( + tokens[tokenSymbol].address, + 10, + ); + console.log( + `${tokenASymbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + + if (!cables.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity( + poolLiquidity, + Tokens[network][tokenASymbol].address, + dexKey, + ); + } + }); + + it('WETH getTopPoolsForToken', async function () { + // We have to check without calling initializePricing, because + // pool-tracker is not calling that function + const tokenSymbol = 'WETH'; + const cables = new Cables(network, dexKey, dexHelper); + const poolLiquidity = await cables.getTopPoolsForToken( + tokens[tokenSymbol].address, + 10, + ); + console.log( + `${tokenASymbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + + if (!cables.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity( + poolLiquidity, + Tokens[network][tokenASymbol].address, + dexKey, + ); + } + }); + + it('ETH getTopPoolsForToken', async function () { + // We have to check without calling initializePricing, because + // pool-tracker is not calling that function + const tokenSymbol = 'ETH'; + const cables = new Cables(network, dexKey, dexHelper); + const poolLiquidity = await cables.getTopPoolsForToken( + tokens[tokenSymbol].address, + 10, + ); + console.log( + `${tokenASymbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + + if (!cables.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity( + poolLiquidity, + Tokens[network][tokenASymbol].address, + dexKey, + ); + } + }); + + it('ARB getTopPoolsForToken', async function () { + // We have to check without calling initializePricing, because + // pool-tracker is not calling that function + const tokenSymbol = 'ARB'; + const cables = new Cables(network, dexKey, dexHelper); + const poolLiquidity = await cables.getTopPoolsForToken( + tokens[tokenSymbol].address, + 10, + ); + console.log( + `${tokenASymbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + + if (!cables.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity( + poolLiquidity, + Tokens[network][tokenASymbol].address, + dexKey, + ); + } + }); + }); + }); +}); diff --git a/src/dex/cables/cables.ts b/src/dex/cables/cables.ts new file mode 100644 index 000000000..eb14b3f8e --- /dev/null +++ b/src/dex/cables/cables.ts @@ -0,0 +1,823 @@ +import { + Address, + NumberAsString, + OptimalSwapExchange, + SwapSide, +} from '@paraswap/core'; +import { assert, AsyncOrSync } from 'ts-essentials'; +import * as CALLDATA_GAS_COST from '../../calldata-gas-cost'; +import { ETHER_ADDRESS, Network, NULL_ADDRESS } from '../../constants'; +import { IDexHelper } from '../../dex-helper'; +import { + AdapterExchangeParam, + DexExchangeParam, + ExchangePrices, + ExchangeTxInfo, + Logger, + PoolLiquidity, + PoolPrices, + PreprocessTransactionOptions, + Token, + TransferFeeParams, +} from '../../types'; +import { getDexKeysWithNetwork, Utils } from '../../utils'; +import { IDex } from '../idex'; +import { SimpleExchange } from '../simple-exchange'; +import { CablesConfig } from './config'; +import { + CABLES_API_BLACKLIST_POLLING_INTERVAL_MS, + CABLES_API_PAIRS_POLLING_INTERVAL_MS, + CABLES_API_PRICES_POLLING_INTERVAL_MS, + CABLES_API_TOKENS_POLLING_INTERVAL_MS, + CABLES_API_URL, + CABLES_BLACKLIST_CACHES_TTL_S, + CABLES_ERRORS_CACHE_KEY, + CABLES_FIRM_QUOTE_TIMEOUT_MS, + CABLES_GAS_COST, + CABLES_PAIRS_CACHES_TTL_S, + CABLES_PRICES_CACHES_TTL_S, + CABLES_RESTRICT_CHECK_INTERVAL_MS, + CABLES_RESTRICT_COUNT_THRESHOLD, + CABLES_RESTRICT_TTL_S, + CABLES_RESTRICTED_CACHE_KEY, + CABLES_TOKENS_CACHES_TTL_S, +} from './constants'; +import { CablesRateFetcher } from './rate-fetcher'; +import { + CablesData, + CablesRFQResponse, + RestrictData, + SlippageError, +} from './types'; +import mainnetRFQAbi from '../../abi/cables/CablesMainnetRFQ.json'; +import { Interface } from 'ethers/lib/utils'; +import BigNumber from 'bignumber.js'; +import { ethers } from 'ethers'; +import { BI_MAX_UINT256 } from '../../bigint-constants'; +import _ from 'lodash'; +import { BebopData } from '../bebop/types'; + +export class Cables extends SimpleExchange implements IDex { + public static dexKeysWithNetwork: { key: string; networks: Network[] }[] = + getDexKeysWithNetwork(CablesConfig); + + readonly isStatePollingDex = true; + + private rateFetcher: CablesRateFetcher; + + logger: Logger; + private tokensMap: { [address: string]: Token } = {}; + + hasConstantPriceLargeAmounts: boolean = false; + + constructor( + readonly network: Network, + readonly dexKey: string, + readonly dexHelper: IDexHelper, + readonly mainnetRFQAddress: string = CablesConfig['Cables'][network] + .mainnetRFQAddress, + protected rfqInterface = new Interface(mainnetRFQAbi), + ) { + super(dexHelper, dexKey); + this.logger = dexHelper.getLogger(dexKey); + + this.rateFetcher = new CablesRateFetcher( + this.dexHelper, + this.dexKey, + this.network, + this.logger, + { + rateConfig: { + pairsReqParams: { + url: CABLES_API_URL + '/pairs', + }, + pricesReqParams: { + url: CABLES_API_URL + '/prices', + }, + blacklistReqParams: { + url: CABLES_API_URL + '/blacklist', + }, + tokensReqParams: { + url: CABLES_API_URL + '/tokens', + }, + + pricesIntervalMs: CABLES_API_PRICES_POLLING_INTERVAL_MS, + pricesCacheTTLSecs: CABLES_PRICES_CACHES_TTL_S, + pricesCacheKey: 'prices', + + pairsIntervalMs: CABLES_API_PAIRS_POLLING_INTERVAL_MS, + pairsCacheTTLSecs: CABLES_PAIRS_CACHES_TTL_S, + pairsCacheKey: 'pairs', + + tokensIntervalMs: CABLES_API_TOKENS_POLLING_INTERVAL_MS, + tokensCacheTTLSecs: CABLES_TOKENS_CACHES_TTL_S, + tokensCacheKey: 'tokens', + + blacklistIntervalMs: CABLES_API_BLACKLIST_POLLING_INTERVAL_MS, + blacklistCacheTTLSecs: CABLES_BLACKLIST_CACHES_TTL_S, + blacklistCacheKey: 'blacklist', + }, + }, + ); + } + + async preProcessTransaction?( + optimalSwapExchange: OptimalSwapExchange, + srcToken: Token, + destToken: Token, + side: SwapSide, + options: PreprocessTransactionOptions, + ): Promise<[OptimalSwapExchange, ExchangeTxInfo]> { + if (await this.isBlacklisted(options.txOrigin)) { + this.logger.warn( + `${this.dexKey}-${this.network}: blacklisted TX Origin address '${options.txOrigin}' trying to build a transaction. Bailing...`, + ); + throw new Error( + `${this.dexKey}-${ + this.network + }: user=${options.txOrigin.toLowerCase()} is blacklisted`, + ); + } + + if (BigInt(optimalSwapExchange.srcAmount) === 0n) { + throw new Error('getFirmRate failed with srcAmount === 0'); + } + + const normalizedSrcToken = this.normalizeToken(srcToken); + const normalizedDestToken = this.normalizeToken(destToken); + const swapIdentifier = `${this.dexKey}_${normalizedSrcToken.address}_${normalizedDestToken.address}_${side}`; + + try { + let makerToken = normalizedDestToken; + let takerToken = normalizedSrcToken; + + const isSell = side === SwapSide.SELL; + const isBuy = side === SwapSide.BUY; + + const rfqParams = { + makerAsset: ethers.utils.getAddress(makerToken.address), + takerAsset: ethers.utils.getAddress(takerToken.address), + ...(isBuy && { makerAmount: optimalSwapExchange.destAmount }), + ...(isSell && { takerAmount: optimalSwapExchange.srcAmount }), + userAddress: options.executionContractAddress, + chainId: String(this.network), + }; + + const rfq: CablesRFQResponse = await this.dexHelper.httpRequest.post( + `${CABLES_API_URL}/quote`, + rfqParams, + CABLES_FIRM_QUOTE_TIMEOUT_MS, + ); + + if (!rfq) { + throw new Error( + 'Failed to fetch RFQ' + + swapIdentifier + + JSON.stringify(rfq + 'params' + rfqParams), + ); + } + + const { order } = rfq; + + assert( + order.makerAsset.toLowerCase() === makerToken.address, + `QuoteData makerAsset=${order.makerAsset} is different from Paraswap makerAsset=${makerToken.address}`, + ); + assert( + order.takerAsset.toLowerCase() === takerToken.address, + `QuoteData takerAsset=${order.takerAsset} is different from Paraswap takerAsset=${takerToken.address}`, + ); + if (isSell) { + assert( + order.takerAmount === optimalSwapExchange.srcAmount, + `QuoteData takerAmount=${order.takerAmount} is different from Paraswap srcAmount=${optimalSwapExchange.srcAmount}`, + ); + } else { + assert( + order.makerAmount === optimalSwapExchange.destAmount, + `QuoteData makerAmount=${order.makerAmount} is different from Paraswap destAmount=${optimalSwapExchange.destAmount}`, + ); + } + + const expiryAsBigInt = BigInt(order.expiry); + const minDeadline = expiryAsBigInt > 0 ? expiryAsBigInt : BI_MAX_UINT256; + + if (side === SwapSide.BUY) { + const requiredAmount = BigInt(optimalSwapExchange.srcAmount); + const quoteAmount = BigInt(order.takerAmount); + const requiredAmountWithSlippage = new BigNumber( + requiredAmount.toString(), + ) + .multipliedBy(options.slippageFactor) + .toFixed(0); + if (quoteAmount > BigInt(requiredAmountWithSlippage)) { + throw new SlippageError( + `Slipped, factor: ${quoteAmount.toString()} > ${requiredAmountWithSlippage}`, + ); + } + } else { + const requiredAmount = BigInt(optimalSwapExchange.destAmount); + const quoteAmount = BigInt(order.makerAmount); + const requiredAmountWithSlippage = new BigNumber( + requiredAmount.toString(), + ) + .multipliedBy(options.slippageFactor) + .toFixed(0); + if (quoteAmount < BigInt(requiredAmountWithSlippage)) { + throw new SlippageError( + `Slipped, factor: ${ + options.slippageFactor + } ${quoteAmount.toString()} < ${requiredAmountWithSlippage}`, + ); + } + } + + return [ + { + ...optimalSwapExchange, + data: { + quoteData: order, + }, + }, + { deadline: minDeadline }, + ]; + } catch (e: any) { + const message = `${this.dexKey}-${this.network}: ${e}`; + this.logger.error(message); + if (!e?.isSlippageError) { + this.restrict(); + } + throw new Error(message); + } + } + + getAdapterParam( + srcToken: string, + destToken: string, + srcAmount: string, + destAmount: string, + data: BebopData, + side: SwapSide, + ): AdapterExchangeParam { + return { + targetExchange: this.mainnetRFQAddress, + payload: '0x', + networkFee: '0', + }; + } + + getDexParam( + srcToken: Address, + destToken: Address, + srcAmount: NumberAsString, + destAmount: NumberAsString, + recipient: Address, + data: CablesData, + side: SwapSide, + ): DexExchangeParam { + const { quoteData } = data; + + assert( + quoteData !== undefined, + `${this.dexKey}-${this.network}: quoteData undefined`, + ); + + const swapFunction = 'partialSwap'; + const swapFunctionParams = [ + [ + quoteData.nonceAndMeta, + quoteData.expiry, + quoteData.makerAsset, + quoteData.takerAsset, + quoteData.maker, + quoteData.taker, + quoteData.makerAmount, + quoteData.takerAmount, + ], + quoteData.signature, + // might be overwritten on Executors + quoteData.takerAmount, + ]; + + const exchangeData = this.rfqInterface.encodeFunctionData( + swapFunction, + swapFunctionParams, + ); + + const fromAmount = ethers.utils.defaultAbiCoder.encode( + ['uint256'], + [quoteData.takerAmount], + ); + + const filledAmountIndex = exchangeData + .replace('0x', '') + .lastIndexOf(fromAmount.replace('0x', '')); + + const filledAmountPos = + (filledAmountIndex !== -1 ? filledAmountIndex : exchangeData.length) / 2; + + return { + exchangeData, + needWrapNative: this.needWrapNative, + dexFuncHasRecipient: false, + targetExchange: this.mainnetRFQAddress, + returnAmountPos: undefined, + insertFromAmountPos: filledAmountPos, + }; + } + + normalizeToken(token: Token): Token { + return { + ...token, + address: this.normalizeTokenAddress(token.address), + }; + } + + normalizeTokenAddress(address: Address): Address { + return address.toLowerCase(); + } + + getTokenFromAddress(address: Address): Token { + return this.tokensMap[this.normalizeAddress(address)]; + } + + getPoolIdentifier(srcAddress: Address, destAddress: Address) { + return `${this.dexKey}_${srcAddress}_${destAddress}`.toLowerCase(); + } + + async getPoolIdentifiers( + srcToken: Token, + destToken: Token, + side: SwapSide, + blockNumber: number, + ): Promise { + if (!srcToken || !destToken) { + return []; + } + + if (srcToken.address.toLowerCase() === destToken.address.toLowerCase()) { + return []; + } + + const normalizedSrcToken = this.normalizeToken(srcToken); + const normalizedDestToken = this.normalizeToken(destToken); + + const pairData = await this.getPairData( + normalizedSrcToken, + normalizedDestToken, + ); + + if (!pairData) { + return []; + } + + return [ + this.getPoolIdentifier( + normalizedSrcToken.address, + normalizedDestToken.address, + ), + ]; + } + + calculateOrderPrice( + amounts: bigint[], + orderbook: string[][], + baseToken: Token, + quoteToken: Token, + isInputQuote: boolean, + ) { + let result = []; + + for (let i = 0; i < amounts.length; i++) { + let amt = amounts[i]; + if (amt === 0n) { + result.push(amt); + continue; + } + + let decimals = baseToken.decimals; + let out_decimals = quoteToken.decimals; + + let price = this.calculatePriceSwap( + orderbook, + Number(amt) / 10 ** decimals, + isInputQuote, + ); + result.push(BigInt(Math.round(price * 10 ** out_decimals))); + } + return result; + } + + calculatePriceSwap( + prices: string[][], + requiredQty: number, + qtyMode: Boolean, + ) { + let sumBaseQty = 0; + let sumQuoteQty = 0; + const selectedRows: string[][] = []; + + const isBase = qtyMode; + const isQuote = !qtyMode; + + for (const [price, volume] of prices) { + if (isBase) { + if (sumBaseQty >= requiredQty) { + break; + } + } + + if (isQuote) { + if (sumQuoteQty >= requiredQty) { + break; + } + } + + let currentBaseQty = Number(volume); + let currentQuoteQty = Number(volume) * Number(price); + + const overQty = isBase + ? currentBaseQty + sumBaseQty > requiredQty + : currentQuoteQty + sumQuoteQty > requiredQty; + + if (overQty) { + if (isBase) { + currentBaseQty = requiredQty - sumBaseQty; + currentQuoteQty = currentBaseQty * Number(price); + } + + if (isQuote) { + currentQuoteQty = requiredQty - sumQuoteQty; + currentBaseQty = + currentQuoteQty * + new BigNumber(1).dividedBy(new BigNumber(price)).toNumber(); + } + } + + sumBaseQty += currentBaseQty; + sumQuoteQty += currentQuoteQty; + selectedRows.push([price, currentBaseQty.toString()]); + } + + const vSumBase = selectedRows.reduce((sum: number, [price, volume]) => { + return sum + Number(price) * Number(volume); + }, 0); + + const price = new BigNumber(vSumBase) + .dividedBy(new BigNumber(sumBaseQty)) + .toNumber(); + + if (isBase) { + return requiredQty / price; + } else { + return requiredQty * price; + } + } + + async getPricesVolume( + srcToken: Token, + destToken: Token, + amounts: bigint[], + side: SwapSide, + blockNumber: number, + limitPools?: string[], + transferFees?: TransferFeeParams, + isFirstSwap?: boolean, + ): Promise | null> { + try { + const normalizedSrcToken = this.normalizeToken(srcToken); + const normalizedDestToken = this.normalizeToken(destToken); + // If: same token, return null + if ( + normalizedSrcToken.address.toLowerCase() === + normalizedDestToken.address.toLowerCase() + ) { + return null; + } + + const isRestricted = await this.isRestricted(); + if (isRestricted) { + return null; + } + + await this.setTokensMap(); + + normalizedSrcToken.symbol = + this.tokensMap[normalizedSrcToken.address].symbol; + normalizedDestToken.symbol = + this.tokensMap[normalizedDestToken.address!].symbol; + + // ---------- Pools ---------- + let pools = + limitPools || + (await this.getPoolIdentifiers(srcToken, destToken, side, blockNumber)); + if (pools.length === 0) return null; + + // ---------- Prices ---------- + const priceMap = await this.getCachedPrices(); + + if (!priceMap) return null; + + let isInputQuote = false; + let pairKey = `${normalizedSrcToken.symbol}/${normalizedDestToken.symbol}`; + const pairsKeys = Object.keys(priceMap); + + if (!pairsKeys.includes(pairKey)) { + // Revert + isInputQuote = true; + pairKey = `${normalizedDestToken.symbol}/${normalizedSrcToken.symbol}`; + if (!pairsKeys.includes(pairKey)) { + return null; + } + } + + /** + * Orderbook + */ + const priceData = priceMap[pairKey]; + + let orderbook: any[] = []; + if (side === SwapSide.BUY) { + orderbook = priceData.asks; + } else { + orderbook = priceData.bids; + } + if (orderbook?.length === 0) { + throw new Error(`Empty orderbook for ${pairKey}`); + } + + const prices = this.calculateOrderPrice( + amounts, + orderbook, + side === SwapSide.SELL ? srcToken : destToken, + side === SwapSide.SELL ? destToken : srcToken, + side === SwapSide.SELL ? isInputQuote : !isInputQuote, + ); + + const result = [ + { + prices: prices, + unit: BigInt(normalizedDestToken.decimals), + exchange: this.dexKey, + gasCost: CABLES_GAS_COST, + poolAddresses: [this.mainnetRFQAddress], + data: {}, + }, + ]; + + return result; + } catch (e: unknown) { + this.logger.error( + `Error in getPricesVolume`, + { + srcToken: srcToken.address || srcToken.symbol, + destToken: destToken.address || destToken.symbol, + side, + }, + e, + ); + return null; + } + } + + getCalldataGasCost(poolPrices: PoolPrices): number | number[] { + return ( + CALLDATA_GAS_COST.DEX_OVERHEAD + + // addresses: makerAsset, takerAsset, maker, taker + CALLDATA_GAS_COST.ADDRESS * 4 + + // uint256: expiry + CALLDATA_GAS_COST.wordNonZeroBytes(16) + + // uint256: nonceAndMeta, makerAmount, takerAmount + CALLDATA_GAS_COST.AMOUNT * 3 + + // bytes: _signature (65 bytes) + CALLDATA_GAS_COST.FULL_WORD * 2 + + CALLDATA_GAS_COST.OFFSET_SMALL + ); + } + + async initializePricing(blockNumber: number): Promise { + if (!this.dexHelper.config.isSlave) { + this.rateFetcher.start(); + } + return; + } + + getAdapters(side: SwapSide): { name: string; index: number }[] | null { + return null; + } + + releaseResources?(): AsyncOrSync { + if (!this.dexHelper.config.isSlave && this.rateFetcher) { + this.rateFetcher.stop(); + } + } + + normalizeAddress(address: string): string { + return address.toLowerCase() === ETHER_ADDRESS + ? NULL_ADDRESS + : address.toLowerCase(); + } + + async setTokensMap() { + const tokens = await this.getCachedTokens(); + + if (tokens) { + this.tokensMap = Object.keys(tokens).reduce((acc, key) => { + //@ts-ignore + acc[tokens[key].address.toLowerCase()] = tokens[key]; + return acc; + }, {}); + } + } + + async getTopPoolsForToken( + tokenAddress: Address, + limit: number, + ): Promise { + return []; + } + + /** + * CACHED UTILS + */ + async getCachedTokens(): Promise { + const cachedTokens = await this.dexHelper.cache.get( + this.dexKey, + this.network, + this.rateFetcher.tokensCacheKey, + ); + + return cachedTokens ? JSON.parse(cachedTokens) : {}; + } + + async getCachedPairs(): Promise { + const cachedPairs = await this.dexHelper.cache.get( + this.dexKey, + this.network, + this.rateFetcher.pairsCacheKey, + ); + + return cachedPairs ? JSON.parse(cachedPairs) : {}; + } + + async getCachedPrices(): Promise { + const cachedPrices = await this.dexHelper.cache.get( + this.dexKey, + this.network, + this.rateFetcher.pricesCacheKey, + ); + + return cachedPrices ? JSON.parse(cachedPrices) : {}; + } + + async getCachedTokensAddr(): Promise { + const tokens = await this.getCachedTokens(); + const tokensAddr: Record = {}; + for (const key of Object.keys(tokens)) { + tokensAddr[tokens[key].symbol.toLowerCase()] = tokens[key].address; + } + return tokensAddr; + } + + getPairString(baseToken: Token, quoteToken: Token): string { + return `${baseToken.symbol}/${quoteToken.symbol}`.toLowerCase(); + } + + // Function to find a key by address + private findKeyByAddress = ( + jsonData: Record, + targetAddress: string, + ): string | undefined => { + const entries = Object.entries(jsonData); + const foundEntry = entries.find( + ([_, value]) => + value.address.toLowerCase() === targetAddress.toLowerCase(), + ); + return foundEntry ? foundEntry[0] : undefined; + }; + + async getPairData(srcToken: Token, destToken: Token): Promise { + if (srcToken.address === destToken.address) { + return null; + } + + const cachedTokens = await this.getCachedTokens(); + + srcToken.symbol = this.findKeyByAddress(cachedTokens, srcToken.address); + destToken.symbol = this.findKeyByAddress(cachedTokens, destToken.address); + + const cachedPairs = await this.getCachedPairs(); + + const potentialPairs = [ + { + base: srcToken.symbol, + quote: destToken.symbol, + identifier: this.getPairString(srcToken, destToken), + isSrcBase: true, + }, + { + base: destToken.symbol, + quote: srcToken.symbol, + identifier: this.getPairString(destToken, srcToken), + isSrcBase: false, + }, + ]; + + for (const pair of potentialPairs) { + if (pair.identifier in cachedPairs) { + const pairData = cachedPairs[pair.identifier]; + pairData.isSrcBase = pair.isSrcBase; + return pairData; + } + } + return null; + } + + async isBlacklisted(txOrigin: Address): Promise { + const cachedBlacklist = await this.dexHelper.cache.get( + this.dexKey, + this.network, + this.rateFetcher.blacklistCacheKey, + ); + + if (cachedBlacklist) { + const blacklist = JSON.parse(cachedBlacklist) as string[]; + return blacklist.includes(txOrigin.toLowerCase()); + } + + return false; + } + + async isRestricted(): Promise { + const result = await this.dexHelper.cache.get( + this.dexKey, + this.network, + CABLES_RESTRICTED_CACHE_KEY, + ); + + return result === 'true'; + } + + async restrict() { + const errorsDataRaw = await this.dexHelper.cache.get( + this.dexKey, + this.network, + CABLES_ERRORS_CACHE_KEY, + ); + + const errorsData: RestrictData = Utils.Parse(errorsDataRaw); + const ERRORS_TTL_S = Math.floor(CABLES_RESTRICT_CHECK_INTERVAL_MS / 1000); + + if ( + !errorsData || + errorsData?.addedDatetimeMs + CABLES_RESTRICT_CHECK_INTERVAL_MS < + Date.now() + ) { + this.logger.warn( + `${this.dexKey}-${this.network}: First encounter of error OR error ocurred outside of threshold, setting up counter`, + ); + const data: RestrictData = { + count: 1, + addedDatetimeMs: Date.now(), + }; + await this.dexHelper.cache.setex( + this.dexKey, + this.network, + CABLES_ERRORS_CACHE_KEY, + ERRORS_TTL_S, + Utils.Serialize(data), + ); + return; + } else { + if (errorsData.count + 1 >= CABLES_RESTRICT_COUNT_THRESHOLD) { + this.logger.warn( + `${this.dexKey}-${this.network}: Restricting due to error count=${ + errorsData.count + 1 + } within ${CABLES_RESTRICT_CHECK_INTERVAL_MS / 1000 / 60} minutes`, + ); + await this.dexHelper.cache.setex( + this.dexKey, + this.network, + CABLES_RESTRICTED_CACHE_KEY, + CABLES_RESTRICT_TTL_S, + 'true', + ); + } else { + this.logger.warn( + `${this.dexKey}-${this.network}: Error count increased`, + ); + const data: RestrictData = { + count: errorsData.count + 1, + addedDatetimeMs: errorsData.addedDatetimeMs, + }; + await this.dexHelper.cache.setex( + this.dexKey, + this.network, + CABLES_RESTRICTED_CACHE_KEY, + ERRORS_TTL_S, + Utils.Serialize(data), + ); + } + } + } +} diff --git a/src/dex/cables/config.ts b/src/dex/cables/config.ts new file mode 100644 index 000000000..07c4ee343 --- /dev/null +++ b/src/dex/cables/config.ts @@ -0,0 +1,13 @@ +import { Network } from '../../constants'; +import { DexConfigMap } from '../../types'; + +export const CablesConfig: DexConfigMap<{ mainnetRFQAddress: string }> = { + Cables: { + [Network.AVALANCHE]: { + mainnetRFQAddress: '0xfA12DCB2e1FD72bD92E8255Db6A781b2c76adC20', + }, + [Network.ARBITRUM]: { + mainnetRFQAddress: '0xfA12DCB2e1FD72bD92E8255Db6A781b2c76adC20', + }, + }, +}; diff --git a/src/dex/cables/constants.ts b/src/dex/cables/constants.ts new file mode 100644 index 000000000..cd2ec378d --- /dev/null +++ b/src/dex/cables/constants.ts @@ -0,0 +1,33 @@ +import BigNumber from 'bignumber.js'; + +/** + * Cables + */ +export const CABLES_API_URL = + 'https://cables-evm-rfq-service.cryptosrvc.com/v1'; + +export const CABLES_PRICES_CACHES_TTL_S = 10; +export const CABLES_API_PRICES_POLLING_INTERVAL_MS = 2000; // 2 sec + +export const CABLES_PAIRS_CACHES_TTL_S = 12; +export const CABLES_API_PAIRS_POLLING_INTERVAL_MS = 10000; // 10 sec + +export const CABLES_BLACKLIST_CACHES_TTL_S = 60; +export const CABLES_API_BLACKLIST_POLLING_INTERVAL_MS = 30000; // 30 sec + +export const CABLES_TOKENS_CACHES_TTL_S = 60; +export const CABLES_API_TOKENS_POLLING_INTERVAL_MS = 30000; // 30 sec + +export const CABLES_FIRM_QUOTE_TIMEOUT_MS = 2000; + +export const CABLES_RESTRICTED_CACHE_KEY = 'restricted'; + +export const CABLES_ERRORS_CACHE_KEY = 'errors'; + +export const CABLES_RESTRICT_CHECK_INTERVAL_MS = 1000 * 60 * 3; // 3 min + +export const CABLES_RESTRICT_COUNT_THRESHOLD = 3; + +export const CABLES_RESTRICT_TTL_S = 10 * 60; // 10 min + +export const CABLES_GAS_COST = 120_000; diff --git a/src/dex/cables/rate-fetcher.ts b/src/dex/cables/rate-fetcher.ts new file mode 100644 index 000000000..3df0b0f83 --- /dev/null +++ b/src/dex/cables/rate-fetcher.ts @@ -0,0 +1,212 @@ +import { Network } from '../../constants'; +import { IDexHelper } from '../../dex-helper'; +import { Fetcher } from '../../lib/fetcher/fetcher'; +import { validateAndCast } from '../../lib/validators'; +import { Logger, Token } from '../../types'; +import { PairData } from '../cables/types'; +import { + CablesBlacklistResponse, + CablesPairsResponse, + CablesPricesResponse, + CablesRateFetcherConfig, + CablesTokensResponse, +} from './types'; +import { + blacklistResponseValidator, + pairsResponseValidator, + pricesResponseValidator, + tokensResponseValidator, +} from './validators'; + +export class CablesRateFetcher { + public tokensFetcher: Fetcher; + public tokensCacheKey: string; + public tokensCacheTTL: number; + + public pairsFetcher: Fetcher; + public pairsCacheKey: string; + public pairsCacheTTL: number; + + public pricesFetcher: Fetcher; + public pricesCacheKey: string; + public pricesCacheTTL: number; + + public blacklistFetcher: Fetcher; + public blacklistCacheKey: string; + public blacklistCacheTTL: number; + + constructor( + private dexHelper: IDexHelper, + private dexKey: string, + private network: Network, + private logger: Logger, + config: CablesRateFetcherConfig, + ) { + this.tokensCacheKey = config.rateConfig.tokensCacheKey; + this.tokensCacheTTL = config.rateConfig.tokensCacheTTLSecs; + + this.pairsCacheKey = config.rateConfig.pairsCacheKey; + this.pairsCacheTTL = config.rateConfig.pairsCacheTTLSecs; + + this.pricesCacheKey = config.rateConfig.pricesCacheKey; + this.pricesCacheTTL = config.rateConfig.pricesCacheTTLSecs; + + this.blacklistCacheKey = config.rateConfig.blacklistCacheKey; + this.blacklistCacheTTL = config.rateConfig.blacklistCacheTTLSecs; + + this.pairsFetcher = new Fetcher( + dexHelper.httpRequest, + { + info: { + requestOptions: config.rateConfig.pairsReqParams, + caster: (data: unknown) => { + return validateAndCast( + data, + pairsResponseValidator, + ); + }, + }, + handler: this.handlePairsResponse.bind(this), + }, + config.rateConfig.pairsIntervalMs, + logger, + ); + + this.pricesFetcher = new Fetcher( + dexHelper.httpRequest, + { + info: { + requestOptions: config.rateConfig.pricesReqParams, + caster: (data: unknown) => { + return validateAndCast( + data, + pricesResponseValidator, + ); + }, + }, + handler: this.handlePricesResponse.bind(this), + }, + config.rateConfig.pricesIntervalMs, + logger, + ); + + this.blacklistFetcher = new Fetcher( + dexHelper.httpRequest, + { + info: { + requestOptions: config.rateConfig.blacklistReqParams, + caster: (data: unknown) => { + return validateAndCast( + data, + blacklistResponseValidator, + ); + }, + }, + handler: this.handleBlacklistResponse.bind(this), + }, + config.rateConfig.blacklistIntervalMs, + logger, + ); + + this.tokensFetcher = new Fetcher( + dexHelper.httpRequest, + { + info: { + requestOptions: config.rateConfig.tokensReqParams, + caster: (data: unknown) => { + return validateAndCast( + data, + tokensResponseValidator, + ); + }, + }, + handler: this.handleTokensResponse.bind(this), + }, + config.rateConfig.tokensIntervalMs, + logger, + ); + } + + /** + * Utils + */ + start() { + this.pairsFetcher.startPolling(); + this.pricesFetcher.startPolling(); + this.blacklistFetcher.startPolling(); + this.tokensFetcher.startPolling(); + } + stop() { + this.pairsFetcher.stopPolling(); + this.pricesFetcher.stopPolling(); + this.blacklistFetcher.stopPolling(); + this.tokensFetcher.stopPolling(); + } + + private handlePairsResponse(res: CablesPairsResponse): void { + const networkId = String(this.network); + const pairs = res.pairs[networkId]; + + let normalized_pairs: { [token: string]: PairData } = {}; + Object.keys(pairs).forEach(key => { + normalized_pairs[key.toLowerCase()] = pairs[key]; + }); + + this.dexHelper.cache.setex( + this.dexKey, + this.network, + this.pairsCacheKey, + this.pairsCacheTTL, + JSON.stringify(normalized_pairs), + ); + } + + private handlePricesResponse(res: CablesPricesResponse): void { + const networkId = String(this.network); + const prices = res.prices[networkId]; + + this.dexHelper.cache.setex( + this.dexKey, + this.network, + this.pricesCacheKey, + this.pricesCacheTTL, + JSON.stringify(prices), + ); + } + + private handleBlacklistResponse(res: CablesBlacklistResponse): void { + const { blacklist } = res; + this.dexHelper.cache.setex( + this.dexKey, + this.network, + this.blacklistCacheKey, + this.blacklistCacheTTL, + JSON.stringify(blacklist.map(item => item.toLowerCase())), + ); + } + + // Convert addresses to lowercase + private normalizeAddressesToLowerCase = ( + jsonData: Record, + ) => { + Object.keys(jsonData).forEach(key => { + jsonData[key].address = jsonData[key].address.toLowerCase(); + }); + return jsonData; + }; + + private async handleTokensResponse(res: CablesTokensResponse): Promise { + const networkId = String(this.network); + const tokens = res.tokens[networkId]; + + const normalizedTokens = this.normalizeAddressesToLowerCase(tokens); + + this.dexHelper.cache.setex( + this.dexKey, + this.network, + this.tokensCacheKey, + this.tokensCacheTTL, + JSON.stringify(normalizedTokens), + ); + } +} diff --git a/src/dex/cables/types.ts b/src/dex/cables/types.ts new file mode 100644 index 000000000..b8f5d14de --- /dev/null +++ b/src/dex/cables/types.ts @@ -0,0 +1,113 @@ +import { RequestHeaders } from '../../dex-helper'; +import { Token } from '../../types'; +import { Method } from '../../dex-helper/irequest-wrapper'; +import { AugustusRFQOrderData } from '../augustus-rfq'; + +export type CablesRFQResponse = { + order: AugustusRFQOrderData; + signature: string; +}; + +export type CablesData = { + quoteData?: AugustusRFQOrderData; +}; +/** + * Types + */ +export type PairData = { + base: string; + quote: string; + liquidityUSD: number; +}; + +type PriceAndAmount = [string, string]; + +type PriceData = { + bids: PriceAndAmount[]; + asks: PriceAndAmount[]; +}; + +type PriceDataMap = { + [network: string]: { + [pair: string]: PriceData; + }; +}; + +type TokenDataMap = { + [network: string]: { + [token: string]: Token; + }; +}; + +type PairsDataMap = { + [network: string]: { + [token: string]: PairData; + }; +}; + +/** + * Responses + */ +export type CablesPricesResponse = { + prices: PriceDataMap; +}; +export type CablesBlacklistResponse = { + blacklist: string[]; +}; +export type CablesTokensResponse = { + tokens: TokenDataMap; +}; +export type CablesPairsResponse = { + pairs: PairsDataMap; +}; + +/** + * Rate Fetcher + */ +export type CablesRateFetcherConfig = { + rateConfig: { + pairsReqParams: { + url: string; + headers?: RequestHeaders; + params?: any; + }; + pricesReqParams: { + url: string; + headers?: RequestHeaders; + params?: any; + }; + blacklistReqParams: { + url: string; + headers?: RequestHeaders; + params?: any; + }; + tokensReqParams: { + url: string; + headers?: RequestHeaders; + params?: any; + }; + pairsIntervalMs: number; + pricesIntervalMs: number; + blacklistIntervalMs: number; + tokensIntervalMs: number; + + pairsCacheKey: string; + pricesCacheKey: string; + blacklistCacheKey: string; + tokensCacheKey: string; + + blacklistCacheTTLSecs: number; + pairsCacheTTLSecs: number; + pricesCacheTTLSecs: number; + tokensCacheTTLSecs: number; + }; +}; + +export type RestrictData = { + count: number; + addedDatetimeMs: number; +} | null; + +export class SlippageError extends Error { + isSlippageError = true; +} diff --git a/src/dex/cables/validators.test.ts b/src/dex/cables/validators.test.ts new file mode 100644 index 000000000..abc36cf82 --- /dev/null +++ b/src/dex/cables/validators.test.ts @@ -0,0 +1,151 @@ +import Joi from 'joi'; +import { + pairsResponseValidator, + pricesResponseValidator, + tokensResponseValidator, + blacklistResponseValidator, +} from './validators'; // + +describe('Validation Schemas', () => { + describe('pairsResponseValidator', () => { + it('should validate correct pairs response', () => { + const validData = { + pairs: { '43114': { 'USDC/USDT': { base: 'USDC', quote: 'USDT' } } }, + }; + const { error } = pairsResponseValidator.validate(validData); + expect(error).toBeUndefined(); + }); + + it('should invalidate incorrect pairs response', () => { + const invalidData = { pairs: { '43114': 'USDC/USDT' } }; + const { error } = pairsResponseValidator.validate(invalidData); + expect(error).toBeDefined(); + }); + }); + + describe('pricesResponseValidator', () => { + it('should validate correct prices response', () => { + const validData = { + prices: { + '43114': { + 'USDC/USDT': { + bids: [ + ['0.9996', '244305.9'], + ['0.9995', '236021.6'], + ], + asks: [ + ['0.9996', '244305.9'], + ['0.9995', '236021.6'], + ], + }, + }, + }, + }; + const { error } = pricesResponseValidator.validate(validData); + expect(error).toBeUndefined(); + }); + + it('should invalidate incorrect prices response', () => { + const invalidData = { + prices: { + chain1: { + bids: [ + ['1000'], // invalid entry length + ], + asks: [['1010', '1']], + }, + }, + }; + const { error } = pricesResponseValidator.validate(invalidData); + expect(error).toBeDefined(); + }); + }); + + describe('tokensResponseValidator', () => { + it('should validate correct tokens response', () => { + const validData = { + tokens: { + '43114': { + AVAX: { + symbol: 'AVAX', + decimals: 18, + name: 'AVAX', + address: '0x0000000000000000000000000000000000000000', + }, + WAVAX: { + symbol: 'WAVAX', + decimals: 18, + name: 'WAVAX', + address: '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7', + }, + 'WETH.e': { + symbol: 'WETH.e', + decimals: 18, + name: 'WETH.e', + address: '0x49D5c2BdFfac6CE2BFdB6640F4F80f226bc10bAB', + }, + USDT: { + symbol: 'USDT', + decimals: 6, + name: 'USDT', + address: '0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7', + }, + USDC: { + symbol: 'USDC', + decimals: 6, + name: 'USDC', + address: '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', + }, + 'USDC.e': { + symbol: 'USDC.e', + decimals: 6, + name: 'USDC.e', + address: '0xA7D7079b0FEaD91F3e65f86E8915Cb59c1a4C664', + }, + }, + }, + }; + const { error } = tokensResponseValidator.validate(validData); + expect(error).toBeUndefined(); + }); + + it('should invalidate incorrect tokens response', () => { + const invalidData = { + tokens: { + chain1: { + ETH: { + symbol: '', // invalid value + name: 'Ethereum', + description: 'A popular cryptocurrency', + address: '0x...', + decimals: 18, + type: 'ERC20', + }, + }, + }, + }; + const { error } = tokensResponseValidator.validate(invalidData); + expect(error).toBeDefined(); + }); + }); + + describe('blacklistResponseValidator', () => { + it('should validate correct blacklist response', () => { + const validData = { + blacklist: ['0xAddress1', '0xAddress2'], + }; + const { error } = blacklistResponseValidator.validate(validData); + expect(error).toBeUndefined(); + }); + + it('should invalidate incorrect blacklist response', () => { + const invalidData = { + blacklist: [ + '', // invalid value + ], + }; + const { error } = blacklistResponseValidator.validate(invalidData); + expect(error).toBeDefined(); + }); + }); +}); diff --git a/src/dex/cables/validators.ts b/src/dex/cables/validators.ts new file mode 100644 index 000000000..d95c65341 --- /dev/null +++ b/src/dex/cables/validators.ts @@ -0,0 +1,61 @@ +import joi from 'joi'; + +const pairValidator = joi.object({ + base: joi.string().min(1), + quote: joi.string().min(1), + liquidityUSD: joi.number().min(0), + baseAddress: joi.string().min(1), + quoteAddress: joi.string().min(1), + baseDecimals: joi.number().min(0), + quoteDecimals: joi.number().min(0), +}); + +const pairMap = joi.object().pattern( + joi.string(), // Pair name ETH/USDT + pairValidator, +); + +export const pairsResponseValidator = joi.object({ + pairs: joi.object().pattern( + joi.string(), // chain id + pairMap, + ), +}); + +const orderbookEntry = joi.array().items(joi.string().min(1)).length(2); + +const orderbookValidator = joi.object({ + bids: joi.array().items(orderbookEntry), + asks: joi.array().items(orderbookEntry), +}); + +const chainDataSchema = joi.object().pattern( + joi.string(), // pair name USDC/USDT + orderbookValidator, +); + +export const pricesResponseValidator = joi.object({ + prices: joi.object().pattern( + joi.string(), // chain id + chainDataSchema, + ), +}); + +const tokenValidator = joi.object({ + symbol: joi.string().min(1), + name: joi.string().min(1), + description: joi.string().min(1), + address: joi.string().min(1), + decimals: joi.number().min(0), + type: joi.string().min(1), +}); + +const chainTokens = joi.object().pattern(joi.string(), tokenValidator); + +export const tokensResponseValidator = joi.object({ + tokens: joi.object().pattern(joi.string(), chainTokens), +}); + +export const blacklistResponseValidator = joi.object({ + blacklist: joi.array().items(joi.string().min(1)), +}); diff --git a/src/dex/dexalot/dexalot.ts b/src/dex/dexalot/dexalot.ts index c27f7147a..6fda285df 100644 --- a/src/dex/dexalot/dexalot.ts +++ b/src/dex/dexalot/dexalot.ts @@ -593,6 +593,7 @@ export class Dexalot extends SimpleExchange implements IDex { .multipliedBy(10000) .toFixed(0) : options.slippageFactor.minus(1).multipliedBy(10000).toFixed(0); + const rfqParams = { makerAsset: ethers.utils.getAddress(makerToken.address), takerAsset: ethers.utils.getAddress(takerToken.address), diff --git a/src/dex/fluid-dex/constants.ts b/src/dex/fluid-dex/constants.ts new file mode 100644 index 000000000..da6d99534 --- /dev/null +++ b/src/dex/fluid-dex/constants.ts @@ -0,0 +1 @@ +export const MIN_SWAP_LIQUIDITY = 10n ** 4n; diff --git a/src/dex/fluid-dex/fluid-dex-e2e.test.ts b/src/dex/fluid-dex/fluid-dex-e2e.test.ts index a6873d07d..333aed44d 100644 --- a/src/dex/fluid-dex/fluid-dex-e2e.test.ts +++ b/src/dex/fluid-dex/fluid-dex-e2e.test.ts @@ -118,6 +118,23 @@ describe('FluidDex E2E', () => { describe('Mainnet', () => { const network = Network.MAINNET; + describe('ETH -> INST', () => { + const tokenASymbol: string = 'ETH'; + const tokenBSymbol: string = 'INST'; + + const tokenAAmount: string = '100000000000000'; + const tokenBAmount: string = '100000000000000'; + + testForNetwork( + network, + dexKey, + tokenASymbol, + tokenBSymbol, + tokenAAmount, + tokenBAmount, + ); + }); + describe('ETH -> wstETH', () => { const tokenASymbol: string = 'wstETH'; const tokenBSymbol: string = 'ETH'; diff --git a/src/dex/fluid-dex/fluid-dex-events.test.ts b/src/dex/fluid-dex/fluid-dex-events.test.ts index 19d6a89d7..f5bad501d 100644 --- a/src/dex/fluid-dex/fluid-dex-events.test.ts +++ b/src/dex/fluid-dex/fluid-dex-events.test.ts @@ -10,14 +10,16 @@ import { FluidDexLiquidityProxyState } from './types'; import { FluidDexConfig } from './config'; import { FluidDexLiquidityProxy } from './fluid-dex-liquidity-proxy'; import { FluidDexFactory } from './fluid-dex-factory'; +import { FluidDexEventPool } from './fluid-dex-pool'; +import { StatefulEventSubscriber } from '../../stateful-event-subscriber'; jest.setTimeout(50 * 1000); -async function fetchLiquidityProxyState( - liquidityProxy: FluidDexLiquidityProxy, +async function fetchState( + statefulEventSubscriber: StatefulEventSubscriber, blockNumber: number, -): Promise { - return liquidityProxy.generateState(blockNumber); +): Promise { + return statefulEventSubscriber.generateState(blockNumber); } // eventName -> blockNumbers @@ -64,7 +66,7 @@ describe('FluidDex EventPool Mainnet', function () { liquidityProxy, liquidityProxy.addressesSubscribed, (_blockNumber: number) => - fetchLiquidityProxyState(liquidityProxy, _blockNumber), + fetchState(liquidityProxy, _blockNumber), blockNumber, `${dexKey}_${poolAddress}`, dexHelper.provider, @@ -126,4 +128,52 @@ describe('FluidDex EventPool Mainnet', function () { }, ); }); + + describe('Pool events', () => { + let dexPool: FluidDexEventPool; + + const eventsToTest: Record = { + '0x8710039D5de6840EdE452A85672B32270a709aE2': { + LogPauseSwapAndArbitrage: [21337128], + }, + '0x2886a01a0645390872a9eb99dae1283664b0c524': { + LogPauseSwapAndArbitrage: [21374547], + }, + }; + + Object.entries(eventsToTest).forEach( + ([poolAddress, events]: [string, EventMappings]) => { + describe(`Events for ${poolAddress}`, () => { + beforeEach(() => { + dexPool = new FluidDexEventPool( + dexKey, + poolAddress, + network, + dexHelper, + logger, + ); + }); + Object.entries(events).forEach( + ([eventName, blockNumbers]: [string, number[]]) => { + describe(`${eventName}`, () => { + blockNumbers.forEach((blockNumber: number) => { + it(`State after ${blockNumber}`, async function () { + await testEventSubscriber( + dexPool, + dexPool.addressesSubscribed, + (_blockNumber: number) => + fetchState(dexPool, _blockNumber), + blockNumber, + `${dexKey}_${poolAddress}`, + dexHelper.provider, + ); + }); + }); + }); + }, + ); + }); + }, + ); + }); }); diff --git a/src/dex/fluid-dex/fluid-dex-liquidity-proxy.ts b/src/dex/fluid-dex/fluid-dex-liquidity-proxy.ts index ae61daa84..f6f7b890b 100644 --- a/src/dex/fluid-dex/fluid-dex-liquidity-proxy.ts +++ b/src/dex/fluid-dex/fluid-dex-liquidity-proxy.ts @@ -28,7 +28,7 @@ export class FluidDexLiquidityProxy extends StatefulEventSubscriber { + handlers: { + [event: string]: ( + event: any, + state: DeepReadonly, + log: Readonly, + ) => DeepReadonly | null; + } = {}; + + logDecoder: (log: Log) => any; + addressesSubscribed: Address[]; + protected poolIface = new Interface(FluidDexPoolABI); + + constructor( + readonly parentName: string, + readonly poolAddress: string, + protected network: number, + protected dexHelper: IDexHelper, + logger: Logger, + ) { + super(parentName, 'pool', dexHelper, logger); + + this.logDecoder = (log: Log) => this.poolIface.parseLog(log); + this.addressesSubscribed = [poolAddress]; + + // Add handlers + this.handlers['LogPauseSwapAndArbitrage'] = + this.handleLogPauseSwapAndArbitrage.bind(this); + this.handlers['LogUnpauseSwapAndArbitrage'] = + this.handleLogUnpauseSwapAndArbitrage.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 + */ + async processLog( + state: DeepReadonly, + log: Readonly, + ): Promise | null> { + try { + let event; + try { + event = this.logDecoder(log); + } catch (e) { + return null; + } + + if (event.name in this.handlers) { + return this.handlers[event.name](event, state, log); + } + } catch (e) { + catchParseLogError(e, this.logger); + } + + return null; + } + + handleLogPauseSwapAndArbitrage(): PoolState { + return { isSwapAndArbitragePaused: true }; + } + + handleLogUnpauseSwapAndArbitrage(): PoolState { + return { isSwapAndArbitragePaused: false }; + } + + async getStateOrGenerate( + blockNumber: number, + readonly: boolean = false, + ): Promise> { + let state = this.getState(blockNumber); + if (!state) { + state = await this.generateState(blockNumber); + if (!readonly) this.setState(state, blockNumber); + } + return state; + } + + /** + * 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 multicallData = [ + { + target: this.addressesSubscribed[0], + callData: this.poolIface.encodeFunctionData('readFromStorage', [ + hexZeroPad(hexlify(1), 32), + ]), + decodeFunction: uint256ToBigInt, + }, + ]; + + const storageResults = await this.dexHelper.multiWrapper.tryAggregate< + bigint | DecodedStateMultiCallResultWithRelativeBitmaps + >( + false, + multicallData, + blockNumber, + this.dexHelper.multiWrapper.defaultBatchSize, + false, + ); + + const isSwapAndArbitragePaused = + BigInt(storageResults[0].returnData.toString()) >> 255n === 1n; + + return { isSwapAndArbitragePaused }; + } +} diff --git a/src/dex/fluid-dex/fluid-dex.ts b/src/dex/fluid-dex/fluid-dex.ts index e8ea87ad6..fa3284ea4 100644 --- a/src/dex/fluid-dex/fluid-dex.ts +++ b/src/dex/fluid-dex/fluid-dex.ts @@ -34,6 +34,8 @@ import { generalDecoder } from '../../lib/decoders'; import { BigNumber } from 'ethers'; import { sqrt } from './utils'; import { FluidDexLiquidityProxy } from './fluid-dex-liquidity-proxy'; +import { FluidDexEventPool } from './fluid-dex-pool'; +import { MIN_SWAP_LIQUIDITY } from './constants'; export class FluidDex extends SimpleExchange implements IDex { readonly hasConstantPriceLargeAmounts = false; @@ -46,14 +48,14 @@ export class FluidDex extends SimpleExchange implements IDex { pools: FluidDexPool[] = []; + eventPools: FluidDexEventPool[] = []; + readonly factory: FluidDexFactory; readonly liquidityProxy: FluidDexLiquidityProxy; readonly fluidDexPoolIface: Interface; - FEE_100_PERCENT = BigInt(1000000); - constructor( readonly network: Network, readonly dexKey: string, @@ -110,6 +112,20 @@ export class FluidDex extends SimpleExchange implements IDex { await this.factory.initialize(blockNumber); this.pools = await this.fetchFluidDexPools(blockNumber); + this.eventPools = await Promise.all( + this.pools.map(async pool => { + const eventPool = new FluidDexEventPool( + this.dexKey, + pool.address, + this.network, + this.dexHelper, + this.logger, + ); + await eventPool.initialize(blockNumber); + return eventPool; + }), + ); + await this.liquidityProxy.initialize(blockNumber); } @@ -147,29 +163,24 @@ export class FluidDex extends SimpleExchange implements IDex { side: SwapSide, blockNumber: number, ): Promise { - const pool = this.getPoolByTokenPair(srcToken.address, destToken.address); - return pool ? [pool.id] : []; + const pools = this.getPoolsByTokenPair(srcToken.address, destToken.address); + return pools.map(pool => pool.id); } - getPoolByTokenPair( - srcToken: Address, - destToken: Address, - ): FluidDexPool | null { + getPoolsByTokenPair(srcToken: Address, destToken: Address): FluidDexPool[] { const srcAddress = srcToken.toLowerCase(); const destAddress = destToken.toLowerCase(); // A pair must have 2 different tokens. - if (srcAddress === destAddress) return null; + if (srcAddress === destAddress) return []; - for (const pool of this.pools) { - if ( + const pools = this.pools.filter( + pool => (srcAddress === pool.token0 && destAddress === pool.token1) || - (srcAddress === pool.token1 && destAddress === pool.token0) - ) { - return pool; - } - } - return null; + (srcAddress === pool.token1 && destAddress === pool.token0), + ); + + return pools; } // Returns pool prices for amounts. @@ -187,61 +198,93 @@ export class FluidDex extends SimpleExchange implements IDex { try { if (srcToken.address.toLowerCase() === destToken.address.toLowerCase()) return null; - // Get the pool to use. - const pool = this.getPoolByTokenPair(srcToken.address, destToken.address); - if (!pool) return null; - // Make sure the pool meets the optional limitPools filter. - if (limitPools && !limitPools.includes(pool.id)) return null; + + // Get the pools to use. + let pools = this.getPoolsByTokenPair(srcToken.address, destToken.address); + + if (limitPools) { + pools = pools.filter(pool => limitPools.includes(pool.id)); + } + + if (!pools.length) return null; const liquidityProxyState = await this.liquidityProxy.getStateOrGenerate( blockNumber, ); - const currentPoolReserves = liquidityProxyState.poolsReserves.find( - poolReserve => - poolReserve.pool.toLowerCase() === pool.address.toLowerCase(), - ); - if (!currentPoolReserves) { - return null; - } - const prices = amounts.map(amount => { - if (side === SwapSide.SELL) { - return this.swapIn( - srcToken.address.toLowerCase() === pool.token0.toLowerCase(), - amount, - currentPoolReserves.collateralReserves, - currentPoolReserves.debtReserves, - srcToken.decimals, - destToken.decimals, - BigInt(currentPoolReserves.fee), - currentPoolReserves.dexLimits, - Math.floor(Date.now() / 1000), + const poolsPrices = await Promise.all( + pools.map(async pool => { + const currentPoolReserves = liquidityProxyState.poolsReserves.find( + poolReserve => + poolReserve.pool.toLowerCase() === pool.address.toLowerCase(), ); - } else { - return this.swapOut( - srcToken.address.toLowerCase() === pool.token0.toLowerCase(), - amount, - currentPoolReserves.collateralReserves, - currentPoolReserves.debtReserves, - srcToken.decimals, - destToken.decimals, - BigInt(currentPoolReserves.fee), - currentPoolReserves.dexLimits, - Math.floor(Date.now() / 1000), + + const eventPool = this.eventPools.find( + eventPool => + eventPool.poolAddress.toLowerCase() === + pool.address.toLowerCase(), ); - } - }); - return [ - { - prices: prices, - unit: getBigIntPow(destToken.decimals), - data: {}, - exchange: this.dexKey, - poolIdentifier: pool.id, - gasCost: FLUID_DEX_GAS_COST, - poolAddresses: [pool.address], - }, - ]; + + if (!eventPool) { + this.logger.warn( + `${this.dexKey}-${this.network}: Event pool ${pool.address} was not found...`, + ); + return null; + } + + const state = await eventPool.getStateOrGenerate(blockNumber); + + if (!currentPoolReserves || state.isSwapAndArbitragePaused === true) { + return null; + } + + const prices = amounts.map(amount => { + if (side === SwapSide.SELL) { + return this.swapIn( + srcToken.address.toLowerCase() === pool.token0.toLowerCase(), + amount, + currentPoolReserves.collateralReserves, + currentPoolReserves.debtReserves, + srcToken.decimals, + destToken.decimals, + BigInt(currentPoolReserves.fee), + currentPoolReserves.dexLimits, + Math.floor(Date.now() / 1000), + ); + } else { + return this.swapOut( + srcToken.address.toLowerCase() === pool.token0.toLowerCase(), + amount, + currentPoolReserves.collateralReserves, + currentPoolReserves.debtReserves, + srcToken.decimals, + destToken.decimals, + BigInt(currentPoolReserves.fee), + currentPoolReserves.dexLimits, + Math.floor(Date.now() / 1000), + ); + } + }); + + return { + prices: prices, + unit: getBigIntPow(destToken.decimals), + data: { + poolId: pool.id, + }, + exchange: this.dexKey, + poolIdentifier: pool.id, + gasCost: FLUID_DEX_GAS_COST, + poolAddresses: [pool.address], + }; + }), + ); + + const notNullResults = poolsPrices.filter( + res => res !== null, + ) as ExchangePrices; + + return notNullResults; } catch (e) { this.logger.error( `Error_getPricesVolume ${srcToken.address || srcToken.symbol}, ${ @@ -272,10 +315,9 @@ export class FluidDex extends SimpleExchange implements IDex { ): AdapterExchangeParam { // Encode here the payload for adapter const payload = ''; - const pool = this.getPoolByTokenPair(srcToken, destToken); return { - targetExchange: pool!.address, + targetExchange: '0x', payload, networkFee: '0', }; @@ -313,7 +355,11 @@ export class FluidDex extends SimpleExchange implements IDex { side === SwapSide.SELL ? 'amountOut_' : 'amountIn_', ); - const pool = this.getPoolByTokenPair(srcToken, destToken); + const pool = this.pools.find(pool => pool.id === data.poolId); + if (!pool) + throw new Error( + `${this.dexKey}-${this.network}: Pool with id: ${data.poolId} was not found`, + ); if (side === SwapSide.SELL) { if (pool!.token0.toLowerCase() !== srcToken.toLowerCase()) { @@ -584,11 +630,84 @@ export class FluidDex extends SimpleExchange implements IDex { if (priceDiff > maxAllowedDiff) { return 0n; } + + if (amountInCollateral > 0) { + let reservesRatioValid = swap0To1 + ? this.verifyToken1Reserves( + colReserveIn + amountInCollateral, + colReserveOut - amountOutCollateral, + oldPrice, + ) + : this.verifyToken0Reserves( + colReserveOut - amountOutCollateral, + colReserveIn + amountInCollateral, + oldPrice, + ); + if (!reservesRatioValid) { + return 0n; + } + } + + if (amountInDebt > 0) { + let reservesRatioValid = swap0To1 + ? this.verifyToken1Reserves( + debtReserveIn + amountInDebt, + debtReserveOut - amountOutDebt, + oldPrice, + ) + : this.verifyToken0Reserves( + debtReserveOut - amountOutDebt, + debtReserveIn + amountInDebt, + oldPrice, + ); + if (!reservesRatioValid) { + return 0n; + } + } + const totalAmountOut = amountOutCollateral + amountOutDebt; return totalAmountOut; } + /** + * Checks if token0 reserves are sufficient compared to token1 reserves. + * This helps prevent edge cases and ensures high precision in calculations. + * @param {number} token0Reserves - The reserves of token0. + * @param {number} token1Reserves - The reserves of token1. + * @param {number} price - The current price used for calculation. + * @returns {boolean} - Returns false if token0 reserves are too low, true otherwise. + */ + protected verifyToken0Reserves( + token0Reserves: bigint, + token1Reserves: bigint, + price: bigint, + ): boolean { + return ( + token0Reserves >= + (token1Reserves * 10n ** 27n) / (price * MIN_SWAP_LIQUIDITY) + ); + } + + /** + * Checks if token1 reserves are sufficient compared to token0 reserves. + * This helps prevent edge cases and ensures high precision in calculations. + * @param {number} token0Reserves - The reserves of token0. + * @param {number} token1Reserves - The reserves of token1. + * @param {number} price - The current price used for calculation. + * @returns {boolean} - Returns false if token1 reserves are too low, true otherwise. + */ + protected verifyToken1Reserves( + token0Reserves: bigint, + token1Reserves: bigint, + price: bigint, + ): boolean { + return ( + token1Reserves >= + (token0Reserves * price) / (10n ** 27n * MIN_SWAP_LIQUIDITY) + ); + } + /** * Calculates the currently available swappable amount for a token limit considering expansion since last syncTime. * @param syncTime - timestamp in seconds when the limits were synced @@ -765,7 +884,7 @@ export class FluidDex extends SimpleExchange implements IDex { syncTime, ); - if (amountIn == 2n ** 256n - 1n) { + if (amountIn === 2n ** 256n - 1n) { return amountIn; } const ans = (amountIn * BigInt(10 ** inDecimals)) / BigInt(10 ** 12); @@ -985,6 +1104,39 @@ export class FluidDex extends SimpleExchange implements IDex { return 2n ** 256n - 1n; } + if (amountInCollateral > 0) { + let reservesRatioValid = swap0to1 + ? this.verifyToken1Reserves( + colReserveIn + amountInCollateral, + colReserveOut - amountOutCollateral, + oldPrice, + ) + : this.verifyToken0Reserves( + colReserveOut - amountOutCollateral, + colReserveIn + amountInCollateral, + oldPrice, + ); + if (!reservesRatioValid) { + return 0n; + } + } + if (amountInDebt > 0) { + let reservesRatioValid = swap0to1 + ? this.verifyToken1Reserves( + debtReserveIn + amountInDebt, + debtReserveOut - amountOutDebt, + oldPrice, + ) + : this.verifyToken0Reserves( + debtReserveOut - amountOutDebt, + debtReserveIn + amountInDebt, + oldPrice, + ); + if (!reservesRatioValid) { + return 0n; + } + } + const totalAmountIn = amountInCollateral + amountInDebt; return totalAmountIn; diff --git a/src/dex/fluid-dex/types.ts b/src/dex/fluid-dex/types.ts index cffe433ce..0fb982e07 100644 --- a/src/dex/fluid-dex/types.ts +++ b/src/dex/fluid-dex/types.ts @@ -72,7 +72,9 @@ export interface PoolWithReserves { debtReserves: DebtReserves; } -export type FluidDexData = {}; +export type FluidDexData = { + poolId: string; +}; // Each pool has a contract address and token pairs. export type FluidDexPool = { diff --git a/src/dex/index.ts b/src/dex/index.ts index f982e5bb6..d206e73ca 100644 --- a/src/dex/index.ts +++ b/src/dex/index.ts @@ -92,10 +92,15 @@ import { ConcentratorArusd } from './concentrator-arusd/concentrator-arusd'; import { FxProtocolRusd } from './fx-protocol-rusd/fx-protocol-rusd'; import { AaveGsm } from './aave-gsm/aave-gsm'; import { LitePsm } from './lite-psm/lite-psm'; -import { UsualBond } from './usual-bond/usual-bond'; import { StkGHO } from './stkgho/stkgho'; +import { BalancerV3 } from './balancer-v3/balancer-v3'; +import { balancerV3Merge } from './balancer-v3/optimizer'; import { SkyConverter } from './sky-converter/sky-converter'; +import { Cables } from './cables/cables'; import { Stader } from './stader/stader'; +import { UsualBond } from './usual/usual-bond'; +import { UsualMWrappedM } from './usual/usual-m-wrapped-m'; +import { UsualMUsd0 } from './usual/usual-m-usd0'; const LegacyDexes = [ CurveV2, @@ -123,6 +128,7 @@ const Dexes = [ Swerve, BalancerV1, BalancerV2, + BalancerV3, UniswapV2, UniswapV3, Algebra, @@ -186,7 +192,10 @@ const Dexes = [ UsualBond, StkGHO, SkyConverter, + Cables, FluidDex, + UsualMWrappedM, + UsualMUsd0, ]; export type LegacyDexConstructor = new (dexHelper: IDexHelper) => IDexTxBuilder< @@ -217,6 +226,7 @@ export class DexAdapterService { public routeOptimizers: IRouteOptimizer[] = [ balancerV1Merge, balancerV2Merge, + balancerV3Merge, uniswapMerge, curveV1Merge, ]; diff --git a/src/dex/maverick-v2/maverick-math/maverick-pool-math.ts b/src/dex/maverick-v2/maverick-math/maverick-pool-math.ts index f487fe7ef..02e0e956b 100644 --- a/src/dex/maverick-v2/maverick-math/maverick-pool-math.ts +++ b/src/dex/maverick-v2/maverick-math/maverick-pool-math.ts @@ -634,7 +634,7 @@ export class MaverickPoolMath { activeTick += delta.tokenAIn ? 1n : -1n; if (MaverickDeltaMath.pastMaxTick(delta, activeTick)) { - this.state.activeTick += delta.tokenAIn ? 1n : -1n; + this.state.activeTick += delta.tokenAIn ? -1n : 1n; return delta; } } diff --git a/src/dex/usual-bond/config.ts b/src/dex/usual-bond/config.ts deleted file mode 100644 index 09fe45eca..000000000 --- a/src/dex/usual-bond/config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { DexParams } from './types'; -import { DexConfigMap } from '../../types'; -import { Network } from '../../constants'; - -export const UsualBondConfig: DexConfigMap = { - UsualBond: { - [Network.MAINNET]: { - usd0Address: '0x73A15FeD60Bf67631dC6cd7Bc5B6e8da8190aCF5', - usd0ppAddress: '0x35D8949372D46B7a3D5A56006AE77B215fc69bC0', - }, - }, -}; diff --git a/src/dex/usual-bond/usual-bond-integration.test.ts b/src/dex/usual-bond/usual-bond-integration.test.ts deleted file mode 100644 index 3844bc200..000000000 --- a/src/dex/usual-bond/usual-bond-integration.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* eslint-disable no-console */ -import dotenv from 'dotenv'; -dotenv.config(); - -import { Interface } from '@ethersproject/abi'; -import { DummyDexHelper } from '../../dex-helper/index'; -import { Network, SwapSide } from '../../constants'; -import { BI_POWS } from '../../bigint-constants'; -import { UsualBond } from './usual-bond'; -import { - checkPoolPrices, - checkConstantPoolPrices, - checkPoolsLiquidity, -} from '../../../tests/utils'; -import { Tokens } from '../../../tests/constants-e2e'; - -async function testPricingOnNetwork( - usualBond: UsualBond, - network: Network, - dexKey: string, - blockNumber: number, - srcTokenAddress: string, - destTokenAddress: string, - side: SwapSide, - amounts: bigint[], - funcNameToCheck: string, -) { - const networkTokens = Tokens[network]; - - console.log(amounts); - - const pools = await usualBond.getPoolIdentifiers( - networkTokens['USD0'], - networkTokens['USD0++'], - side, - blockNumber, - ); - console.log(`${'USD0'} <> ${'USD0++'} Pool Identifiers: `, pools); - - expect(pools.length).toBeGreaterThan(0); - - const poolPrices = await usualBond.getPricesVolume( - networkTokens['USD0'], - networkTokens['USD0++'], - amounts, - side, - blockNumber, - pools, - ); - console.log(`${'USD0'} <> ${'USD0++'} Pool Prices: `, poolPrices); - - expect(poolPrices).not.toBeNull(); - if (usualBond.hasConstantPriceLargeAmounts) { - checkConstantPoolPrices(poolPrices!, amounts, dexKey); - } else { - checkPoolPrices(poolPrices!, amounts, side, dexKey); - } - - // Check if onchain pricing equals to calculated ones - checkPoolPrices(poolPrices!, amounts, side, dexKey); -} - -describe('UsualBond', function () { - const dexKey = 'UsualBond'; - let blockNumber: number; - let usualBond: UsualBond; - - describe('Mainnet', () => { - const network = Network.MAINNET; - const dexHelper = new DummyDexHelper(network); - - // Don't forget to update relevant tokens in constant-e2e.ts - - const amountsForSell = [ - 0n, - 1n * BI_POWS[18], - 2n * BI_POWS[18], - 3n * BI_POWS[18], - 4n * BI_POWS[18], - 5n * BI_POWS[18], - 6n * BI_POWS[18], - 7n * BI_POWS[18], - 8n * BI_POWS[18], - 9n * BI_POWS[18], - 10n * BI_POWS[18], - ]; - - beforeAll(async () => { - blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); - usualBond = new UsualBond(network, dexKey, dexHelper); - if (usualBond.initializePricing) { - await usualBond.initializePricing(blockNumber); - } - }); - - it('getPoolIdentifiers and getPricesVolume SELL', async function () { - await testPricingOnNetwork( - usualBond, - network, - dexKey, - blockNumber, - 'USD0', - 'USD0++', - SwapSide.SELL, - amountsForSell, - '', - ); - }); - - it('getTopPoolsForToken: USD0', async function () { - const tokenA = Tokens[network]['USD0']; - const dexHelper = new DummyDexHelper(network); - const usualBond = new UsualBond(network, dexKey, dexHelper); - - const poolLiquidity = await usualBond.getTopPoolsForToken( - tokenA.address, - 10, - ); - console.log( - `${tokenA.symbol} Top Pools:`, - JSON.stringify(poolLiquidity, null, 2), - ); - - checkPoolsLiquidity(poolLiquidity, tokenA.address, dexKey); - }); - - it('getTopPoolsForToken: USD0++', async function () { - const tokenA = Tokens[network]['USD0++']; - const dexHelper = new DummyDexHelper(network); - const usualBond = new UsualBond(network, dexKey, dexHelper); - - const poolLiquidity = await usualBond.getTopPoolsForToken( - tokenA.address, - 10, - ); - console.log( - `${tokenA.symbol} Top Pools:`, - JSON.stringify(poolLiquidity, null, 2), - ); - - checkPoolsLiquidity(poolLiquidity, tokenA.address, dexKey); - }); - }); -}); diff --git a/src/dex/usual-bond/types.ts b/src/dex/usual/types.ts similarity index 55% rename from src/dex/usual-bond/types.ts rename to src/dex/usual/types.ts index ce90c4eb4..ef8151986 100644 --- a/src/dex/usual-bond/types.ts +++ b/src/dex/usual/types.ts @@ -5,6 +5,6 @@ export type PoolState = {}; export type UsualBondData = {}; export type DexParams = { - usd0Address: Address; - usd0ppAddress: Address; + fromToken: { address: Address; decimals: number }; + toToken: { address: Address; decimals: number }; }; diff --git a/src/dex/usual/usual-bond.ts b/src/dex/usual/usual-bond.ts new file mode 100644 index 000000000..ac39a92bc --- /dev/null +++ b/src/dex/usual/usual-bond.ts @@ -0,0 +1,69 @@ +import { + Address, + NumberAsString, + DexExchangeParam, + DexConfigMap, +} from '../../types'; +import { SwapSide, Network } from '../../constants'; +import { IDexHelper } from '../../dex-helper/idex-helper'; +import { UsualBondData, DexParams } from './types'; +import { Interface, JsonFragment } from '@ethersproject/abi'; +import USD0PP_ABI from '../../abi/usual-bond/usd0pp.abi.json'; +import { Usual } from './usual'; +import { getDexKeysWithNetwork } from '../../utils'; + +const Config: DexConfigMap = { + UsualBond: { + [Network.MAINNET]: { + fromToken: { + address: '0x73a15fed60bf67631dc6cd7bc5b6e8da8190acf5', + decimals: 18, + }, + toToken: { + address: '0x35d8949372d46b7a3d5a56006ae77b215fc69bc0', + decimals: 18, + }, + }, + }, +}; + +export class UsualBond extends Usual { + public static dexKeysWithNetwork: { key: string; networks: Network[] }[] = + getDexKeysWithNetwork(Config); + + usd0ppIface: Interface; + + constructor( + readonly network: Network, + readonly dexKey: string, + readonly dexHelper: IDexHelper, + ) { + super(network, dexKey, dexHelper, Config[dexKey][network]); + this.usd0ppIface = new Interface(USD0PP_ABI as JsonFragment[]); + } + + async getDexParam( + srcToken: Address, + destToken: Address, + srcAmount: NumberAsString, + destAmount: NumberAsString, + recipient: Address, + data: UsualBondData, + side: SwapSide, + ): Promise { + if (this.isFromToken(srcToken) && this.isToToken(destToken)) { + const exchangeData = this.usd0ppIface.encodeFunctionData('mint', [ + srcAmount, + ]); + + return { + needWrapNative: false, + dexFuncHasRecipient: false, + exchangeData, + targetExchange: this.config.toToken.address, + returnAmountPos: undefined, + }; + } + throw new Error('LOGIC ERROR'); + } +} diff --git a/src/dex/usual-bond/usual-bond-e2e.test.ts b/src/dex/usual/usual-e2e.test.ts similarity index 66% rename from src/dex/usual-bond/usual-bond-e2e.test.ts rename to src/dex/usual/usual-e2e.test.ts index f6d987514..1c3008a15 100644 --- a/src/dex/usual-bond/usual-bond-e2e.test.ts +++ b/src/dex/usual/usual-e2e.test.ts @@ -3,11 +3,7 @@ import dotenv from 'dotenv'; dotenv.config(); import { testE2E } from '../../../tests/utils-e2e'; -import { - Tokens, - Holders, - NativeTokenSymbols, -} from '../../../tests/constants-e2e'; +import { Tokens, Holders } from '../../../tests/constants-e2e'; import { Network, ContractMethod, SwapSide } from '../../constants'; import { StaticJsonRpcProvider } from '@ethersproject/providers'; import { generateConfig } from '../../config'; @@ -78,3 +74,49 @@ describe('UsualBond E2E', () => { ); }); }); + +describe('UsualMWrappedM E2E', () => { + const dexKey = 'UsualMWrappedM'; + + describe('Mainnet', () => { + const network = Network.MAINNET; + + const tokenASymbol: string = 'WrappedM'; + const tokenBSymbol: string = 'UsualM'; + + const tokenAAmount: string = '100000'; + const tokenBAmount: string = '100000'; + + testForNetwork( + network, + dexKey, + tokenASymbol, + tokenBSymbol, + tokenAAmount, + tokenBAmount, + ); + }); +}); + +describe('UsualM<>Usd0 E2E', () => { + const dexKey = 'UsualMUsd0'; + + describe('Mainnet', () => { + const network = Network.MAINNET; + + const tokenASymbol: string = 'UsualM'; + const tokenBSymbol: string = 'USD0'; + + const tokenAAmount: string = '1000000'; + const tokenBAmount: string = '1000000'; + + testForNetwork( + network, + dexKey, + tokenASymbol, + tokenBSymbol, + tokenAAmount, + tokenBAmount, + ); + }); +}); diff --git a/src/dex/usual/usual-integration.test.ts b/src/dex/usual/usual-integration.test.ts new file mode 100644 index 000000000..3041c157c --- /dev/null +++ b/src/dex/usual/usual-integration.test.ts @@ -0,0 +1,319 @@ +/* eslint-disable no-console */ +import dotenv from 'dotenv'; +dotenv.config(); + +import { Interface } from '@ethersproject/abi'; +import { DummyDexHelper } from '../../dex-helper/index'; +import { Network, SwapSide } from '../../constants'; +import { BI_POWS } from '../../bigint-constants'; +import { + checkPoolPrices, + checkConstantPoolPrices, + checkPoolsLiquidity, +} from '../../../tests/utils'; +import { Tokens } from '../../../tests/constants-e2e'; +import { UsualMWrappedM } from './usual-m-wrapped-m'; +import { UsualMUsd0 } from './usual-m-usd0'; +import { Usual } from './usual'; +import { UsualBond } from './usual-bond'; + +async function testPricingOnNetwork( + usual: Usual, + network: Network, + dexKey: string, + blockNumber: number, + srcTokenSymbol: string, + destTokenSymbol: string, + side: SwapSide, + amounts: bigint[], + funcNameToCheck: string, +) { + const networkTokens = Tokens[network]; + + console.log(amounts); + + const pools = await usual.getPoolIdentifiers( + networkTokens[srcTokenSymbol], + networkTokens[destTokenSymbol], + side, + blockNumber, + ); + console.log( + `${srcTokenSymbol} <> ${destTokenSymbol} Pool Identifiers: `, + pools, + ); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await usual.getPricesVolume( + networkTokens[srcTokenSymbol], + networkTokens[destTokenSymbol], + amounts, + side, + blockNumber, + pools, + ); + console.log( + `${srcTokenSymbol} <> ${destTokenSymbol} Pool Prices: `, + poolPrices, + ); + + expect(poolPrices).not.toBeNull(); + if (usual.hasConstantPriceLargeAmounts) { + checkConstantPoolPrices(poolPrices!, amounts, dexKey); + } else { + checkPoolPrices(poolPrices!, amounts, side, dexKey); + } + + // Check if onchain pricing equals to calculated ones + checkPoolPrices(poolPrices!, amounts, side, dexKey); +} + +describe('UsualBond', function () { + const dexKey = 'UsualBond'; + let blockNumber: number; + let usualBond: UsualBond; + + describe('Mainnet', () => { + const network = Network.MAINNET; + const dexHelper = new DummyDexHelper(network); + + // Don't forget to update relevant tokens in constant-e2e.ts + + const amountsForSell = [ + 0n, + 1n * BI_POWS[18], + 2n * BI_POWS[18], + 3n * BI_POWS[18], + 4n * BI_POWS[18], + 5n * BI_POWS[18], + 6n * BI_POWS[18], + 7n * BI_POWS[18], + 8n * BI_POWS[18], + 9n * BI_POWS[18], + 10n * BI_POWS[18], + ]; + + beforeAll(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + usualBond = new UsualBond(network, dexKey, dexHelper); + if (usualBond.initializePricing) { + await usualBond.initializePricing(blockNumber); + } + }); + + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + await testPricingOnNetwork( + usualBond, + network, + dexKey, + blockNumber, + 'USD0', + 'USD0++', + SwapSide.SELL, + amountsForSell, + '', + ); + }); + + it('getTopPoolsForToken: USD0', async function () { + const tokenA = Tokens[network]['USD0']; + const dexHelper = new DummyDexHelper(network); + const usualBond = new UsualBond(network, dexKey, dexHelper); + + const poolLiquidity = await usualBond.getTopPoolsForToken( + tokenA.address, + 10, + ); + console.log( + `${tokenA.symbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + + checkPoolsLiquidity(poolLiquidity, tokenA.address, dexKey); + }); + + it('getTopPoolsForToken: USD0++', async function () { + const tokenA = Tokens[network]['USD0++']; + const dexHelper = new DummyDexHelper(network); + const usualBond = new UsualBond(network, dexKey, dexHelper); + + const poolLiquidity = await usualBond.getTopPoolsForToken( + tokenA.address, + 10, + ); + console.log( + `${tokenA.symbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + + checkPoolsLiquidity(poolLiquidity, tokenA.address, dexKey); + }); + }); +}); + +describe('WrappedM<>UsualM', function () { + const dexKey = 'UsualMWrappedM'; + let blockNumber: number; + let usualMWrappedM: UsualMWrappedM; + + describe('Mainnet', () => { + const network = Network.MAINNET; + const dexHelper = new DummyDexHelper(network); + + // Don't forget to update relevant tokens in constant-e2e.ts + + const amountsForSell = [ + 0n, + 1n * BI_POWS[18], + 2n * BI_POWS[18], + 3n * BI_POWS[18], + 4n * BI_POWS[18], + 5n * BI_POWS[18], + 6n * BI_POWS[18], + 7n * BI_POWS[18], + 8n * BI_POWS[18], + 9n * BI_POWS[18], + 10n * BI_POWS[18], + ]; + + beforeAll(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + usualMWrappedM = new UsualMWrappedM(network, dexKey, dexHelper); + if (usualMWrappedM.initializePricing) { + await usualMWrappedM.initializePricing(blockNumber); + } + }); + + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + await testPricingOnNetwork( + usualMWrappedM, + network, + dexKey, + blockNumber, + 'WrappedM', + 'UsualM', + SwapSide.SELL, + amountsForSell, + '', + ); + }); + + it('getTopPoolsForToken: WrappedM', async function () { + const tokenA = Tokens[network]['WrappedM']; + const dexHelper = new DummyDexHelper(network); + const usualMWrappedM = new UsualMWrappedM(network, dexKey, dexHelper); + + const poolLiquidity = await usualMWrappedM.getTopPoolsForToken( + tokenA.address, + 10, + ); + console.log( + `${tokenA.symbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + + checkPoolsLiquidity(poolLiquidity, tokenA.address, dexKey); + }); + + it('getTopPoolsForToken: UsualM', async function () { + const tokenA = Tokens[network]['UsualM']; + const dexHelper = new DummyDexHelper(network); + const usualMWrappedM = new UsualMWrappedM(network, dexKey, dexHelper); + + const poolLiquidity = await usualMWrappedM.getTopPoolsForToken( + tokenA.address, + 10, + ); + console.log( + `${tokenA.symbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + + checkPoolsLiquidity(poolLiquidity, tokenA.address, dexKey); + }); + }); +}); + +describe('UsualM<>USD0', function () { + const dexKey = 'UsualMUsd0'; + let blockNumber: number; + let usualMUsd0: UsualMUsd0; + + describe('Mainnet', () => { + const network = Network.MAINNET; + const dexHelper = new DummyDexHelper(network); + + // Don't forget to update relevant tokens in constant-e2e.ts + + const amountsForSell = [ + 0n, + 1n * BI_POWS[18], + 2n * BI_POWS[18], + 3n * BI_POWS[18], + 4n * BI_POWS[18], + 5n * BI_POWS[18], + 6n * BI_POWS[18], + 7n * BI_POWS[18], + 8n * BI_POWS[18], + 9n * BI_POWS[18], + 10n * BI_POWS[18], + ]; + + beforeAll(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + usualMUsd0 = new UsualMUsd0(network, dexKey, dexHelper); + if (usualMUsd0.initializePricing) { + await usualMUsd0.initializePricing(blockNumber); + } + }); + + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + await testPricingOnNetwork( + usualMUsd0, + network, + dexKey, + blockNumber, + 'UsualM', + 'USD0', + SwapSide.SELL, + amountsForSell, + '', + ); + }); + + it('getTopPoolsForToken: UsualM', async function () { + const tokenA = Tokens[network]['UsualM']; + const dexHelper = new DummyDexHelper(network); + const usualMUsd0 = new UsualMUsd0(network, dexKey, dexHelper); + + const poolLiquidity = await usualMUsd0.getTopPoolsForToken( + tokenA.address, + 10, + ); + console.log( + `${tokenA.symbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + + checkPoolsLiquidity(poolLiquidity, tokenA.address, dexKey); + }); + + it('getTopPoolsForToken: USD0', async function () { + const tokenA = Tokens[network]['USD0']; + const dexHelper = new DummyDexHelper(network); + const usualMUsd0 = new UsualMUsd0(network, dexKey, dexHelper); + + const poolLiquidity = await usualMUsd0.getTopPoolsForToken( + tokenA.address, + 10, + ); + console.log( + `${tokenA.symbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + + checkPoolsLiquidity(poolLiquidity, tokenA.address, dexKey); + }); + }); +}); diff --git a/src/dex/usual/usual-m-usd0.ts b/src/dex/usual/usual-m-usd0.ts new file mode 100644 index 000000000..653450cee --- /dev/null +++ b/src/dex/usual/usual-m-usd0.ts @@ -0,0 +1,76 @@ +import { + Address, + NumberAsString, + DexExchangeParam, + DexConfigMap, +} from '../../types'; +import { SwapSide, Network } from '../../constants'; +import { IDexHelper } from '../../dex-helper/idex-helper'; +import { DexParams } from './types'; +import { Interface, JsonFragment } from '@ethersproject/abi'; +import { Usual } from './usual'; +import { getDexKeysWithNetwork } from '../../utils'; +import USUAL_DAO_COLLATERAL_ABI from '../../abi/usual-m-usd0/usualCollateralDao.abi.json'; + +const Config: DexConfigMap = + { + UsualMUsd0: { + [Network.MAINNET]: { + usualDaoCollateralAddress: '0xde6e1F680C4816446C8D515989E2358636A38b04', + fromToken: { + address: '0x4cbc25559dbbd1272ec5b64c7b5f48a2405e6470', + decimals: 6, + }, + toToken: { + address: '0x73a15fed60bf67631dc6cd7bc5b6e8da8190acf5', + decimals: 18, + }, + }, + }, + }; + +export class UsualMUsd0 extends Usual { + public static dexKeysWithNetwork: { key: string; networks: Network[] }[] = + getDexKeysWithNetwork(Config); + + usualDaoCollateralIface: Interface; + + constructor( + readonly network: Network, + readonly dexKey: string, + readonly dexHelper: IDexHelper, + ) { + super(network, dexKey, dexHelper, Config[dexKey][network]); + this.usualDaoCollateralIface = new Interface( + USUAL_DAO_COLLATERAL_ABI as JsonFragment[], + ); + } + + async getDexParam( + srcToken: Address, + destToken: Address, + srcAmount: NumberAsString, + destAmount: NumberAsString, + recipient: Address, + data: {}, + side: SwapSide, + ): Promise { + if (this.isFromToken(srcToken) && this.isToToken(destToken)) { + const exchangeData = this.usualDaoCollateralIface.encodeFunctionData( + 'swap', + [srcToken, srcAmount, destAmount], + ); + + return { + needWrapNative: false, + dexFuncHasRecipient: false, + exchangeData, + targetExchange: + Config[this.dexKey][this.network].usualDaoCollateralAddress, + returnAmountPos: undefined, + }; + } + + throw new Error('LOGIC ERROR'); + } +} diff --git a/src/dex/usual/usual-m-wrapped-m.ts b/src/dex/usual/usual-m-wrapped-m.ts new file mode 100644 index 000000000..8eb5d822f --- /dev/null +++ b/src/dex/usual/usual-m-wrapped-m.ts @@ -0,0 +1,71 @@ +import { + Address, + NumberAsString, + DexExchangeParam, + DexConfigMap, +} from '../../types'; +import { SwapSide, Network } from '../../constants'; +import { IDexHelper } from '../../dex-helper/idex-helper'; +import { DexParams } from './types'; +import { Interface, JsonFragment } from '@ethersproject/abi'; +import { Usual } from './usual'; +import { getDexKeysWithNetwork } from '../../utils'; +import USUALM_ABI from '../../abi/usual-m-wrapped-m/usualM.abi.json'; + +const Config: DexConfigMap = { + UsualMWrappedM: { + [Network.MAINNET]: { + fromToken: { + address: '0x437cc33344a0b27a429f795ff6b469c72698b291', + decimals: 6, + }, + toToken: { + address: '0x4cbc25559dbbd1272ec5b64c7b5f48a2405e6470', + decimals: 6, + }, + }, + }, +}; + +export class UsualMWrappedM extends Usual { + public static dexKeysWithNetwork: { key: string; networks: Network[] }[] = + getDexKeysWithNetwork(Config); + + usualMIface: Interface; + + constructor( + readonly network: Network, + readonly dexKey: string, + readonly dexHelper: IDexHelper, + ) { + super(network, dexKey, dexHelper, Config[dexKey][network]); + this.usualMIface = new Interface(USUALM_ABI as JsonFragment[]); + } + + async getDexParam( + srcToken: Address, + destToken: Address, + srcAmount: NumberAsString, + destAmount: NumberAsString, + recipient: Address, + data: {}, + side: SwapSide, + ): Promise { + if (this.isFromToken(srcToken) && this.isToToken(destToken)) { + const exchangeData = this.usualMIface.encodeFunctionData( + 'wrap(address, uint256)', + [recipient, srcAmount], + ); + + return { + needWrapNative: false, + dexFuncHasRecipient: true, + exchangeData, + targetExchange: this.config.toToken.address, + returnAmountPos: undefined, + }; + } + + throw new Error('LOGIC ERROR'); + } +} diff --git a/src/dex/usual-bond/usual-bond.ts b/src/dex/usual/usual.ts similarity index 51% rename from src/dex/usual-bond/usual-bond.ts rename to src/dex/usual/usual.ts index 295dc0db4..b17019e5d 100644 --- a/src/dex/usual-bond/usual-bond.ts +++ b/src/dex/usual/usual.ts @@ -5,47 +5,30 @@ import { PoolPrices, AdapterExchangeParam, Logger, - NumberAsString, - DexExchangeParam, PoolLiquidity, } from '../../types'; import { SwapSide, Network } from '../../constants'; import * as CALLDATA_GAS_COST from '../../calldata-gas-cost'; -import { getDexKeysWithNetwork } from '../../utils'; import { IDex } from '../idex'; import { IDexHelper } from '../../dex-helper/idex-helper'; import { UsualBondData, DexParams } from './types'; import { SimpleExchange } from '../simple-exchange'; -import { UsualBondConfig } from './config'; -import { Interface, JsonFragment } from '@ethersproject/abi'; -import USD0PP_ABI from '../../abi/usual-bond/usd0pp.abi.json'; import { BI_POWS } from '../../bigint-constants'; -export class UsualBond extends SimpleExchange implements IDex { - protected config: DexParams; - +export class Usual extends SimpleExchange implements IDex { readonly hasConstantPriceLargeAmounts = true; readonly needWrapNative = false; readonly isFeeOnTransferSupported = false; - public static dexKeysWithNetwork: { key: string; networks: Network[] }[] = - getDexKeysWithNetwork(UsualBondConfig); - - usd0ppIface: Interface; logger: Logger; constructor( readonly network: Network, readonly dexKey: string, readonly dexHelper: IDexHelper, + readonly config: DexParams, ) { super(dexHelper, dexKey); - const config = UsualBondConfig[dexKey][network]; - this.usd0ppIface = new Interface(USD0PP_ABI as JsonFragment[]); - this.config = { - usd0Address: config.usd0Address.toLowerCase(), - usd0ppAddress: config.usd0ppAddress.toLowerCase(), - }; this.logger = dexHelper.getLogger(dexKey); } @@ -53,20 +36,16 @@ export class UsualBond extends SimpleExchange implements IDex { // No initialization needed for constant price } - getConfig() { - return this.config; + isFromToken(token: string) { + return token.toLowerCase() === this.config.fromToken.address.toLowerCase(); } - is_usd0(token: string) { - return token.toLowerCase() === this.config.usd0Address.toLowerCase(); + isToToken(token: string) { + return token.toLowerCase() === this.config.toToken.address.toLowerCase(); } - is_usd0pp(token: string) { - return token.toLowerCase() === this.config.usd0ppAddress.toLowerCase(); - } - - is_usd0_swap_token(srcToken: string, destToken: string) { - return this.is_usd0(srcToken) && this.is_usd0pp(destToken); + isValidTokens(srcToken: string, destToken: string) { + return this.isFromToken(srcToken) && this.isToToken(destToken); } getAdapters() { @@ -92,8 +71,8 @@ export class UsualBond extends SimpleExchange implements IDex { return []; } - if (this.is_usd0_swap_token(srcTokenAddress, destTokenAddress)) { - return [`${this.dexKey}_${this.config.usd0ppAddress}`]; + if (this.isValidTokens(srcTokenAddress, destTokenAddress)) { + return [`${this.dexKey}_${this.config.toToken}`]; } return []; @@ -111,24 +90,25 @@ export class UsualBond extends SimpleExchange implements IDex { return null; } - const isUSD0SwapToken = this.is_usd0_swap_token( - srcToken.address, - destToken.address, - ); + const isValidSwap = this.isValidTokens(srcToken.address, destToken.address); - if (!isUSD0SwapToken) { + if (!isValidSwap) { return null; } - const unitOut = BI_POWS[18]; // 1:1 swap - const amountsOut = amounts; // 1:1 swap, so output amounts are the same as input + const unitOut = BI_POWS[this.config.toToken.decimals]; // 1:1 swap + const amountsOut = amounts.map( + amount => + (amount * BI_POWS[this.config.toToken.decimals]) / + BI_POWS[this.config.fromToken.decimals], + ); // 1:1 swap, so output amounts are the same as input return [ { unit: unitOut, prices: amountsOut, data: {}, - poolAddresses: [this.config.usd0ppAddress], + poolAddresses: [this.config.toToken.address], exchange: this.dexKey, gasCost: 70000, poolIdentifier: this.dexKey, @@ -151,55 +131,27 @@ export class UsualBond extends SimpleExchange implements IDex { const payload = '0x'; return { - targetExchange: this.config.usd0ppAddress, + targetExchange: this.config.toToken.address, payload, networkFee: '0', }; } - async getDexParam( - srcToken: Address, - destToken: Address, - srcAmount: NumberAsString, - destAmount: NumberAsString, - recipient: Address, - data: UsualBondData, - side: SwapSide, - ): Promise { - if (this.is_usd0(srcToken) && this.is_usd0pp(destToken)) { - const exchangeData = this.usd0ppIface.encodeFunctionData('mint', [ - srcAmount, - ]); - - return { - needWrapNative: false, - dexFuncHasRecipient: false, - exchangeData, - targetExchange: this.config.usd0ppAddress, - returnAmountPos: undefined, - }; - } - throw new Error('LOGIC ERROR'); - } - async getTopPoolsForToken( tokenAddress: Address, limit: number, ): Promise { - const isUsd0 = this.is_usd0(tokenAddress); - if (!isUsd0 && !this.is_usd0pp(tokenAddress)) return []; + const isFromToken = this.isFromToken(tokenAddress); + const isToToken = this.isToToken(tokenAddress); + + if (!(isFromToken || isToToken)) return []; return [ { exchange: this.dexKey, - address: this.config.usd0ppAddress, + address: this.config.toToken.address, connectorTokens: [ - { - decimals: 18, - address: isUsd0 - ? this.config.usd0ppAddress - : this.config.usd0Address, - }, + isFromToken ? this.config.toToken : this.config.fromToken, ], liquidityUSD: 1000000000, // Just returning a big number so this DEX will be preferred }, diff --git a/src/dex/weth/config.ts b/src/dex/weth/config.ts index e91ceeaf6..f1208d4cf 100644 --- a/src/dex/weth/config.ts +++ b/src/dex/weth/config.ts @@ -25,6 +25,9 @@ export const WethConfig: DexConfigMap = { [Network.BASE]: { poolGasCost: WethGasCost, }, + [Network.SEPOLIA]: { + poolGasCost: WethGasCost, + }, }, Wbnb: { [Network.BSC]: { diff --git a/src/executor/Executor01BytecodeBuilder.ts b/src/executor/Executor01BytecodeBuilder.ts index c46a14481..9d6e743b0 100644 --- a/src/executor/Executor01BytecodeBuilder.ts +++ b/src/executor/Executor01BytecodeBuilder.ts @@ -275,6 +275,7 @@ export class Executor01BytecodeBuilder extends ExecutorBytecodeBuilder< curExchangeParam.approveData.target, curExchangeParam.approveData.token, flags.approves[index], + curExchangeParam.permit2Approval, ); swapCallData = hexConcat([approveCallData, swapCallData]); @@ -298,6 +299,7 @@ export class Executor01BytecodeBuilder extends ExecutorBytecodeBuilder< curExchangeParam.approveData.target, curExchangeParam.approveData.token, flags.approves[index], + curExchangeParam.permit2Approval, ); } @@ -377,17 +379,21 @@ export class Executor01BytecodeBuilder extends ExecutorBytecodeBuilder< let fromAmountPos = 0; if (insertFromAmount) { - const fromAmount = ethers.utils.defaultAbiCoder.encode( - ['uint256'], - [swap.swapExchanges[swapExchangeIndex].srcAmount], - ); - - const fromAmountIndex = exchangeData - .replace('0x', '') - .indexOf(fromAmount.replace('0x', '')); - - fromAmountPos = - (fromAmountIndex !== -1 ? fromAmountIndex : exchangeData.length) / 2; + if (exchangeParam.insertFromAmountPos) { + fromAmountPos = exchangeParam.insertFromAmountPos; + } else { + const fromAmount = ethers.utils.defaultAbiCoder.encode( + ['uint256'], + [swap.swapExchanges[swapExchangeIndex].srcAmount], + ); + + const fromAmountIndex = exchangeData + .replace('0x', '') + .indexOf(fromAmount.replace('0x', '')); + + fromAmountPos = + (fromAmountIndex !== -1 ? fromAmountIndex : exchangeData.length) / 2; + } } return this.buildCallData( diff --git a/src/executor/Executor02BytecodeBuilder.ts b/src/executor/Executor02BytecodeBuilder.ts index 63e149a65..90cf76be8 100644 --- a/src/executor/Executor02BytecodeBuilder.ts +++ b/src/executor/Executor02BytecodeBuilder.ts @@ -335,16 +335,20 @@ export class Executor02BytecodeBuilder extends ExecutorBytecodeBuilder< let fromAmountPos = 0; if (insertFromAmount) { - const fromAmount = ethers.utils.defaultAbiCoder.encode( - ['uint256'], - [swapExchange.srcAmount], - ); - const fromAmountIndex = exchangeData - .replace('0x', '') - .indexOf(fromAmount.replace('0x', '')); + if (exchangeParam.insertFromAmountPos) { + fromAmountPos = exchangeParam.insertFromAmountPos; + } else { + const fromAmount = ethers.utils.defaultAbiCoder.encode( + ['uint256'], + [swapExchange.srcAmount], + ); + const fromAmountIndex = exchangeData + .replace('0x', '') + .indexOf(fromAmount.replace('0x', '')); - fromAmountPos = - (fromAmountIndex !== -1 ? fromAmountIndex : exchangeData.length) / 2; + fromAmountPos = + (fromAmountIndex !== -1 ? fromAmountIndex : exchangeData.length) / 2; + } } return this.buildCallData( @@ -613,6 +617,7 @@ export class Executor02BytecodeBuilder extends ExecutorBytecodeBuilder< curExchangeParam.approveData.target, curExchangeParam.approveData.token, flags.approves[exchangeParamIndex], + curExchangeParam.permit2Approval, ); swapExchangeCallData = hexConcat([approveCallData, swapExchangeCallData]); @@ -630,6 +635,7 @@ export class Executor02BytecodeBuilder extends ExecutorBytecodeBuilder< curExchangeParam.approveData.target, curExchangeParam.approveData.token, flags.approves[exchangeParamIndex], + curExchangeParam.permit2Approval, ); } diff --git a/src/executor/Executor03BytecodeBuilder.ts b/src/executor/Executor03BytecodeBuilder.ts index 2a150171c..c9331b80c 100644 --- a/src/executor/Executor03BytecodeBuilder.ts +++ b/src/executor/Executor03BytecodeBuilder.ts @@ -186,6 +186,7 @@ export class Executor03BytecodeBuilder extends ExecutorBytecodeBuilder< curExchangeParam.approveData.target, curExchangeParam.approveData.token, flags.approves[index], + curExchangeParam.permit2Approval, ); swapCallData = hexConcat([approveCallData, swapCallData]); @@ -205,6 +206,7 @@ export class Executor03BytecodeBuilder extends ExecutorBytecodeBuilder< curExchangeParam.approveData.target, curExchangeParam.approveData.token, flags.approves[index], + curExchangeParam.permit2Approval, ); } @@ -329,24 +331,31 @@ export class Executor03BytecodeBuilder extends ExecutorBytecodeBuilder< let fromAmountPos = 0; let toAmountPos = 0; if (insertAmount) { - const fromAmount = ethers.utils.defaultAbiCoder.encode( - ['uint256'], - [swap.swapExchanges[swapExchangeIndex].srcAmount], - ); + if (exchangeParam.insertFromAmountPos) { + fromAmountPos = exchangeParam.insertFromAmountPos; + } else { + const fromAmount = ethers.utils.defaultAbiCoder.encode( + ['uint256'], + [swap.swapExchanges[swapExchangeIndex].srcAmount], + ); + + const fromAmountIndex = exchangeData + .replace('0x', '') + .indexOf(fromAmount.replace('0x', '')); + + fromAmountPos = + (fromAmountIndex !== -1 ? fromAmountIndex : exchangeData.length) / 2; + } + const toAmount = ethers.utils.defaultAbiCoder.encode( ['uint256'], [swap.swapExchanges[swapExchangeIndex].destAmount], ); - const fromAmountIndex = exchangeData - .replace('0x', '') - .indexOf(fromAmount.replace('0x', '')); const toAmountIndex = exchangeData .replace('0x', '') .indexOf(toAmount.replace('0x', '')); - fromAmountPos = - (fromAmountIndex !== -1 ? fromAmountIndex : exchangeData.length) / 2; toAmountPos = (toAmountIndex !== -1 ? toAmountIndex : exchangeData.length) / 2; } diff --git a/src/executor/ExecutorBytecodeBuilder.ts b/src/executor/ExecutorBytecodeBuilder.ts index ce333eabe..e81fc5be1 100644 --- a/src/executor/ExecutorBytecodeBuilder.ts +++ b/src/executor/ExecutorBytecodeBuilder.ts @@ -1,6 +1,7 @@ import { Interface } from '@ethersproject/abi'; import { IDexHelper } from '../dex-helper'; import ERC20ABI from '../abi/erc20.json'; +import Permit2Abi from '../abi/permit2.json'; import { ethers } from 'ethers'; import { Address, @@ -23,14 +24,17 @@ import { DISABLED_MAX_UNIT_APPROVAL_TOKENS, } from './constants'; import { Executors, Flag, SpecialDex } from './types'; -import { MAX_UINT, Network } from '../constants'; +import { MAX_UINT, Network, PERMIT2_ADDRESS } from '../constants'; import { DexExchangeBuildParam, DexExchangeParam } from '../types'; -import { ExecutorDetector } from './ExecutorDetector'; +import { BI_MAX_UINT160, BI_MAX_UINT48 } from '../bigint-constants'; const { utils: { hexlify, hexDataLength, hexConcat, hexZeroPad, solidityPack }, } = ethers; +const MAX_UINT48 = BI_MAX_UINT48.toString(); +const MAX_UINT160 = BI_MAX_UINT160.toString(); + export type SingleSwapCallDataParams = { priceRoute: OptimalRate; exchangeParams: DexExchangeBuildParam[]; @@ -54,9 +58,11 @@ export type DexCallDataParams = { export abstract class ExecutorBytecodeBuilder { type!: Executors; erc20Interface: Interface; + permit2Interface: Interface; constructor(protected dexHelper: IDexHelper) { this.erc20Interface = new Interface(ERC20ABI); + this.permit2Interface = new Interface(Permit2Abi); } protected buildSimpleSwapFlags( @@ -120,8 +126,13 @@ export abstract class ExecutorBytecodeBuilder { spender: string, tokenAddr: Address, flag: Flag, + permit2 = false, amount = MAX_UINT, ): string { + if (permit2) { + return this.buildPermit2CallData(spender, tokenAddr, flag); + } + let approveCalldata = this.erc20Interface.encodeFunctionData('approve', [ spender, amount, @@ -155,6 +166,7 @@ export abstract class ExecutorBytecodeBuilder { spender, tokenAddr, Flag.DONT_INSERT_FROM_AMOUNT_DONT_CHECK_BALANCE_AFTER_SWAP, + false, '0', ), approvalCalldata, @@ -164,6 +176,55 @@ export abstract class ExecutorBytecodeBuilder { return approvalCalldata; } + protected buildPermit2CallData( + spender: string, + tokenAddr: Address, + flag: Flag, + ): string { + // first, give approval for Permit2 on the token contract + // (with this approval, Permit2 contract can invoke safeTransferFrom on the token) + let approveData = this.erc20Interface.encodeFunctionData('approve', [ + PERMIT2_ADDRESS, + MAX_UINT, + ]); + + let approvalCalldata = this.buildCallData( + tokenAddr, + approveData, + 0, + APPROVE_CALLDATA_DEST_TOKEN_POS, + SpecialDex.DEFAULT, + Flag.DONT_INSERT_FROM_AMOUNT_DONT_CHECK_BALANCE_AFTER_SWAP, + ); + + // second, give approval for spender on Permit2 contract + // (with this approval, spender can interact with Permit2 on behalf of the executor) + let permit2Data = this.permit2Interface.encodeFunctionData('approve', [ + tokenAddr, + spender, + MAX_UINT160, + MAX_UINT48, + ]); + + let permit2Calldata = this.buildCallData( + PERMIT2_ADDRESS, + permit2Data, + 0, + APPROVE_CALLDATA_DEST_TOKEN_POS, + SpecialDex.DEFAULT, + flag, + ); + + // as approval given only for MAX_UNIT or 0, no need to use insertFromAmount flag + const checkSrcTokenBalance = flag % 3 === 2; + + if (checkSrcTokenBalance) { + permit2Calldata = hexConcat([permit2Calldata, ZEROS_12_BYTES, tokenAddr]); + } + + return hexConcat([approvalCalldata, permit2Calldata]); + } + protected buildWrapEthCallData( wethAddress: string, depositCallData: string, diff --git a/src/generic-swap-transaction-builder.ts b/src/generic-swap-transaction-builder.ts index fedb6c7a1..8442d38d5 100644 --- a/src/generic-swap-transaction-builder.ts +++ b/src/generic-swap-transaction-builder.ts @@ -687,7 +687,7 @@ export class GenericSwapTransactionBuilder { ): Promise { const spender = bytecodeBuilder.getAddress(); const tokenTargetMapping: { - params: [token: Address, target: Address]; + params: [token: Address, target: Address, permit2: boolean]; exchangeParamIndex: number; }[] = []; @@ -704,7 +704,11 @@ export class GenericSwapTransactionBuilder { if (approveParams) { tokenTargetMapping.push({ - params: [approveParams.token, approveParams?.target], + params: [ + approveParams.token, + approveParams.target, + !!curExchangeParam.permit2Approval, + ], exchangeParamIndex: currentExchangeParamIndex, }); } diff --git a/src/types.ts b/src/types.ts index 38f10a85e..843cf67e8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -173,6 +173,8 @@ export type DexExchangeParam = { swappedAmountNotPresentInExchangeData?: boolean; preSwapUnwrapCalldata?: string; returnAmountPos: number | undefined; + insertFromAmountPos?: number; + permit2Approval?: boolean; }; export type DexExchangeParamWithBooleanNeedWrapNative = DexExchangeParam & { diff --git a/tests/constants-e2e.ts b/tests/constants-e2e.ts index 1b0b6b326..3903f8a71 100644 --- a/tests/constants-e2e.ts +++ b/tests/constants-e2e.ts @@ -131,6 +131,10 @@ export const Tokens: { addBalance: balancesFn, addAllowance: allowedFn, }, + INST: { + address: '0x6f40d4A6237C257fff2dB00FA0510DeEECd303eb', + decimals: 18, + }, aEthUSDC: { address: '0x98c23e9d8f34fefb1b7bd6a91b7ff122f4e16f5c', decimals: 6, @@ -554,6 +558,21 @@ export const Tokens: { decimals: 18, symbol: 'USD0++', }, + wUSDL: { + address: '0x7751e2f4b8ae93ef6b79d86419d42fe3295a4559', + decimals: 18, + symbol: 'wUSDL', + }, + WrappedM: { + address: '0x437cc33344a0B27A429f795ff6B469C72698B291', + decimals: 6, + symbol: 'wM', + }, + UsualM: { + address: '0x4cbc25559dbbd1272ec5b64c7b5f48a2405e6470', + decimals: 6, + symbol: 'USUALM', + }, }, [Network.POLYGON]: { jGBP: { @@ -1484,10 +1503,18 @@ export const Tokens: { address: '0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83', decimals: 6, }, + USDCe: { + address: '0x2a22f9c3b484c3629090feed35f17ff8f88f76f0', + decimals: 6, + }, USDT: { address: '0x4ECaBa5870353805a9F068101A40E0f32ed605C6', decimals: 6, }, + COW: { + address: '0x177127622c4A00F3d409B75571e12cB3c8973d3c', + decimals: 18, + }, WXDAI: { address: '0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d', decimals: 18, @@ -1496,14 +1523,26 @@ export const Tokens: { address: '0xa818f1b57c201e092c4a2017a91815034326efd1', decimals: 18, }, + waGnoWETH: { + address: '0x57f664882F762FA37903FC864e2B633D384B411A', + decimals: 18, + }, aGnowstETH: { address: '0x23e4e76d01b2002be436ce8d6044b0aa2f68b68a', decimals: 18, }, + waGnowstETH: { + address: '0x773CDA0CADe2A3d86E6D4e30699d40bB95174ff2', + decimals: 18, + }, aGnoUSDC: { address: '0xc6b7aca6de8a6044e0e32d0c841a89244a10d284', decimals: 6, }, + GNO: { + address: '0x9c58bacc331c9aa871afd802db6379a98e80cedb', + decimals: 18, + }, wstETH: { address: '0x6C76971f98945AE98dD7d4DFcA8711ebea946eA6', decimals: 18, @@ -1623,6 +1662,41 @@ export const Tokens: { decimals: 6, }, }, + [Network.SEPOLIA]: { + ETH: { address: ETHER_ADDRESS, decimals: 18 }, + WETH: { + address: '0x7b79995e5f793a07bc00c21412e50ecae098e7f9', + decimals: 18, + }, + bal: { + address: `0xb19382073c7a0addbb56ac6af1808fa49e377b75`, + decimals: 18, + }, + daiAave: { + address: `0xff34b3d4aee8ddcd6f9afffb6fe49bd371b8a357`, + decimals: 18, + }, + usdcAave: { + address: `0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8`, + decimals: 6, + }, + usdtAave: { + address: `0xaa8e23fb1079ea71e0a56f48a2aa51851d8433d0`, + decimals: 6, + }, + aDaiAave: { + address: `0xde46e43f46ff74a23a65ebb0580cbe3dfe684a17`, + decimals: 18, + }, + stataUSDC: { + address: `0x8a88124522dbbf1e56352ba3de1d9f78c143751e`, + decimals: 6, + }, + stataUSDT: { + address: `0x978206fae13faf5a8d293fb614326b237684b750`, + decimals: 6, + }, + }, }; export const Holders: { @@ -1633,7 +1707,6 @@ export const Holders: { sUSDS: '0xd564B3aE673CAa49D054Bf185bD72a6853763eE7', SKY: '0x0ddda327A6614130CCb20bc0097313A282176A01', MKR: '0xe9aAA7A9DDc0877626C1779AbC29993aD89A6c1f', - ETHx: '0xFCC1A2c71F01B7f58Ed538a6B4AAa5A0724eB5A6', // Idle tokens AA_wstETH: '0xd7C1b48877A7dFA7D51cf1144c89C0A3F134F935', 'AA_idle_cpPOR-USDC': '0x085c8eaccA6911fE60aE3f8FbAe5F3012E3A05Ec', @@ -1738,10 +1811,12 @@ export const Holders: { weETH: '0x267ed5f71EE47D3E45Bb1569Aa37889a2d10f91e', rUSD: '0xEC2eda1C4F981E468ABF62424a10B69B738b498E', arUSD: '0xeFc24206053a452e2299BF3b8f964512b041Db4C', - USD0: '0x6A5d5Af0E266a24648a9d7E8D388EAEc7AbD8433', + USD0: '0x224762e69169E425239EeEE0012d1B0e041C123D', + WrappedM: '0xE0663f2372cAa1459b7ade90812Dc737CE587FA6', 'USD0++': '0x2227b6806339906707b43F36a1f07B52FF7Fa776', USDM: '0x57F5E098CaD7A3D1Eed53991D4d66C45C9AF7812', wUSDM: '0x3B95bC951EE0f553ba487327278cAc44f29715E5', + UsualM: '0xE3f7A0c4a44b740328157A5152A85c3bCB54DA09', }, [Network.POLYGON]: { jGBP: '0x02aa0B826c7BA6386DdBE04C0a8715A1c0A16B24', @@ -1854,7 +1929,7 @@ export const Holders: { BETS: '0x8cc2284c90d05578633418f9cde104f402375a65', HATCHY: '0x14ec295ec8def851ec6e2959df872dd24e422631', USDCe: '0x3a2434c698f8d79af1f5a9e43013157ca8b11a66', - USDC: '0xcc2da711D621A4491b338CAC88B9C0954db3e75B', + USDC: '0x64b4dE1b00EF830f3CC2FD68ee056aAD76C45BF6', USDTe: '0x84d34f4f83a87596cd3fb6887cff8f17bf5a7b83', WETHe: '0x9bdB521a97E95177BF252C253E256A60C3e14447', POPS: '0x5268c2331658cb0b2858cfa9db27d8f22f5434bc', @@ -1870,7 +1945,7 @@ export const Holders: { TSD: '0x691A89db352B72dDb249bFe16503494eC0D920A4', THO: '0xc40d16c47394a506d451475c8a7c46c1175c1da1', aAvaUSDT: '0x50B1Ba98Cf117c9682048D56628B294ebbAA4ec2', - USDT: '0x0d0707963952f2fba59dd06f2b425ace40b492fe', + USDT: '0xCddc5d0Ebeb71a08ffF26909AA6c0d4e256b4fE1', aAvaWAVAX: '0x1B18Df70863636AEe4BfBAb6F7C70ceBCA9bA404', oldFRAX: '0x4e3376018add04ebe4c46bf6f924ddec8c67aa7b', newFRAX: '0x4e3376018add04ebe4c46bf6f924ddec8c67aa7b', @@ -1965,6 +2040,8 @@ export const Holders: { USDC: '0x99b31498b0a1dae01fc3433e3cb60f095340935c', }, [Network.GNOSIS]: { + GNO: '0x458cD345B4C05e8DF39d0A07220feb4Ec19F5e6f', + COW: '0x4fFAD6ac852c0Af0AA301376F4C5Dea3a928b120', XDAI: '0x9fc062032d4F2Fe7dAA601bd8B06C45F9c8f17Be', WXDAI: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', WETH: '0x800e12aF6c96790EDDdc5B3f3302899e27B2A918', @@ -1976,8 +2053,11 @@ export const Holders: { wstETH: '0x458cD345B4C05e8DF39d0A07220feb4Ec19F5e6f', aGnowstETH: '0x458cD345B4C05e8DF39d0A07220feb4Ec19F5e6f', sDAI: '0x79f08F2e75A8C99428DE4A2e6456c07C99E55da5', + USDCe: '0x555CE236C0220695b68341bc48C68d52210cC35b', crvUSD: '0xE4A982fa1f1E8AD1AF238A7b1226b13b56bf5CcD', SWPR: '0x9467dcFD4519287e3878C018c02f5670465a9003', + waGnoWETH: '0x854B004700885A61107B458f11eCC169A019b764', + waGnowstETH: '0x854B004700885A61107B458f11eCC169A019b764', }, [Network.BASE]: { WETH: '0x4bb6b2efe7036020ba6f02a05602546c9f25bf28', @@ -2005,6 +2085,15 @@ export const Holders: { cbETH: '0x50e011dD1e2b4906F1534623cD134B30422bb11E', wUSDM: '0xe30965Acd0Ee1CE2e0Cd0AcBFB3596bD6fC78A51', }, + [Network.SEPOLIA]: { + bal: '0xDb4ff41B4C1222c2b1869A67Be115070688989a2', + daiAave: '0xbB0bc84687fFb642fd90a3D12215e7eC16352A49', + WETH: '0x546e37DAA15cdb82fd1a717E5dEEa4AF08D4349A', + ETH: '0x2CdA41645F2dBffB852a605E92B185501801FC28', + stataUSDC: '0x75D06bae37a9c349142fE7cee77804900b1C0EC3', + stataUSDT: '0x75D06bae37a9c349142fE7cee77804900b1C0EC3', + usdcAave: '0xdD5De55eA6804EFb283f43b0C091C25000a6486c', + }, }; export const SmartTokens = Object.keys(Tokens).reduce((acc, _network) => { @@ -2030,6 +2119,7 @@ export const NativeTokenSymbols: { [network: number]: string } = { [Network.ARBITRUM]: 'ETH', [Network.OPTIMISM]: 'ETH', [Network.BASE]: 'ETH', + [Network.SEPOLIA]: 'ETH', [Network.GNOSIS]: 'XDAI', }; @@ -2042,5 +2132,6 @@ export const WrappedNativeTokenSymbols: { [network: number]: string } = { [Network.ARBITRUM]: 'WETH', [Network.OPTIMISM]: 'WETH', [Network.BASE]: 'WETH', + [Network.SEPOLIA]: 'WETH', [Network.GNOSIS]: 'WXDAI', }; diff --git a/tests/utils-e2e.ts b/tests/utils-e2e.ts index 8c448bcae..a0343d057 100644 --- a/tests/utils-e2e.ts +++ b/tests/utils-e2e.ts @@ -397,15 +397,18 @@ export async function testE2E( await ts.setup(); if (srcToken.address.toLowerCase() !== ETHER_ADDRESS.toLowerCase()) { - const allowanceTx = await ts.simulate( - allowTokenTransferProxyParams(srcToken.address, senderAddress, network), - ); + // check if v5 is available in the config + if (generateConfig(network).tokenTransferProxyAddress !== NULL_ADDRESS) { + const allowanceTx = await ts.simulate( + allowTokenTransferProxyParams(srcToken.address, senderAddress, network), + ); + if (!allowanceTx.success) console.log(allowanceTx.url); + expect(allowanceTx!.success).toEqual(true); + } const augustusV6Allowance = await ts.simulate( allowAugustusV6(srcToken.address, senderAddress, network), ); - if (!allowanceTx.success) console.log(allowanceTx.url); if (!augustusV6Allowance.success) console.log(augustusV6Allowance.url); - expect(allowanceTx!.success).toEqual(true); expect(augustusV6Allowance!.success).toEqual(true); } diff --git a/yarn.lock b/yarn.lock index e0a05a043..735d7b7af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -348,6 +348,11 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@balancer-labs/balancer-maths@^0.0.19": + version "0.0.19" + resolved "https://registry.yarnpkg.com/@balancer-labs/balancer-maths/-/balancer-maths-0.0.19.tgz#d3b92c4b17caca67894806bd7768af1ea31cb956" + integrity sha512-x8t17q8fbXYZJSaidr4BWBg3O9jVnF6xoAsoMWWyUd6AnaxPyFnxAGs76+iQln4WEmC7S5G5uufWJ0OefvcqXQ== + "@balancer-labs/sor@4.1.1-beta.4": version "4.1.1-beta.4" resolved "https://registry.yarnpkg.com/@balancer-labs/sor/-/sor-4.1.1-beta.4.tgz#9369435f8bbc781e9048f33aa496a484631b56e9"