diff --git a/package.json b/package.json index 964e571a6..cdb086212 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@paraswap/dex-lib", - "version": "4.0.1-cables.0", + "version": "4.0.2", "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": "2.21.1", "@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/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/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/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..8c612c876 --- /dev/null +++ b/src/dex/balancer-v3/balancer-v3-e2e.test.ts @@ -0,0 +1,275 @@ +/* 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 = '1000000000000000000'; + const tokenBAmount: string = '1000000000000000000'; + const nativeTokenAmount = '100000000000000'; + + testForNetwork( + network, + dexKey, + tokenASymbol, + tokenBSymbol, + tokenAAmount, + tokenBAmount, + nativeTokenAmount, + false, + ); + }); + + describe('Weighed Path', () => { + const tokenASymbol: string = 'USDCe'; + const tokenBSymbol: string = 'sDAI'; + + const tokenAAmount: string = '1000000'; + const tokenBAmount: string = '100000000000000000'; + const nativeTokenAmount = '0'; + + 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, + ); + }); + }); +}); 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..ae659cf7b --- /dev/null +++ b/src/dex/balancer-v3/balancer-v3-integration.test.ts @@ -0,0 +1,879 @@ +/* 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) + 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, + ); + } + }); + }); + }); +}); + +// 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..6b6e97575 --- /dev/null +++ b/src/dex/balancer-v3/balancer-v3-pool.ts @@ -0,0 +1,660 @@ +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 { decodeThrowError, 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'; + +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)', + ]), + }; + + 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(Object.keys(poolState)); + + // Update each pools rate + poolRates.forEach(({ poolAddress, tokenRates }, i) => { + poolState[poolAddress].tokenRates = tokenRates; + }); + + // Update state + const blockNumber = await this.dexHelper.provider.getBlockNumber(); + this.setState(poolState, blockNumber); + } + + private async getPoolRates(poolAddresses: string[]) { + // For each pool make the getPoolTokenRates call + const multiCallData = 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(multiCallData, 500); + + // Make the multicall + const multicallData = ( + await Promise.all( + slicedMultiCallData.map(async _multiCallData => + this.dexHelper.multiContract.methods + .tryAggregate(false, _multiCallData) + .call({}), + ), + ) + ).flat(); + + return poolAddresses.map((address, i) => { + const tokenRateResult = decodeThrowError( + this.interfaces['VAULT'], + 'getPoolTokenRates', + multicallData[i], + address, + ); + return { + poolAddress: address, + tokenRates: tokenRateResult.tokenRates.map((r: string) => BigInt(r)), + }; + }); + } + + // 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) { + return { + isBoosted: true, + mainToken: poolState.tokens[tokenIndex], + underlyingToken: tokenAddress, + index: tokenIndex, + rate: poolState.tokenRates[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..d670beac1 --- /dev/null +++ b/src/dex/balancer-v3/config.ts @@ -0,0 +1,46 @@ +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]: [ + // pools with rate provider that returns nested rate instead of wrap/unwrap rate + '0x6e6bb18449fcf15b79efa2cfa70acf7593088029', + '0x272d6be442e30d7c87390edeb9b96f1e84cecd8d', + ], + }, +}; + +// 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..582e798f2 --- /dev/null +++ b/src/dex/balancer-v3/getOnChainState.ts @@ -0,0 +1,326 @@ +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'; + +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, + ): CommonMutableState => { + 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, + ); +} + +// 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 multiCallData = 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(multiCallData, 500); + + const multicallData = ( + await Promise.all( + slicedMultiCallData.map(async _multiCallData => + dexHelper.multiContract.methods + .tryAggregate(false, _multiCallData) + .call({}, blockNumber), + ), + ) + ).flat(); + + let i = 0; + const poolStateMap = Object.fromEntries( + Object.entries(immutablePoolStateMap).map(([address, pool]) => { + const commonMutableData = poolOnChain['COMMON'].decode( + interfaces['VAULT'], + address, + multicallData, + i, + ) as CommonMutableState; + i = i + poolOnChain['COMMON'].count; + const poolMutableData = poolOnChain[pool.poolType].decode( + interfaces[pool.poolType], + address, + multicallData, + i, + ); + i = i + poolOnChain[pool.poolType].count; + return [ + address, + { + ...pool, + ...commonMutableData, + ...poolMutableData, + }, + ]; + }), + ); + 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..dc803977e --- /dev/null +++ b/src/dex/balancer-v3/types.ts @@ -0,0 +1,86 @@ +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[]; + 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/cables/cables.ts b/src/dex/cables/cables.ts index f9f889358..de88af43d 100644 --- a/src/dex/cables/cables.ts +++ b/src/dex/cables/cables.ts @@ -55,8 +55,6 @@ import { Interface } from 'ethers/lib/utils'; import BigNumber from 'bignumber.js'; import { ethers } from 'ethers'; import { BI_MAX_UINT256 } from '../../bigint-constants'; -import { SpecialDex } from '../../executor/types'; -import { uin256DecodeToFloat } from '../../lib/decoders'; import _ from 'lodash'; export class Cables extends SimpleExchange implements IDex { diff --git a/src/dex/index.ts b/src/dex/index.ts index bc740eb91..4161334ad 100644 --- a/src/dex/index.ts +++ b/src/dex/index.ts @@ -93,6 +93,8 @@ 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'; @@ -123,6 +125,7 @@ const Dexes = [ Swerve, BalancerV1, BalancerV2, + BalancerV3, UniswapV2, UniswapV3, Algebra, @@ -217,6 +220,7 @@ export class DexAdapterService { public routeOptimizers: IRouteOptimizer[] = [ balancerV1Merge, balancerV2Merge, + balancerV3Merge, uniswapMerge, curveV1Merge, ]; 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 2a790f6a5..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, ); } diff --git a/src/executor/Executor02BytecodeBuilder.ts b/src/executor/Executor02BytecodeBuilder.ts index 70b06cb60..90cf76be8 100644 --- a/src/executor/Executor02BytecodeBuilder.ts +++ b/src/executor/Executor02BytecodeBuilder.ts @@ -617,6 +617,7 @@ export class Executor02BytecodeBuilder extends ExecutorBytecodeBuilder< curExchangeParam.approveData.target, curExchangeParam.approveData.token, flags.approves[exchangeParamIndex], + curExchangeParam.permit2Approval, ); swapExchangeCallData = hexConcat([approveCallData, swapExchangeCallData]); @@ -634,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 dbe262762..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, ); } 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 f58d93b28..843cf67e8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -174,6 +174,7 @@ export type DexExchangeParam = { 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 d0c93b558..79bd60b47 100644 --- a/tests/constants-e2e.ts +++ b/tests/constants-e2e.ts @@ -1480,10 +1480,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, @@ -1492,10 +1500,18 @@ 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, @@ -1619,6 +1635,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: { @@ -1960,6 +2011,7 @@ export const Holders: { USDC: '0x99b31498b0a1dae01fc3433e3cb60f095340935c', }, [Network.GNOSIS]: { + COW: '0x4fFAD6ac852c0Af0AA301376F4C5Dea3a928b120', XDAI: '0x9fc062032d4F2Fe7dAA601bd8B06C45F9c8f17Be', WXDAI: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', WETH: '0x800e12aF6c96790EDDdc5B3f3302899e27B2A918', @@ -1971,8 +2023,11 @@ export const Holders: { wstETH: '0x458cD345B4C05e8DF39d0A07220feb4Ec19F5e6f', aGnowstETH: '0x458cD345B4C05e8DF39d0A07220feb4Ec19F5e6f', sDAI: '0x79f08F2e75A8C99428DE4A2e6456c07C99E55da5', + USDCe: '0x555CE236C0220695b68341bc48C68d52210cC35b', crvUSD: '0xE4A982fa1f1E8AD1AF238A7b1226b13b56bf5CcD', SWPR: '0x9467dcFD4519287e3878C018c02f5670465a9003', + waGnoWETH: '0x854B004700885A61107B458f11eCC169A019b764', + waGnowstETH: '0x854B004700885A61107B458f11eCC169A019b764', }, [Network.BASE]: { WETH: '0x4bb6b2efe7036020ba6f02a05602546c9f25bf28', @@ -2000,6 +2055,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) => { @@ -2025,6 +2089,7 @@ export const NativeTokenSymbols: { [network: number]: string } = { [Network.ARBITRUM]: 'ETH', [Network.OPTIMISM]: 'ETH', [Network.BASE]: 'ETH', + [Network.SEPOLIA]: 'ETH', [Network.GNOSIS]: 'XDAI', }; @@ -2037,5 +2102,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 f47dec85f..ba5b0cc5c 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"