diff --git a/balancer-js/README.md b/balancer-js/README.md index 9f0a62af9..8fb4e4611 100644 --- a/balancer-js/README.md +++ b/balancer-js/README.md @@ -593,6 +593,7 @@ Can exit with CS0_BPT proportionally to: DAI, USDC, USDT and FRAX * @param signer JsonRpcSigner that will sign the staticCall transaction if Static simulation chosen * @param simulationType Simulation type (VaultModel, Tenderly or Static) * @param authorisation Optional auhtorisation call to be added to the chained transaction + * @param unwrapTokens Determines if wrapped tokens should be unwrapped. Default = false * @returns transaction data ready to be sent to the network along with tokens, min and expected amounts out. */ async generalisedExit( @@ -602,7 +603,8 @@ async generalisedExit( slippage: string, signer: JsonRpcSigner, simulationType: SimulationType, - authorisation?: string + authorisation?: string, + unwrapTokens = false ): Promise<{ to: string; encodedCall: string; @@ -616,6 +618,7 @@ async generalisedExit( [Example](./examples/exitGeneralised.ts) # Factory + ### Creating Pools ### WeightedPool @@ -685,6 +688,7 @@ create({ data: BytesLike; } ``` + [Example](./examples/pools/composable-stable/create-and-init-join.ts) ### Linear Pool @@ -717,7 +721,9 @@ Builds a transaction to create a linear pool. data: BytesLike; } ``` + [Example](./examples/pools/linear/create.ts) + ## RelayerService Relayers are (user opt-in, audited) contracts that can make calls to the vault (with the transaction “sender” being any arbitrary address) and use the sender’s ERC20 vault allowance, internal balance or BPTs on their behalf. diff --git a/balancer-js/examples/exitGeneralised.ts b/balancer-js/examples/exitGeneralised.ts index 718a93c8c..b0dcce471 100644 --- a/balancer-js/examples/exitGeneralised.ts +++ b/balancer-js/examples/exitGeneralised.ts @@ -8,6 +8,7 @@ import { GraphQLArgs, Network, truncateAddresses, + removeItem, } from '../src/index'; import { forkSetup, sendTransactionGetBalances } from '../src/test/lib/utils'; import { ADDRESSES } from '../src/test/lib/constants'; @@ -17,37 +18,39 @@ import { SimulationType } from '../src/modules/simulation/simulation.module'; // Expected frontend (FE) flow: // 1. User selects BPT amount to exit a pool -// 2. FE calls exitGeneralised with simulation type VaultModel -// 3. SDK calculates expectedAmountsOut that is at least 99% accurate -// 4. User agrees expectedAmountsOut and approves relayer -// 5. With approvals in place, FE calls exitGeneralised with simulation type Static +// 2. FE calls exitInfo +// 3. SDK returns estimatedAmountsOut that is at least 99% accurate and indicates which tokens should be unwrapped (tokensToUnwrap) +// 4. User agrees estimatedAmountsOut and approves relayer +// 5. With approvals in place, FE calls exitGeneralised with simulation type Static and tokensToUnwrap // 6. SDK calculates expectedAmountsOut that is 100% accurate -// 7. SDK returns exitGeneralised transaction data with proper minAmountsOut limits in place +// 7. SDK returns exitGeneralised transaction data with proper minAmountsOut limits in place (calculated using user defined slippage) // 8. User is now able to submit a safe transaction to the blockchain dotenv.config(); -// const network = Network.GOERLI; -// const jsonRpcUrl = process.env.ALCHEMY_URL_GOERLI; -// const blockNumber = 7890980; -// const rpcUrl = 'http://127.0.0.1:8000'; -// const customSubgraphUrl = -// 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-goerli-v2-beta'; +const RPC_URLS: Record = { + [Network.MAINNET]: `http://127.0.0.1:8545`, + [Network.GOERLI]: `http://127.0.0.1:8000`, + [Network.POLYGON]: `http://127.0.0.1:8137`, + [Network.ARBITRUM]: `http://127.0.0.1:8161`, +}; -const network = Network.MAINNET; -const jsonRpcUrl = process.env.ALCHEMY_URL; -const blockNumber = 16940624; -const rpcUrl = 'http://127.0.0.1:8545'; -const customSubgraphUrl = - 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-v2'; +const FORK_NODES: Record = { + [Network.MAINNET]: `${process.env.ALCHEMY_URL}`, + [Network.GOERLI]: `${process.env.ALCHEMY_URL_GOERLI}`, + [Network.POLYGON]: `${process.env.ALCHEMY_URL_POLYGON}`, + [Network.ARBITRUM]: `${process.env.ALCHEMY_URL_ARBITRUM}`, +}; +const network = Network.MAINNET; +const blockNumber = 17263241; const addresses = ADDRESSES[network]; - +const jsonRpcUrl = FORK_NODES[network]; +const rpcUrl = RPC_URLS[network]; // bb-a-usd -const testPool = addresses.bbausd2; - +const testPool = addresses.bbgusd; // Amount of testPool BPT that will be used to exit -const amount = parseFixed('2', testPool.decimals).toString(); +const amount = parseFixed('1000000', testPool.decimals).toString(); // Setup local fork with correct balances/approval to exit bb-a-usd2 pool const setUp = async (provider: JsonRpcProvider) => { @@ -117,27 +120,25 @@ const exit = async () => { const balancer = new BalancerSDK({ network, rpcUrl, - customSubgraphUrl, subgraphQuery, }); // Use SDK to create exit transaction - const { expectedAmountsOut, tokensOut } = - await balancer.pools.generalisedExit( + const { estimatedAmountsOut, tokensOut, tokensToUnwrap } = + await balancer.pools.getExitInfo( testPool.id, amount, signerAddress, - slippage, - signer, - SimulationType.VaultModel, - undefined + signer ); // User reviews expectedAmountOut - console.log(' -- Simulating using Vault Model -- '); + console.log(' -- getExitInfo() -- '); + console.log(tokensToUnwrap.toString()); console.table({ - tokensOut: truncateAddresses([testPool.address, ...tokensOut]), - expectedAmountsOut: ['0', ...expectedAmountsOut], + tokensOut: truncateAddresses(tokensOut), + estimatedAmountsOut: estimatedAmountsOut, + unwrap: tokensOut.map((t) => tokensToUnwrap.includes(t)), }); // User approves relayer @@ -160,7 +161,8 @@ const exit = async () => { slippage, signer, SimulationType.Static, - relayerAuth + relayerAuth, + tokensToUnwrap ); // Submit transaction and check balance deltas to confirm success @@ -174,11 +176,12 @@ const exit = async () => { console.log(' -- Simulating using Static Call -- '); console.log('Price impact: ', formatFixed(query.priceImpact, 18)); + console.log(`Amount Pool Token In: ${balanceDeltas[0].toString()}`); console.table({ - tokensOut: truncateAddresses([testPool.address, ...query.tokensOut]), - minAmountsOut: ['0', ...query.minAmountsOut], - expectedAmountsOut: ['0', ...query.expectedAmountsOut], - balanceDeltas: balanceDeltas.map((b) => b.toString()), + tokensOut: truncateAddresses(query.tokensOut), + minAmountsOut: query.minAmountsOut, + expectedAmountsOut: query.expectedAmountsOut, + balanceDeltas: removeItem(balanceDeltas, 0).map((b) => b.toString()), }); }; diff --git a/balancer-js/examples/join.ts b/balancer-js/examples/join.ts index 9c2c3cfc4..2b21bb883 100644 --- a/balancer-js/examples/join.ts +++ b/balancer-js/examples/join.ts @@ -86,6 +86,7 @@ async function join() { console.log('Balances before exit: ', tokenBalancesBefore); console.log('Balances after exit: ', tokenBalancesAfter); console.log('Min BPT expected after exit: ', [minBPTOut.toString()]); + console.log('Price impact: ', priceImpact.toString()); } // yarn examples:run ./examples/join.ts diff --git a/balancer-js/examples/swapSor.ts b/balancer-js/examples/swapSor.ts index 64a96b49d..33be6192f 100644 --- a/balancer-js/examples/swapSor.ts +++ b/balancer-js/examples/swapSor.ts @@ -121,12 +121,12 @@ async function getAndProcessSwaps( } async function swapExample() { - const network = Network.GOERLI; + const network = Network.POLYGON; const rpcUrl = PROVIDER_URLS[network]; - const tokenIn = ADDRESSES[network].DAI.address; - const tokenOut = ADDRESSES[network].USDT.address; + const tokenIn = ADDRESSES[network].stMATIC.address; + const tokenOut = ADDRESSES[network].MATIC.address; const swapType = SwapTypes.SwapExactIn; - const amount = parseFixed('200', 18); + const amount = parseFixed('20', 18); // Currently Relayer only suitable for ExactIn and non-eth swaps const canUseJoinExitPaths = canUseJoinExit(swapType, tokenIn!, tokenOut!); diff --git a/balancer-js/hardhat.config.gnosis.ts b/balancer-js/hardhat.config.gnosis.ts new file mode 100644 index 000000000..6858bde0a --- /dev/null +++ b/balancer-js/hardhat.config.gnosis.ts @@ -0,0 +1,12 @@ +import '@nomiclabs/hardhat-ethers'; + +/** + * @type import('hardhat/config').HardhatUserConfig + */ +export default { + networks: { + hardhat: { + chainId: 100, + }, + }, +}; diff --git a/balancer-js/package.json b/balancer-js/package.json index 64ba6e8d8..d61e9311d 100644 --- a/balancer-js/package.json +++ b/balancer-js/package.json @@ -1,6 +1,6 @@ { "name": "@balancer-labs/sdk", - "version": "1.0.5", + "version": "1.1.0", "description": "JavaScript SDK for interacting with the Balancer Protocol V2", "license": "GPL-3.0-only", "homepage": "https://github.com/balancer-labs/balancer-sdk#readme", @@ -32,6 +32,7 @@ "node:goerli": "npx hardhat --tsconfig tsconfig.testing.json --config hardhat.config.goerli.ts node --fork $(. ./.env && echo $ALCHEMY_URL_GOERLI) --port 8000", "node:polygon": "npx hardhat --tsconfig tsconfig.testing.json --config hardhat.config.polygon.ts node --fork $(. ./.env && echo $ALCHEMY_URL_POLYGON) --port 8137", "node:arbitrum": "npx hardhat --tsconfig tsconfig.testing.json --config hardhat.config.arbitrum.ts node --fork $(. ./.env && echo $ALCHEMY_URL_ARBITRUM) --port 8161", + "node:gnosis": "npx hardhat --tsconfig tsconfig.testing.json --config hardhat.config.gnosis.ts node --fork $(. ./.env && echo $RPC_URL_GNOSIS) --port 8100", "typechain:generate": "npx typechain --target ethers-v5 --out-dir src/contracts './src/lib/abi/*.json'" }, "devDependencies": { @@ -84,7 +85,7 @@ "typescript": "^4.0.2" }, "dependencies": { - "@balancer-labs/sor": "^4.1.1-beta.8", + "@balancer-labs/sor": "^4.1.1-beta.9", "@ethersproject/abi": "^5.4.0", "@ethersproject/abstract-signer": "^5.4.0", "@ethersproject/address": "^5.4.0", diff --git a/balancer-js/src/lib/abi/BatchRelayerLibrary.json b/balancer-js/src/lib/abi/BatchRelayerLibrary.json index 3c3b2bc33..0e6c72416 100644 --- a/balancer-js/src/lib/abi/BatchRelayerLibrary.json +++ b/balancer-js/src/lib/abi/BatchRelayerLibrary.json @@ -35,7 +35,7 @@ ], "name": "approveVault", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -143,13 +143,7 @@ } ], "name": "batchSwap", - "outputs": [ - { - "internalType": "int256[]", - "name": "", - "type": "int256[]" - } - ], + "outputs": [], "stateMutability": "payable", "type": "function" }, @@ -662,13 +656,7 @@ } ], "name": "swap", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], + "outputs": [], "stateMutability": "payable", "type": "function" }, @@ -710,6 +698,39 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract ICToken", + "name": "wrappedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "unwrapCompoundV2", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { @@ -743,6 +764,171 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract IEulerToken", + "name": "wrappedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "unwrapEuler", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IGearboxDieselToken", + "name": "wrappedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "dieselAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "unwrapGearbox", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IReaperTokenVault", + "name": "vaultToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "unwrapReaperVaultToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IShareToken", + "name": "wrappedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "unwrapShareToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ITetuSmartVault", + "name": "wrappedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "unwrapTetu", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { @@ -804,6 +990,39 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract IYearnTokenVault", + "name": "wrappedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "unwrapYearn", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { @@ -933,6 +1152,39 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract ICToken", + "name": "wrappedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "wrapCompoundV2", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { @@ -966,6 +1218,143 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract IEulerToken", + "name": "wrappedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "eulerProtocol", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "wrapEuler", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IGearboxDieselToken", + "name": "wrappedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "mainAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "wrapGearbox", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IReaperTokenVault", + "name": "vaultToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "wrapReaperVaultToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IShareToken", + "name": "wrappedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "wrapShareToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { @@ -994,6 +1383,39 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract ITetuSmartVault", + "name": "wrappedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "wrapTetu", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { @@ -1026,5 +1448,38 @@ "outputs": [], "stateMutability": "payable", "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IYearnTokenVault", + "name": "wrappedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "wrapYearn", + "outputs": [], + "stateMutability": "payable", + "type": "function" } ] diff --git a/balancer-js/src/lib/abi/gyroEV2.json b/balancer-js/src/lib/abi/gyroEV2.json new file mode 100644 index 000000000..f7175fc92 --- /dev/null +++ b/balancer-js/src/lib/abi/gyroEV2.json @@ -0,0 +1,1968 @@ +[ + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "contract IVault", + "name": "vault", + "type": "address" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "contract IERC20", + "name": "token0", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "token1", + "type": "address" + }, + { + "internalType": "uint256", + "name": "swapFeePercentage", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pauseWindowDuration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "bufferPeriodDuration", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "oracleEnabled", + "type": "bool" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "internalType": "struct ExtensibleWeightedPool2Tokens.NewPoolParams", + "name": "baseParams", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "int256", + "name": "alpha", + "type": "int256" + }, + { + "internalType": "int256", + "name": "beta", + "type": "int256" + }, + { + "internalType": "int256", + "name": "c", + "type": "int256" + }, + { + "internalType": "int256", + "name": "s", + "type": "int256" + }, + { + "internalType": "int256", + "name": "lambda", + "type": "int256" + } + ], + "internalType": "struct GyroECLPMath.Params", + "name": "eclpParams", + "type": "tuple" + }, + { + "components": [ + { + "components": [ + { + "internalType": "int256", + "name": "x", + "type": "int256" + }, + { + "internalType": "int256", + "name": "y", + "type": "int256" + } + ], + "internalType": "struct GyroECLPMath.Vector2", + "name": "tauAlpha", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "int256", + "name": "x", + "type": "int256" + }, + { + "internalType": "int256", + "name": "y", + "type": "int256" + } + ], + "internalType": "struct GyroECLPMath.Vector2", + "name": "tauBeta", + "type": "tuple" + }, + { + "internalType": "int256", + "name": "u", + "type": "int256" + }, + { + "internalType": "int256", + "name": "v", + "type": "int256" + }, + { + "internalType": "int256", + "name": "w", + "type": "int256" + }, + { + "internalType": "int256", + "name": "z", + "type": "int256" + }, + { + "internalType": "int256", + "name": "dSq", + "type": "int256" + } + ], + "internalType": "struct GyroECLPMath.DerivedParams", + "name": "derivedEclpParams", + "type": "tuple" + }, + { + "internalType": "address", + "name": "rateProvider0", + "type": "address" + }, + { + "internalType": "address", + "name": "rateProvider1", + "type": "address" + }, + { + "internalType": "address", + "name": "capManager", + "type": "address" + }, + { + "components": [ + { + "internalType": "bool", + "name": "capEnabled", + "type": "bool" + }, + { + "internalType": "uint120", + "name": "perAddressCap", + "type": "uint120" + }, + { + "internalType": "uint128", + "name": "globalCap", + "type": "uint128" + } + ], + "internalType": "struct ICappedLiquidity.CapParams", + "name": "capParams", + "type": "tuple" + }, + { + "internalType": "address", + "name": "pauseManager", + "type": "address" + } + ], + "internalType": "struct GyroECLPPool.GyroParams", + "name": "params", + "type": "tuple" + }, + { + "internalType": "address", + "name": "configAddress", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "capManager", + "type": "address" + } + ], + "name": "CapManagerUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "capEnabled", + "type": "bool" + }, + { + "internalType": "uint120", + "name": "perAddressCap", + "type": "uint120" + }, + { + "internalType": "uint128", + "name": "globalCap", + "type": "uint128" + } + ], + "indexed": false, + "internalType": "struct ICappedLiquidity.CapParams", + "name": "params", + "type": "tuple" + } + ], + "name": "CapParamsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "derivedParamsValidated", + "type": "bool" + } + ], + "name": "ECLPDerivedParamsValidated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "paramsValidated", + "type": "bool" + } + ], + "name": "ECLPParamsValidated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "invariantAfterJoin", + "type": "uint256" + } + ], + "name": "InvariantAterInitializeJoin", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldInvariant", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newInvariant", + "type": "uint256" + } + ], + "name": "InvariantOldAndNew", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "name": "OracleEnabledChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oracleUpdatedIndex", + "type": "uint256" + } + ], + "name": "OracleIndexUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldPauseManager", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newPauseManager", + "type": "address" + } + ], + "name": "PauseManagerChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "PausedLocally", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "paused", + "type": "bool" + } + ], + "name": "PausedStateChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "swapFeePercentage", + "type": "uint256" + } + ], + "name": "SwapFeePercentageChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256[]", + "name": "balances", + "type": "uint256[]" + }, + { + "components": [ + { + "internalType": "int256", + "name": "x", + "type": "int256" + }, + { + "internalType": "int256", + "name": "y", + "type": "int256" + } + ], + "indexed": false, + "internalType": "struct GyroECLPMath.Vector2", + "name": "invariant", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "SwapParams", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "UnpausedLocally", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "_dSq", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "_paramsAlpha", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "_paramsBeta", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "_paramsC", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "_paramsLambda", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "_paramsS", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "_tauAlphaX", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "_tauAlphaY", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "_tauBetaX", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "_tauBetaY", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "_u", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "_v", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "_w", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "_z", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "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": "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": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "capManager", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "capParams", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "capEnabled", + "type": "bool" + }, + { + "internalType": "uint120", + "name": "perAddressCap", + "type": "uint120" + }, + { + "internalType": "uint128", + "name": "globalCap", + "type": "uint128" + } + ], + "internalType": "struct ICappedLiquidity.CapParams", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_pauseManager", + "type": "address" + } + ], + "name": "changePauseManager", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "startIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endIndex", + "type": "uint256" + } + ], + "name": "dirtyUninitializedOracleSamples", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "enableOracle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "selector", + "type": "bytes4" + } + ], + "name": "getActionId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAuthorizer", + "outputs": [ + { + "internalType": "contract IAuthorizer", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getECLPParams", + "outputs": [ + { + "components": [ + { + "internalType": "int256", + "name": "alpha", + "type": "int256" + }, + { + "internalType": "int256", + "name": "beta", + "type": "int256" + }, + { + "internalType": "int256", + "name": "c", + "type": "int256" + }, + { + "internalType": "int256", + "name": "s", + "type": "int256" + }, + { + "internalType": "int256", + "name": "lambda", + "type": "int256" + } + ], + "internalType": "struct GyroECLPMath.Params", + "name": "params", + "type": "tuple" + }, + { + "components": [ + { + "components": [ + { + "internalType": "int256", + "name": "x", + "type": "int256" + }, + { + "internalType": "int256", + "name": "y", + "type": "int256" + } + ], + "internalType": "struct GyroECLPMath.Vector2", + "name": "tauAlpha", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "int256", + "name": "x", + "type": "int256" + }, + { + "internalType": "int256", + "name": "y", + "type": "int256" + } + ], + "internalType": "struct GyroECLPMath.Vector2", + "name": "tauBeta", + "type": "tuple" + }, + { + "internalType": "int256", + "name": "u", + "type": "int256" + }, + { + "internalType": "int256", + "name": "v", + "type": "int256" + }, + { + "internalType": "int256", + "name": "w", + "type": "int256" + }, + { + "internalType": "int256", + "name": "z", + "type": "int256" + }, + { + "internalType": "int256", + "name": "dSq", + "type": "int256" + } + ], + "internalType": "struct GyroECLPMath.DerivedParams", + "name": "d", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getInvariant", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLargestSafeQueryWindow", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "getLastInvariant", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum IPriceOracle.Variable", + "name": "variable", + "type": "uint8" + } + ], + "name": "getLatest", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMiscData", + "outputs": [ + { + "internalType": "int256", + "name": "logInvariant", + "type": "int256" + }, + { + "internalType": "int256", + "name": "logTotalSupply", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "oracleSampleCreationTimestamp", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "oracleIndex", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "oracleEnabled", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "swapFeePercentage", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNormalizedWeights", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "enum IPriceOracle.Variable", + "name": "variable", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "ago", + "type": "uint256" + } + ], + "internalType": "struct IPriceOracle.OracleAccumulatorQuery[]", + "name": "queries", + "type": "tuple[]" + } + ], + "name": "getPastAccumulators", + "outputs": [ + { + "internalType": "int256[]", + "name": "results", + "type": "int256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPausedState", + "outputs": [ + { + "internalType": "bool", + "name": "paused", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "pauseWindowEndTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "bufferPeriodEndTime", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPoolId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "spotPrice", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "getSample", + "outputs": [ + { + "internalType": "int256", + "name": "logPairPrice", + "type": "int256" + }, + { + "internalType": "int256", + "name": "accLogPairPrice", + "type": "int256" + }, + { + "internalType": "int256", + "name": "logBptPrice", + "type": "int256" + }, + { + "internalType": "int256", + "name": "accLogBptPrice", + "type": "int256" + }, + { + "internalType": "int256", + "name": "logInvariant", + "type": "int256" + }, + { + "internalType": "int256", + "name": "accLogInvariant", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getSwapFeePercentage", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "enum IPriceOracle.Variable", + "name": "variable", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "secs", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ago", + "type": "uint256" + } + ], + "internalType": "struct IPriceOracle.OracleAverageQuery[]", + "name": "queries", + "type": "tuple[]" + } + ], + "name": "getTimeWeightedAverage", + "outputs": [ + { + "internalType": "uint256[]", + "name": "results", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getTokenRates", + "outputs": [ + { + "internalType": "uint256", + "name": "rate0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "rate1", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getTotalSamples", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "getVault", + "outputs": [ + { + "internalType": "contract IVault", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gyroConfig", + "outputs": [ + { + "internalType": "contract IGyroConfig", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "balances", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "lastChangeBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "protocolSwapFeePercentage", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "onExitPool", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "balances", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "lastChangeBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "protocolSwapFeePercentage", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "onJoinPool", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amountsIn", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "dueProtocolFeeAmounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "enum IVault.SwapKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "lastChangeBlock", + "type": "uint256" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IPoolSwapStructs.SwapRequest", + "name": "request", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "balanceTokenIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "balanceTokenOut", + "type": "uint256" + } + ], + "name": "onSwap", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "pauseManager", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "balances", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "lastChangeBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "protocolSwapFeePercentage", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "queryExit", + "outputs": [ + { + "internalType": "uint256", + "name": "bptIn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "balances", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "lastChangeBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "protocolSwapFeePercentage", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "queryJoin", + "outputs": [ + { + "internalType": "uint256", + "name": "bptOut", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "amountsIn", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rateProvider0", + "outputs": [ + { + "internalType": "contract IRateProvider", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rateProvider1", + "outputs": [ + { + "internalType": "contract IRateProvider", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_capManager", + "type": "address" + } + ], + "name": "setCapManager", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "capEnabled", + "type": "bool" + }, + { + "internalType": "uint120", + "name": "perAddressCap", + "type": "uint120" + }, + { + "internalType": "uint128", + "name": "globalCap", + "type": "uint128" + } + ], + "internalType": "struct ICappedLiquidity.CapParams", + "name": "params", + "type": "tuple" + } + ], + "name": "setCapParams", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "paused", + "type": "bool" + } + ], + "name": "setPaused", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "swapFeePercentage", + "type": "uint256" + } + ], + "name": "setSwapFeePercentage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/balancer-js/src/lib/constants/config.ts b/balancer-js/src/lib/constants/config.ts index d58189ac9..3e5f9a166 100644 --- a/balancer-js/src/lib/constants/config.ts +++ b/balancer-js/src/lib/constants/config.ts @@ -398,6 +398,74 @@ export const BALANCER_NETWORK_CONFIG: Record = { }, ], }, + [Network.SEPOLIA]: { + chainId: Network.SEPOLIA, //11155111 + addresses: { + contracts: { + aaveLinearPoolFactory: '0xdf9b5b00ef9bca66e9902bd813db14e4343be025', + balancerHelpers: '0xdae7e32adc5d490a43ccba1f0c736033f2b4efca', + balancerMinterAddress: '0x1783cd84b3d01854a96b4ed5843753c2ccbd574a', + composableStablePoolFactory: + '0xa3fd20e29358c056b727657e83dfd139abbc9924', + erc4626LinearPoolFactory: '0x59562f93c447656f6e4799fc1fc7c3d977c3324f', + feeDistributor: '0xa6971317fb06c76ef731601c64433a4846fca707', + gaugeController: '0x577e5993b9cc480f07f98b5ebd055604bd9071c4', + gearboxLinearPoolFactory: '0x8df317a729fcaa260306d7de28888932cb579b88', + multicall: '0x25eef291876194aefad0d60dff89e268b90754bb', + protocolFeePercentagesProvider: + '0xf7d5dce55e6d47852f054697bab6a1b48a00ddbd', + relayer: '0x6d5342d716c13d9a3f072a2b11498624ade27f90', + vault: '0xba12222222228d8ba445958a75a0704d566bf2c8', + weightedPoolFactory: '0x7920bfa1b2041911b354747ca7a6cdd2dfc50cfd', + yearnLinearPoolFactory: '0xacf05be5134d64d150d153818f8c67ee36996650', + }, + tokens: { + bal: '0xb19382073c7a0addbb56ac6af1808fa49e377b75', + wrappedNativeAsset: '0x7b79995e5f793a07bc00c21412e50ecae098e7f9', + }, + }, + urls: { + subgraph: + 'https://api.studio.thegraph.com/proxy/24660/balancer-sepolia-v2/v0.0.1', + }, + pools: {}, + poolsToIgnore: [], + sorConnectingTokens: [], + }, + [Network.ZKEVM]: { + chainId: Network.ZKEVM, //1101 + addresses: { + contracts: { + aaveLinearPoolFactory: '0x4b7b369989e613ff2C65768B7Cf930cC927F901E', + balancerHelpers: '0x8E9aa87E45e92bad84D5F8DD1bff34Fb92637dE9', + balancerMinterAddress: '0x475D18169BE8a89357A9ee3Ab00ca386d20fA229', + composableStablePoolFactory: + '0x8eA89804145c007e7D226001A96955ad53836087', + erc4626LinearPoolFactory: '0x6B1Da720Be2D11d95177ccFc40A917c2688f396c', + feeDistributor: '', + gaugeController: '', + gearboxLinearPoolFactory: '0x687b8C9b41E01Be8B591725fac5d5f52D0564d79', + multicall: '0xcA11bde05977b3631167028862bE2a173976CA11', + protocolFeePercentagesProvider: + '0x1802953277FD955f9a254B80Aa0582f193cF1d77', + relayer: '0x4678731DC41142A902a114aC5B2F77b63f4a259D', + vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', + weightedPoolFactory: '0x03F3Fb107e74F2EAC9358862E91ad3c692712054', + yearnLinearPoolFactory: '', + }, + tokens: { + bal: '0x120eF59b80774F02211563834d8E3b72cb1649d6', + wrappedNativeAsset: '0x4F9A0e7FD2Bf6067db6994CF12E4495Df938E6e9', + }, + }, + urls: { + subgraph: + 'https://api.studio.thegraph.com/query/24660/balancer-polygon-zkevm-v2/v0.0.2', + }, + pools: {}, + poolsToIgnore: [], + sorConnectingTokens: [], + }, }; export const networkAddresses = ( diff --git a/balancer-js/src/lib/constants/network.ts b/balancer-js/src/lib/constants/network.ts index e920fb4ec..b4876a0e1 100644 --- a/balancer-js/src/lib/constants/network.ts +++ b/balancer-js/src/lib/constants/network.ts @@ -8,6 +8,8 @@ export enum Network { KOVAN = 42, GNOSIS = 100, POLYGON = 137, + ZKEVM = 1101, ARBITRUM = 42161, FANTOM = 250, + SEPOLIA = 11155111, } diff --git a/balancer-js/src/modules/exits/exits.module.integration-mainnet.spec.ts b/balancer-js/src/modules/exits/exits.module.integration-mainnet.spec.ts index fa6dfc9cb..2ea09858b 100644 --- a/balancer-js/src/modules/exits/exits.module.integration-mainnet.spec.ts +++ b/balancer-js/src/modules/exits/exits.module.integration-mainnet.spec.ts @@ -1,256 +1,221 @@ // yarn test:only ./src/modules/exits/exits.module.integration-mainnet.spec.ts import dotenv from 'dotenv'; import { expect } from 'chai'; - -import { BalancerSDK, GraphQLQuery, GraphQLArgs, Network } from '@/.'; +import { Network } from '@/.'; import { BigNumber, parseFixed } from '@ethersproject/bignumber'; -import { JsonRpcProvider } from '@ethersproject/providers'; -import { Contracts } from '@/modules/contracts/contracts.module'; -import { accuracy, forkSetup, getBalances } from '@/test/lib/utils'; +import { accuracy } from '@/test/lib/utils'; import { ADDRESSES } from '@/test/lib/constants'; -import { Relayer } from '@/modules/relayer/relayer.module'; -import { JsonRpcSigner } from '@ethersproject/providers'; -import { SimulationType } from '../simulation/simulation.module'; - -/** - * -- Integration tests for generalisedExit -- - * - * It compares results from local fork transactions with simulated results from - * the Simulation module, which can be of 3 different types: - * 1. Tenderly: uses Tenderly Simulation API (third party service) - * 2. VaultModel: uses TS math, which may be less accurate (min. 99% accuracy) - * 3. Static: uses staticCall, which is 100% accurate but requires vault approval - */ +import { testFlow } from './testHelper'; dotenv.config(); -const TEST_BBEUSD = true; - -/* - * Testing on MAINNET - * - Run node on terminal: yarn run node - * - Uncomment section below: - */ -const network = Network.MAINNET; -const blockNumber = 16685902; -const customSubgraphUrl = - 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-v2'; -const { ALCHEMY_URL: jsonRpcUrl } = process.env; -const rpcUrl = 'http://127.0.0.1:8545'; - -const addresses = ADDRESSES[network]; - -// Set tenderly config blockNumber and use default values for other parameters -const tenderlyConfig = { - blockNumber, -}; - -/** - * Example of subgraph query that allows filtering pools. - * Might be useful to reduce the response time by limiting the amount of pool - * data that will be queried by the SDK. Specially when on chain data is being - * fetched as well. - */ -const poolAddresses = Object.values(addresses).map( - (address) => address.address -); -const subgraphArgs: GraphQLArgs = { - where: { - swapEnabled: { - eq: true, - }, - totalShares: { - gt: 0.000000000001, - }, - address: { - in: poolAddresses, - }, - }, - orderBy: 'totalLiquidity', - orderDirection: 'desc', - block: { number: blockNumber }, -}; -const subgraphQuery: GraphQLQuery = { args: subgraphArgs, attrs: {} }; - -const sdk = new BalancerSDK({ - network, - rpcUrl, - customSubgraphUrl, - tenderly: tenderlyConfig, - subgraphQuery, -}); -const { pools } = sdk; -const provider = new JsonRpcProvider(rpcUrl, network); -const signer = provider.getSigner(1); // signer 0 at this blockNumber has DAI balance and tests were failling -const { contracts, contractAddresses } = new Contracts( - network as number, - provider -); -const relayer = contractAddresses.relayer; +const TEST_BBAUSD3 = true; + +describe('generalised exit execution', async function () { + this.timeout(120000); // Sets timeout for all tests within this scope to 2 minutes + + context('ERC4626 - bbausd3', async () => { + if (!TEST_BBAUSD3) return true; + const network = Network.MAINNET; + const blockNo = 17223300; + const pool = ADDRESSES[network].bbausd3; + const slippage = '10'; // 10 bps = 0.1% + let unwrappingTokensAmountsOut: string[]; + let unwrappingTokensGasUsed: BigNumber; + let mainTokensAmountsOut: string[]; + let mainTokensGasUsed: BigNumber; + const poolAddresses = Object.values(ADDRESSES[network]).map( + (address) => address.address + ); -interface Test { - signer: JsonRpcSigner; - description: string; - pool: { - id: string; - address: string; - }; - amount: string; - authorisation: string | undefined; - simulationType?: SimulationType; -} + const amountRatio = 10; + // Amount greater than the underlying main token balance, which will cause the exit to be unwrapped + const unwrapExitAmount = parseFixed('6000000', pool.decimals); + // Amount smaller than the underlying main token balance, which will cause the exit to be done directly + const mainExitAmount = unwrapExitAmount.div(amountRatio); + + context('exit by unwrapping tokens', async () => { + it('should exit via unwrapping', async () => { + const { expectedAmountsOut, gasUsed } = await testFlow( + pool, + slippage, + unwrapExitAmount.toString(), + [ADDRESSES[network].USDC.address], + network, + blockNo, + poolAddresses + ); + unwrappingTokensAmountsOut = expectedAmountsOut; + unwrappingTokensGasUsed = gasUsed; + }); + }); -const runTests = async (tests: Test[]) => { - for (let i = 0; i < tests.length; i++) { - const test = tests[i]; - it(test.description, async () => { - const signerAddress = await test.signer.getAddress(); - const authorisation = await Relayer.signRelayerApproval( - relayer, - signerAddress, - test.signer, - contracts.vault - ); - await testFlow( - test.signer, - signerAddress, - test.pool, - test.amount, - authorisation, - test.simulationType - ); - }).timeout(120000); - } -}; + context('exit to main tokens directly', async () => { + it('should exit to main tokens directly', async () => { + const { expectedAmountsOut, gasUsed } = await testFlow( + pool, + slippage, + mainExitAmount.toString(), + [], + network, + blockNo, + poolAddresses + ); + mainTokensAmountsOut = expectedAmountsOut; + mainTokensGasUsed = gasUsed; + }); + }); -const testFlow = async ( - signer: JsonRpcSigner, - signerAddress: string, - pool: { id: string; address: string }, - amount: string, - authorisation: string | undefined, - simulationType = SimulationType.VaultModel -) => { - const gasLimit = 8e6; - const slippage = '10'; // 10 bps = 0.1% + context('exit by unwrapping vs exit to main tokens', async () => { + it('should return similar amounts (proportional to the input)', async () => { + mainTokensAmountsOut.forEach((amount, i) => { + const unwrappedAmount = BigNumber.from( + unwrappingTokensAmountsOut[i] + ).div(amountRatio); + expect( + accuracy(unwrappedAmount, BigNumber.from(amount)) + ).to.be.closeTo(1, 1e-4); // inaccuracy should not be over 1 bps + }); + }); + it('should spend more gas when unwrapping tokens', async () => { + expect(unwrappingTokensGasUsed.gt(mainTokensGasUsed)).to.be.true; + }); + }); + }); - const { to, encodedCall, tokensOut, expectedAmountsOut, minAmountsOut } = - await pools.generalisedExit( - pool.id, - amount, - signerAddress, - slippage, - signer, - simulationType, - authorisation + context('GearboxLinear - bbgusd', async () => { + const network = Network.MAINNET; + const blockNo = 17263241; + const pool = ADDRESSES[network].bbgusd; + const slippage = '10'; // 10 bps = 0.1% + let unwrappingTokensAmountsOut: string[]; + let unwrappingTokensGasUsed: BigNumber; + let mainTokensAmountsOut: string[]; + let mainTokensGasUsed: BigNumber; + const poolAddresses = Object.values(ADDRESSES[network]).map( + (address) => address.address ); - const [bptBalanceBefore, ...tokensOutBalanceBefore] = await getBalances( - [pool.address, ...tokensOut], - signer, - signerAddress - ); - - const response = await signer.sendTransaction({ - to, - data: encodedCall, - gasLimit, - }); - - const receipt = await response.wait(); - console.log('Gas used', receipt.gasUsed.toString()); + const amountRatio = 100000; + // Amount greater than the underlying main token balance, which will cause the exit to be unwrapped + const unwrapExitAmount = parseFixed('1000000', pool.decimals); + // Amount smaller than the underlying main token balance, which will cause the exit to be done directly + const mainExitAmount = unwrapExitAmount.div(amountRatio); + + context('exit by unwrapping tokens', async () => { + it('should exit via unwrapping', async () => { + const { expectedAmountsOut, gasUsed } = await testFlow( + pool, + slippage, + unwrapExitAmount.toString(), + [ADDRESSES[network].DAI.address, ADDRESSES[network].USDC.address], + network, + blockNo, + poolAddresses + ); + unwrappingTokensAmountsOut = expectedAmountsOut; + unwrappingTokensGasUsed = gasUsed; + }); + }); - const [bptBalanceAfter, ...tokensOutBalanceAfter] = await getBalances( - [pool.address, ...tokensOut], - signer, - signerAddress - ); + context('exit to main tokens directly', async () => { + it('should exit to main tokens directly', async () => { + const { expectedAmountsOut, gasUsed } = await testFlow( + pool, + slippage, + mainExitAmount.toString(), + [], + network, + blockNo, + poolAddresses + ); + mainTokensAmountsOut = expectedAmountsOut; + mainTokensGasUsed = gasUsed; + }); + }); - console.table({ - tokensOut: tokensOut.map((t) => `${t.slice(0, 6)}...${t.slice(38, 42)}`), - minOut: minAmountsOut, - expectedOut: expectedAmountsOut, - balanceAfter: tokensOutBalanceAfter.map((b) => b.toString()), - balanceBefore: tokensOutBalanceBefore.map((b) => b.toString()), + context('exit by unwrapping vs exit to main tokens', async () => { + it('should return similar amounts (proportional to the input)', async () => { + mainTokensAmountsOut.forEach((amount, i) => { + const unwrappedAmount = BigNumber.from( + unwrappingTokensAmountsOut[i] + ).div(amountRatio); + expect( + accuracy(unwrappedAmount, BigNumber.from(amount)) + ).to.be.closeTo(1, 1e-4); // inaccuracy should not be over 1 bps + }); + }); + it('should spend more gas when unwrapping tokens', async () => { + expect(unwrappingTokensGasUsed.gt(mainTokensGasUsed)).to.be.true; + }); + }); }); - expect(receipt.status).to.eql(1); - minAmountsOut.forEach((minAmountOut) => { - expect(BigNumber.from(minAmountOut).gte('0')).to.be.true; - }); - expectedAmountsOut.forEach((expectedAmountOut, i) => { - expect( - BigNumber.from(expectedAmountOut).gte(BigNumber.from(minAmountsOut[i])) - ).to.be.true; - }); - expect(bptBalanceAfter.eq(bptBalanceBefore.sub(amount))).to.be.true; - tokensOutBalanceBefore.forEach((b) => expect(b.eq(0)).to.be.true); - tokensOutBalanceAfter.forEach((balanceAfter, i) => { - const minOut = BigNumber.from(minAmountsOut[i]); - expect(balanceAfter.gte(minOut)).to.be.true; - const expectedOut = BigNumber.from(expectedAmountsOut[i]); - expect(accuracy(balanceAfter, expectedOut)).to.be.closeTo(1, 1e-2); // inaccuracy should not be over to 1% - }); -}; + context('AaveLinear - bbausd', async () => { + const network = Network.MAINNET; + const blockNo = 17263241; + const pool = ADDRESSES[network].bbausd2; + const slippage = '10'; // 10 bps = 0.1% + let unwrappingTokensAmountsOut: string[]; + let unwrappingTokensGasUsed: BigNumber; + let mainTokensAmountsOut: string[]; + let mainTokensGasUsed: BigNumber; + const poolAddresses = Object.values(ADDRESSES[network]).map( + (address) => address.address + ); -// Skipping Euler specific tests while eTokens transactions are paused -describe.skip('generalised exit execution', async () => { - /* - bbeusd: ComposableStable, bbeusdt/bbeusdc/bbedai - bbeusdt: Linear, eUsdt/usdt - bbeusdc: Linear, eUsdc/usdc - bbedai: Linear, eDai/dai - */ - context('bbeusd', async () => { - if (!TEST_BBEUSD) return true; - let authorisation: string | undefined; - beforeEach(async () => { - const tokens = [addresses.bbeusd.address]; - const slots = [addresses.bbeusd.slot]; - const balances = [parseFixed('2', addresses.bbeusd.decimals).toString()]; - await forkSetup( - signer, - tokens, - slots, - balances, - jsonRpcUrl as string, - blockNumber - ); + const amountRatio = 1000; + // Amount greater than the underlying main token balance, which will cause the exit to be unwrapped + const unwrapExitAmount = parseFixed('3000000', pool.decimals); + // Amount smaller than the underlying main token balance, which will cause the exit to be done directly + const mainExitAmount = unwrapExitAmount.div(amountRatio); + + context('exit by unwrapping tokens', async () => { + it('should exit via unwrapping', async () => { + const { expectedAmountsOut, gasUsed } = await testFlow( + pool, + slippage, + unwrapExitAmount.toString(), + [ADDRESSES[network].DAI.address], + network, + blockNo, + poolAddresses + ); + unwrappingTokensAmountsOut = expectedAmountsOut; + unwrappingTokensGasUsed = gasUsed; + }); }); - await runTests([ - { - signer, - description: 'exit pool', - pool: { - id: addresses.bbeusd.id, - address: addresses.bbeusd.address, - }, - amount: parseFixed('2', addresses.bbeusd.decimals).toString(), - authorisation: authorisation, - simulationType: SimulationType.Tenderly, - }, - { - signer, - description: 'exit pool', - pool: { - id: addresses.bbeusd.id, - address: addresses.bbeusd.address, - }, - amount: parseFixed('2', addresses.bbeusd.decimals).toString(), - authorisation: authorisation, - simulationType: SimulationType.Static, - }, - { - signer, - description: 'exit pool', - pool: { - id: addresses.bbeusd.id, - address: addresses.bbeusd.address, - }, - amount: parseFixed('2', addresses.bbeusd.decimals).toString(), - authorisation: authorisation, - }, - ]); + context('exit to main tokens directly', async () => { + it('should exit to main tokens directly', async () => { + const { expectedAmountsOut, gasUsed } = await testFlow( + pool, + slippage, + mainExitAmount.toString(), + [], + network, + blockNo, + poolAddresses + ); + mainTokensAmountsOut = expectedAmountsOut; + mainTokensGasUsed = gasUsed; + }); + }); + + context('exit by unwrapping vs exit to main tokens', async () => { + it('should return similar amounts (proportional to the input)', async () => { + mainTokensAmountsOut.forEach((amount, i) => { + const unwrappedAmount = BigNumber.from( + unwrappingTokensAmountsOut[i] + ).div(amountRatio); + expect( + accuracy(unwrappedAmount, BigNumber.from(amount)) + ).to.be.closeTo(1, 1e-3); // inaccuracy should not be over 10 bps + }); + }); + it('should spend more gas when unwrapping tokens', async () => { + expect(unwrappingTokensGasUsed.gt(mainTokensGasUsed)).to.be.true; + }); + }); }); }); diff --git a/balancer-js/src/modules/exits/exits.module.integration.spec.ts b/balancer-js/src/modules/exits/exits.module.integration.spec.ts index 79b65a0eb..bd35a25c9 100644 --- a/balancer-js/src/modules/exits/exits.module.integration.spec.ts +++ b/balancer-js/src/modules/exits/exits.module.integration.spec.ts @@ -1,36 +1,9 @@ // yarn test:only ./src/modules/exits/exits.module.integration.spec.ts import dotenv from 'dotenv'; -import { expect } from 'chai'; -import { BigNumber, parseFixed } from '@ethersproject/bignumber'; -import { JsonRpcProvider } from '@ethersproject/providers'; - -import { - BalancerSDK, - GraphQLQuery, - GraphQLArgs, - Network, - truncateAddresses, -} from '@/.'; -import { subSlippage } from '@/lib/utils/slippageHelper'; -import { Relayer } from '@/modules/relayer/relayer.module'; -import { SimulationType } from '@/modules/simulation/simulation.module'; -import { Contracts } from '@/modules/contracts/contracts.module'; -import { - accuracy, - forkSetup, - sendTransactionGetBalances, -} from '@/test/lib/utils'; +import { parseFixed } from '@ethersproject/bignumber'; +import { Network } from '@/.'; import { ADDRESSES } from '@/test/lib/constants'; - -/** - * -- Integration tests for generalisedExit -- - * - * It compares results from local fork transactions with simulated results from - * the Simulation module, which can be of 3 different types: - * 1. Tenderly: uses Tenderly Simulation API (third party service) - * 2. VaultModel: uses TS math, which may be less accurate (min. 99% accuracy) - * 3. Static: uses staticCall, which is 100% accurate but requires vault approval - */ +import { testFlow } from './testHelper'; dotenv.config(); @@ -44,148 +17,13 @@ const TEST_BOOSTED_WEIGHTED_META = true; const TEST_BOOSTED_WEIGHTED_META_ALT = true; const TEST_BOOSTED_WEIGHTED_META_GENERAL = true; -/* - * Testing on GOERLI - * - Run node on terminal: yarn run node:goerli - * - Uncomment section below: - */ const network = Network.GOERLI; const blockNumber = 8744170; -const { ALCHEMY_URL_GOERLI: jsonRpcUrl } = process.env; -const rpcUrl = 'http://127.0.0.1:8000'; - -/* - * Testing on MAINNET - * - Run node on terminal: yarn run node - * - Uncomment section below: - */ -// const network = Network.MAINNET; -// const blockNumber = 15519886; -// const customSubgraphUrl = -// 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-v2-beta'; -// const { ALCHEMY_URL: jsonRpcUrl } = process.env; -// const rpcUrl = 'http://127.0.0.1:8545'; - -const addresses = ADDRESSES[network]; - -// Set tenderly config blockNumber and use default values for other parameters -const tenderlyConfig = { - blockNumber, -}; - -/** - * Example of subgraph query that allows filtering pools. - * Might be useful to reduce the response time by limiting the amount of pool - * data that will be queried by the SDK. Specially when on chain data is being - * fetched as well. - */ -const poolAddresses = Object.values(addresses).map( +const slippage = '10'; // 10 bps = 0.1% +const poolAddresses = Object.values(ADDRESSES[network]).map( (address) => address.address ); -const subgraphArgs: GraphQLArgs = { - where: { - swapEnabled: { - eq: true, - }, - totalShares: { - gt: 0.000000000001, - }, - address: { - in: poolAddresses, - }, - }, - orderBy: 'totalLiquidity', - orderDirection: 'desc', - block: { number: blockNumber }, -}; -const subgraphQuery: GraphQLQuery = { args: subgraphArgs, attrs: {} }; - -const sdk = new BalancerSDK({ - network, - rpcUrl, - tenderly: tenderlyConfig, - subgraphQuery, -}); -const { pools } = sdk; -const provider = new JsonRpcProvider(rpcUrl, network); -const signer = provider.getSigner(); -const { contracts, contractAddresses } = new Contracts( - network as number, - provider -); -const relayer = contractAddresses.relayer; - -const testFlow = async ( - pool: { id: string; address: string; slot: number }, - amount: string, - simulationType = SimulationType.VaultModel -) => { - const slippage = '10'; // 10 bps = 0.1% - - const tokens = [pool.address]; - const slots = [pool.slot]; - const balances = [amount]; - - await forkSetup( - signer, - tokens, - slots, - balances, - jsonRpcUrl as string, - blockNumber - ); - - const signerAddress = await signer.getAddress(); - const authorisation = await Relayer.signRelayerApproval( - relayer, - signerAddress, - signer, - contracts.vault - ); - - const { to, encodedCall, tokensOut, expectedAmountsOut, minAmountsOut } = - await pools.generalisedExit( - pool.id, - amount, - signerAddress, - slippage, - signer, - simulationType, - authorisation - ); - - const { transactionReceipt, balanceDeltas, gasUsed } = - await sendTransactionGetBalances( - tokensOut, - signer, - signerAddress, - to, - encodedCall - ); - - console.log('Gas used', gasUsed.toString()); - - console.table({ - tokensOut: truncateAddresses(tokensOut), - minOut: minAmountsOut, - expectedOut: expectedAmountsOut, - balanceDeltas: balanceDeltas.map((b) => b.toString()), - }); - - expect(transactionReceipt.status).to.eq(1); - balanceDeltas.forEach((b, i) => { - const minOut = BigNumber.from(minAmountsOut[i]); - expect(b.gte(minOut)).to.be.true; - expect(accuracy(b, BigNumber.from(expectedAmountsOut[i]))).to.be.closeTo( - 1, - 1e-2 - ); // inaccuracy should be less than 1% - }); - const expectedMins = expectedAmountsOut.map((a) => - subSlippage(BigNumber.from(a), BigNumber.from(slippage)).toString() - ); - expect(expectedMins).to.deep.eq(minAmountsOut); -}; +const addresses = ADDRESSES[network]; describe('generalised exit execution', async function () { this.timeout(120000); // Sets timeout for all tests within this scope to 2 minutes @@ -201,7 +39,15 @@ describe('generalised exit execution', async function () { const amount = parseFixed('0.02', pool.decimals).toString(); it('should exit pool correctly', async () => { - await testFlow(pool, amount); + await testFlow( + pool, + slippage, + amount, + [], + network, + blockNumber, + poolAddresses + ); }); }); @@ -216,7 +62,15 @@ describe('generalised exit execution', async function () { const amount = parseFixed('0.05', pool.decimals).toString(); it('should exit pool correctly', async () => { - await testFlow(pool, amount); + await testFlow( + pool, + slippage, + amount, + [], + network, + blockNumber, + poolAddresses + ); }); }); @@ -231,7 +85,15 @@ describe('generalised exit execution', async function () { const amount = parseFixed('0.05', pool.decimals).toString(); it('should exit pool correctly', async () => { - await testFlow(pool, amount); + await testFlow( + pool, + slippage, + amount, + [], + network, + blockNumber, + poolAddresses + ); }); }); @@ -247,22 +109,16 @@ describe('generalised exit execution', async function () { const pool = addresses.boostedMetaBig1; const amount = parseFixed('0.05', pool.decimals).toString(); - context('using simulation type VaultModel', async () => { - it('should exit pool correctly', async () => { - await testFlow(pool, amount, SimulationType.VaultModel); - }); - }); - - context('using simulation type Tenderly', async () => { - it('should exit pool correctly', async () => { - await testFlow(pool, amount, SimulationType.Tenderly); - }); - }); - - context('using simulation type Satic', async () => { - it('should exit pool correctly', async () => { - await testFlow(pool, amount, SimulationType.Static); - }); + it('should exit pool correctly', async () => { + await testFlow( + pool, + slippage, + amount, + [], + network, + blockNumber, + poolAddresses + ); }); }); @@ -277,7 +133,15 @@ describe('generalised exit execution', async function () { const amount = parseFixed('0.05', pool.decimals).toString(); it('should exit pool correctly', async () => { - await testFlow(pool, amount); + await testFlow( + pool, + slippage, + amount, + [], + network, + blockNumber, + poolAddresses + ); }); }); @@ -294,7 +158,15 @@ describe('generalised exit execution', async function () { const amount = parseFixed('0.05', pool.decimals).toString(); it('should exit pool correctly', async () => { - await testFlow(pool, amount); + await testFlow( + pool, + slippage, + amount, + [], + network, + blockNumber, + poolAddresses + ); }); }); @@ -310,7 +182,15 @@ describe('generalised exit execution', async function () { const amount = parseFixed('0.05', pool.decimals).toString(); it('should exit pool correctly', async () => { - await testFlow(pool, amount); + await testFlow( + pool, + slippage, + amount, + [], + network, + blockNumber, + poolAddresses + ); }); }); @@ -325,7 +205,15 @@ describe('generalised exit execution', async function () { const amount = parseFixed('0.01', pool.decimals).toString(); it('should exit pool correctly', async () => { - await testFlow(pool, amount); + await testFlow( + pool, + slippage, + amount, + [], + network, + blockNumber, + poolAddresses + ); }); }); @@ -341,22 +229,16 @@ describe('generalised exit execution', async function () { const pool = addresses.boostedWeightedMetaGeneral1; const amount = parseFixed('0.05', pool.decimals).toString(); - context('using simulation type VaultModel', async () => { - it('should exit pool correctly', async () => { - await testFlow(pool, amount, SimulationType.VaultModel); - }); - }); - - context('using simulation type Tenderly', async () => { - it('should exit pool correctly', async () => { - await testFlow(pool, amount, SimulationType.Tenderly); - }); - }); - - context('using simulation type Satic', async () => { - it('should exit pool correctly', async () => { - await testFlow(pool, amount, SimulationType.Static); - }); + it('should exit pool correctly', async () => { + await testFlow( + pool, + slippage, + amount, + [], + network, + blockNumber, + poolAddresses + ); }); }); }); diff --git a/balancer-js/src/modules/exits/exits.module.ts b/balancer-js/src/modules/exits/exits.module.ts index 6f94a24d3..207c56202 100644 --- a/balancer-js/src/modules/exits/exits.module.ts +++ b/balancer-js/src/modules/exits/exits.module.ts @@ -10,7 +10,7 @@ import { AssetHelpers, subSlippage } from '@/lib/utils'; import { PoolGraph, Node } from '@/modules/graph/graph'; import { Join } from '@/modules/joins/joins.module'; import { calcPriceImpact } from '@/modules/pricing/priceImpact'; -import { Relayer } from '@/modules/relayer/relayer.module'; +import { EncodeUnwrapInput, Relayer } from '@/modules/relayer/relayer.module'; import { Simulation, SimulationType, @@ -23,6 +23,7 @@ import { } from '@/modules/swaps/types'; import { ExitPoolRequest as ExitPoolModelRequest } from '@/modules/vaultModel/poolModel/exit'; import { SwapRequest } from '@/modules/vaultModel/poolModel/swap'; +import { UnwrapRequest } from '@/modules/vaultModel/poolModel/unwrap'; import { Requests, VaultModel } from '@/modules/vaultModel/vaultModel.module'; import { ComposableStablePoolEncoder } from '@/pool-composable-stable'; import { StablePoolEncoder } from '@/pool-stable'; @@ -32,6 +33,29 @@ import { BalancerNetworkConfig, ExitPoolRequest, PoolType } from '@/types'; const balancerRelayerInterface = BalancerRelayer__factory.createInterface(); +export interface GeneralisedExitOutput { + to: string; + encodedCall: string; + tokensOut: string[]; + expectedAmountsOut: string[]; + minAmountsOut: string[]; + priceImpact: string; +} + +export interface ExitInfo { + tokensOut: string[]; + estimatedAmountsOut: string[]; + priceImpact: string; + tokensToUnwrap: string[]; +} + +// Quickly switch useful debug logs on/off +const DEBUG = false; + +function debugLog(log: string) { + if (DEBUG) console.log(log); +} + export class Exit { private wrappedNativeAsset: string; private relayer: string; @@ -46,14 +70,53 @@ export class Exit { this.relayer = contracts.relayer; } - async exitPool( + async getExitInfo( + poolId: string, + amountBptIn: string, + userAddress: string, + signer: JsonRpcSigner + ): Promise<{ + tokensOut: string[]; + estimatedAmountsOut: string[]; + priceImpact: string; + tokensToUnwrap: string[]; + }> { + debugLog(`\n--- getExitInfo()`); + /* + Overall exit flow description: + - Create calls with 0 expected min amount for each token out + - static call (or V4 special call) to get actual amounts for each token out + - Apply slippage to amountsOut + - Recreate calls with minAmounts === actualAmountsWithSlippage + - Return minAmoutsOut, UI would use this to display to user + - Return updatedCalls, UI would use this to execute tx + */ + const exit = await this.getExit( + poolId, + amountBptIn, + userAddress, + signer, + [], + SimulationType.VaultModel + ); + + return { + tokensOut: exit.tokensOut, + estimatedAmountsOut: exit.expectedAmountsOut, + priceImpact: exit.priceImpact, + tokensToUnwrap: exit.tokensToUnwrap, + }; + } + + async buildExitCall( poolId: string, amountBptIn: string, userAddress: string, slippage: string, signer: JsonRpcSigner, - simulationType: SimulationType, - authorisation?: string + simulationType: SimulationType.Static | SimulationType.Tenderly, + authorisation?: string, + tokensToUnwrap?: string[] ): Promise<{ to: string; encodedCall: string; @@ -62,6 +125,9 @@ export class Exit { minAmountsOut: string[]; priceImpact: string; }> { + debugLog( + `\n--- exitPool(): simulationType: ${simulationType} - tokensToUnwrap: ${tokensToUnwrap}` + ); /* Overall exit flow description: - Create calls with 0 expected min amount for each token out @@ -72,11 +138,77 @@ export class Exit { - Return updatedCalls, UI would use this to execute tx */ - // Create nodes and order by breadth first - const orderedNodes = await this.poolGraph.getGraphNodes(false, poolId); + const exit = await this.getExit( + poolId, + amountBptIn, + userAddress, + signer, + tokensToUnwrap ?? [], + simulationType, + authorisation + ); + + const { minAmountsOutByExitPath, minAmountsOutByTokenOut } = + this.minAmountsOut( + exit.expectedAmountsOutByExitPath, + exit.expectedAmountsOut, + slippage + ); + + debugLog(`------------ Updating limits...`); + // Create calls with minimum expected amount out for each exit path + const { encodedCall, deltas } = await this.createCalls( + exit.exitPaths, + userAddress, + exit.isProportional, + minAmountsOutByExitPath, + authorisation + ); + + this.assertDeltas( + poolId, + deltas, + amountBptIn, + exit.tokensOut, + minAmountsOutByTokenOut + ); + + return { + to: this.relayer, + encodedCall, + tokensOut: exit.tokensOut, + expectedAmountsOut: exit.expectedAmountsOut, + minAmountsOut: minAmountsOutByTokenOut, + priceImpact: exit.priceImpact, + }; + } + + private async getExit( + poolId: string, + amountBptIn: string, + userAddress: string, + signer: JsonRpcSigner, + tokensToUnwrap: string[], + simulationType: SimulationType, + authorisation?: string + ): Promise<{ + tokensToUnwrap: string[]; + tokensOut: string[]; + exitPaths: Node[][]; + isProportional: boolean; + expectedAmountsOut: string[]; + expectedAmountsOutByExitPath: string[]; + priceImpact: string; + }> { + // Create nodes and order by breadth first - initially trys with no unwrapping + const orderedNodes = await this.poolGraph.getGraphNodes( + false, + poolId, + tokensToUnwrap + ); const isProportional = PoolGraph.isProportionalPools(orderedNodes); - console.log(`isProportional`, isProportional); + debugLog(`\nisProportional = ${isProportional}`); let exitPaths: Node[][] = []; let tokensOutByExitPath: string[] = []; @@ -123,52 +255,58 @@ export class Exit { simulationType ); - const expectedAmountsOutByTokenOut = this.amountsOutByTokenOut( - tokensOut, - tokensOutByExitPath, - expectedAmountsOutByExitPath - ); - - const { minAmountsOutByExitPath, minAmountsOutByTokenOut } = - this.minAmountsOut( - expectedAmountsOutByExitPath, - expectedAmountsOutByTokenOut, - slippage + const tokensWithInsufficientBalance = outputNodes + .filter((outputNode, i) => + BigNumber.from(expectedAmountsOutByExitPath[i]).gt(outputNode.balance) + ) + .map((node) => node.address.toLowerCase()); + + if ( + tokensToUnwrap.some((t) => + tokensWithInsufficientBalance.includes(t.toLowerCase()) + ) + ) { + /** + * This means there is not enough balance to exit to main or wrapped tokens only + */ + throw new Error( + 'Insufficient pool balance to perform generalised exit - try exitting with smaller amounts' + ); + } else if (tokensWithInsufficientBalance.length > 0) { + return await this.getExit( + poolId, + amountBptIn, + userAddress, + signer, + [...new Set(tokensWithInsufficientBalance)].sort(), + simulationType, + authorisation + ); + } else { + const expectedAmountsOut = this.amountsOutByTokenOut( + tokensOut, + tokensOutByExitPath, + expectedAmountsOutByExitPath ); - // Create calls with minimum expected amount out for each exit path - const { encodedCall, deltas } = await this.createCalls( - exitPaths, - userAddress, - isProportional, - minAmountsOutByExitPath, - authorisation - ); - - this.assertDeltas( - poolId, - deltas, - amountBptIn, - tokensOut, - minAmountsOutByTokenOut - ); - - const priceImpact = await this.calculatePriceImpact( - poolId, - this.poolGraph, - tokensOut, - expectedAmountsOutByTokenOut, - amountBptIn - ); + const priceImpact = await this.calculatePriceImpact( + poolId, + this.poolGraph, + tokensOut, + expectedAmountsOut, + amountBptIn + ); - return { - to: this.relayer, - encodedCall, - tokensOut, - expectedAmountsOut: expectedAmountsOutByTokenOut, - minAmountsOut: minAmountsOutByTokenOut, - priceImpact, - }; + return { + tokensToUnwrap, + tokensOut, + exitPaths, + isProportional, + expectedAmountsOut, + expectedAmountsOutByExitPath, + priceImpact, + }; + } } /* @@ -185,7 +323,7 @@ export class Exit { amountBptIn: string ): Promise { // Create nodes for each pool/token interaction and order by breadth first - const orderedNodesForJoin = await poolGraph.getGraphNodes(true, poolId); + const orderedNodesForJoin = await poolGraph.getGraphNodes(true, poolId, []); const joinPaths = Join.getJoinPaths( orderedNodesForJoin, tokensOut, @@ -464,15 +602,18 @@ export class Exit { const exitChildren = node.children.filter((child) => exitPath.map((n) => n.index).includes(child.index) ); - const hasOutputChild = exitChildren.some( - (c) => c.exitAction === 'output' + // An action that has either outputs or unwraps as child actions is the last action where we're able to set limits on expected output amounts + const isLastActionWithLimits = exitChildren.some( + (c) => c.exitAction === 'output' || c.exitAction === 'unwrap' ); // Last calls will use minAmountsOut to protect user. Middle calls can safely have 0 minimum as tx will revert if last fails. let minAmountOut = '0'; const minAmountsOutProportional = Array(node.children.length).fill('0'); - if (minAmountsOut && hasOutputChild) { + if (minAmountsOut && isLastActionWithLimits) { if (isProportional) { + // Proportional exits have a minAmountOut for each output node within a single exit path + /** * minAmountsOut is related to the whole multicall transaction, while * minAmountsOutProportional is related only to the current node/transaction @@ -481,22 +622,48 @@ export class Exit { * TODO: extract to a function so it's easier to understand */ node.children.forEach((child, i) => { - if (child.exitAction === 'output') { - minAmountsOutProportional[i] = - minAmountsOut[outputNodes.indexOf(child)]; + let outputChildIndex: number; + if (child.exitAction === 'unwrap') { + outputChildIndex = outputNodes.indexOf(child.children[0]); + minAmountOut = WeiPerEther.mul(minAmountsOut[outputChildIndex]) + .div(child.priceRate) + .toString(); + } else if (child.exitAction === 'output') { + outputChildIndex = outputNodes.indexOf(child); + minAmountOut = minAmountsOut[outputChildIndex]; + } else { + minAmountOut = '0'; // clears minAmountOut if it's not an output or unwrap } + minAmountsOutProportional[i] = minAmountOut; }); - - // Proportional exits have a minAmountOut for each output node within a single exit path - minAmountOut = - minAmountsOut[outputNodes.indexOf(exitChild as Node)]; } else { // Non-proportional exits have a minAmountOut for each exit path - minAmountOut = minAmountsOut[i]; + if (exitChild?.exitAction === 'unwrap') { + minAmountOut = WeiPerEther.mul(minAmountsOut[i]) + .div(exitChild.priceRate) + .toString(); + } else { + minAmountOut = minAmountsOut[i]; + } } } switch (node.exitAction) { + case 'unwrap': { + const { modelRequest, encodedCall, assets, amounts } = + this.createUnwrap( + node, + exitChild as Node, + i, + minAmountOut, + sender, + recipient + ); + modelRequests.push(modelRequest); + calls.push(encodedCall); + this.updateDeltas(deltas, assets, amounts); + break; + } case 'batchSwap': { const { modelRequest, encodedCall, assets, amounts } = this.createSwap( @@ -565,6 +732,53 @@ export class Exit { return { multiRequests, calls, outputIndexes, deltas }; } + private createUnwrap = ( + node: Node, + exitChild: Node, + exitPathIndex: number, + minAmountOut: string, + sender: string, + recipient: string + ): { + modelRequest: UnwrapRequest; + encodedCall: string; + assets: string[]; + amounts: string[]; + } => { + const amount = Relayer.toChainedReference( + this.getOutputRef(exitPathIndex, node.index) + ).toString(); + const outputReference = Relayer.toChainedReference( + this.getOutputRef(exitPathIndex, exitChild.index) + ); + + const linearPoolType = node.parent?.type as string; + + const call: EncodeUnwrapInput = { + wrappedToken: node.address, + sender, + recipient, + amount, + outputReference, + }; + + const encodedCall = Relayer.encodeUnwrap(call, linearPoolType); + + debugLog(`linear type: , ${linearPoolType}`); + debugLog('\nUwrap:'); + debugLog(JSON.stringify(call)); + + const modelRequest = VaultModel.mapUnwrapRequest( + amount, + outputReference, + node.parent?.id as string // linear pool id + ); + + const assets = [exitChild.address]; + const amounts = [Zero.sub(minAmountOut).toString()]; // needs to be negative because it's handled by the vault model as an amount going out of the vault + return { modelRequest, encodedCall, assets, amounts }; + }; + private createSwap( node: Node, exitChild: Node, @@ -601,33 +815,20 @@ export class Exit { userData: '0x', }; + const fromInternalBalance = this.receivesFromInternal(node); + const toInternalBalance = this.receivesFromInternal(exitChild); + const funds: FundManagement = { sender, recipient, - fromInternalBalance: this.receivesFromInternal(node), - toInternalBalance: this.receivesFromInternal(exitChild), + fromInternalBalance, + toInternalBalance, }; const outputReference = Relayer.toChainedReference( this.getOutputRef(exitPathIndex, exitChild.index) ); - // console.log( - // `${node.type} ${node.address} prop: ${formatFixed( - // node.proportionOfParent, - // 18 - // )} - // ${node.exitAction}( - // inputAmt: ${amountIn}, - // inputToken: ${node.address}, - // pool: ${node.id}, - // outputToken: ${exitChild.address}, - // outputRef: ${this.getOutputRef(exitPathIndex, exitChild.index)}, - // sender: ${sender}, - // recipient: ${recipient} - // )` - // ); - const call: Swap = { request, funds, @@ -636,6 +837,8 @@ export class Exit { value: '0', // TODO: check if swap with ETH is possible in this case and handle it outputReference, }; + debugLog('\nSwap:'); + debugLog(JSON.stringify(call)); const encodedCall = Relayer.encodeSwap(call); @@ -736,23 +939,7 @@ export class Exit { }, ]; - // console.log( - // `${node.type} ${node.address} prop: ${formatFixed( - // node.proportionOfParent, - // 18 - // )} - // ${node.exitAction}( - // poolId: ${node.id}, - // tokensOut: ${sortedTokens}, - // tokenOut: ${sortedTokens[sortedTokens.indexOf(tokenOut)].toString()}, - // amountOut: ${sortedAmounts[sortedTokens.indexOf(tokenOut)].toString()}, - // amountIn: ${amountIn}, - // minAmountOut: ${minAmountOut}, - // outputRef: ${this.getOutputRef(exitPathIndex, exitChild.index)}, - // sender: ${sender}, - // recipient: ${recipient} - // )` - // ); + const toInternalBalance = this.receivesFromInternal(exitChild); const call = Relayer.formatExitPoolInput({ poolId: node.id, @@ -764,8 +951,11 @@ export class Exit { assets: sortedTokens, minAmountsOut: sortedAmounts, userData, - toInternalBalance: this.receivesFromInternal(exitChild), + toInternalBalance, }); + debugLog('\nExit:'); + debugLog(JSON.stringify(call)); + const encodedCall = Relayer.encodeExitPool(call); const modelRequest = VaultModel.mapExitPoolRequest(call); @@ -847,25 +1037,6 @@ export class Exit { key: Relayer.toChainedReference(this.getOutputRef(0, child.index)), }; }); - - // console.log( - // `${node.type} ${node.address} prop: ${formatFixed( - // node.proportionOfParent, - // 18 - // )} - // ${node.exitAction}( - // poolId: ${node.id}, - // tokensOut: ${sortedTokens}, - // tokenOut: ${sortedTokens[sortedTokens.indexOf(tokenOut)].toString()}, - // amountOut: ${sortedAmounts[sortedTokens.indexOf(tokenOut)].toString()}, - // amountIn: ${amountIn}, - // minAmountOut: ${minAmountOut}, - // outputRef: ${this.getOutputRef(exitPathIndex, exitChild.index)}, - // sender: ${sender}, - // recipient: ${recipient} - // )` - // ); - // We have to use correct pool type based off following from Relayer: // enum PoolKind { WEIGHTED, LEGACY_STABLE, COMPOSABLE_STABLE, COMPOSABLE_STABLE_V2 } // (note only Weighted and COMPOSABLE_STABLE_V2 will support proportional exits) @@ -874,6 +1045,10 @@ export class Exit { kind = 3; } + const allChildrenReceiveFromInternal = node.children.every((child) => + this.receivesFromInternal(child) + ); + const call = Relayer.formatExitPoolInput({ poolId: node.id, poolKind: kind, @@ -884,8 +1059,10 @@ export class Exit { assets: sortedTokens, minAmountsOut: sortedAmounts, userData, - toInternalBalance: false, + toInternalBalance: allChildrenReceiveFromInternal, }); + debugLog('\nExitProportional:'); + debugLog(JSON.stringify(call)); const encodedCall = Relayer.encodeExitPool(call); const modelRequest = VaultModel.mapExitPoolRequest(call); @@ -925,6 +1102,10 @@ export class Exit { // others should always receive from internal balance private receivesFromInternal = (node: Node): boolean => { if (!node.parent) return false; - return node.exitAction !== 'output' && node.exitAction !== 'exitPool'; + return ( + node.exitAction !== 'output' && + node.exitAction !== 'unwrap' && + node.exitAction !== 'exitPool' + ); }; } diff --git a/balancer-js/src/modules/exits/exitsProportional.module.integration.spec.ts b/balancer-js/src/modules/exits/exitsProportional.module.integration.spec.ts index 5c417fb52..408d83bd7 100644 --- a/balancer-js/src/modules/exits/exitsProportional.module.integration.spec.ts +++ b/balancer-js/src/modules/exits/exitsProportional.module.integration.spec.ts @@ -1,264 +1,83 @@ // yarn test:only ./src/modules/exits/exitsProportional.module.integration.spec.ts import dotenv from 'dotenv'; -import { expect } from 'chai'; - -import { BigNumber, parseFixed } from '@ethersproject/bignumber'; -import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; - -import { BalancerSDK, GraphQLQuery, GraphQLArgs, Network } from '@/.'; -import { Relayer } from '@/modules/relayer/relayer.module'; -import { accuracy, forkSetup, getBalances } from '@/test/lib/utils'; +import { parseFixed } from '@ethersproject/bignumber'; +import { Network } from '@/.'; import { ADDRESSES } from '@/test/lib/constants'; -import { SimulationType } from '../simulation/simulation.module'; +import { testFlow, Pool } from './testHelper'; dotenv.config(); const network = Network.MAINNET; const blockNumber = 17116836; -const { ALCHEMY_URL: jsonRpcUrl } = process.env; -const rpcUrl = 'http://127.0.0.1:8545'; - +const slippage = '10'; // 10 bps = 0.1% const addresses = ADDRESSES[network]; - -// Set tenderly config blockNumber and use default values for other parameters -const tenderlyConfig = { - blockNumber, -}; - -/** - * Example of subgraph query that allows filtering pools. - * Might be useful to reduce the response time by limiting the amount of pool - * data that will be queried by the SDK. Specially when on chain data is being - * fetched as well. - */ const poolAddresses = Object.values(addresses).map( (address) => address.address ); -const subgraphArgs: GraphQLArgs = { - where: { - swapEnabled: { - eq: true, - }, - totalShares: { - gt: 0.000000000001, - }, - address: { - in: poolAddresses, - }, - }, - orderBy: 'totalLiquidity', - orderDirection: 'desc', - block: { number: blockNumber }, -}; -const subgraphQuery: GraphQLQuery = { args: subgraphArgs, attrs: {} }; - -const sdk = new BalancerSDK({ - network, - rpcUrl, - tenderly: tenderlyConfig, - subgraphQuery, -}); -const { pools, balancerContracts } = sdk; -const provider = new JsonRpcProvider(rpcUrl, network); -const signer = provider.getSigner(1); -const { contracts, contractAddresses } = balancerContracts; -const relayerAddress = contractAddresses.relayer as string; interface Test { - signer: JsonRpcSigner; description: string; - pool: { - id: string; - address: string; - }; + pool: Pool; amount: string; - authorisation: string | undefined; - simulationType?: SimulationType; } const runTests = async (tests: Test[]) => { for (let i = 0; i < tests.length; i++) { const test = tests[i]; it(test.description, async () => { - const signerAddress = await test.signer.getAddress(); - const authorisation = await Relayer.signRelayerApproval( - relayerAddress, - signerAddress, - test.signer, - contracts.vault - ); await testFlow( - test.signer, - signerAddress, test.pool, + slippage, test.amount, - authorisation, - test.simulationType + [], + network, + blockNumber, + poolAddresses ); }).timeout(120000); } }; -const testFlow = async ( - signer: JsonRpcSigner, - signerAddress: string, - pool: { id: string; address: string }, - amount: string, - authorisation: string | undefined, - simulationType = SimulationType.VaultModel -) => { - const gasLimit = 8e6; - const slippage = '10'; // 10 bps = 0.1% - - const { to, encodedCall, tokensOut, expectedAmountsOut, minAmountsOut } = - await pools.generalisedExit( - pool.id, - amount, - signerAddress, - slippage, - signer, - simulationType, - authorisation - ); - - const [bptBalanceBefore, ...tokensOutBalanceBefore] = await getBalances( - [pool.address, ...tokensOut], - signer, - signerAddress - ); - - const response = await signer.sendTransaction({ - to, - data: encodedCall, - gasLimit, - }); - - const receipt = await response.wait(); - console.log('Gas used', receipt.gasUsed.toString()); - - const [bptBalanceAfter, ...tokensOutBalanceAfter] = await getBalances( - [pool.address, ...tokensOut], - signer, - signerAddress - ); - - console.table({ - tokensOut: tokensOut.map((t) => `${t.slice(0, 6)}...${t.slice(38, 42)}`), - minOut: minAmountsOut, - expectedOut: expectedAmountsOut, - balanceAfter: tokensOutBalanceAfter.map((b) => b.toString()), - }); - - expect(receipt.status).to.eql(1); - minAmountsOut.forEach((minAmountOut) => { - expect(BigNumber.from(minAmountOut).gte('0')).to.be.true; - }); - expectedAmountsOut.forEach((expectedAmountOut, i) => { - expect( - BigNumber.from(expectedAmountOut).gte(BigNumber.from(minAmountsOut[i])) - ).to.be.true; - }); - expect(bptBalanceAfter.eq(bptBalanceBefore.sub(amount))).to.be.true; - tokensOutBalanceBefore.forEach((b) => expect(b.eq(0)).to.be.true); - tokensOutBalanceAfter.forEach((balanceAfter, i) => { - const minOut = BigNumber.from(minAmountsOut[i]); - expect(balanceAfter.gte(minOut)).to.be.true; - const expectedOut = BigNumber.from(expectedAmountsOut[i]); - expect(accuracy(balanceAfter, expectedOut)).to.be.closeTo(1, 1e-2); // inaccuracy should not be over to 1% - }); -}; - describe('generalised exit execution', async () => { context('composable stable pool - non-boosted', async () => { - let authorisation: string | undefined; const testPool = addresses.wstETH_rETH_sfrxETH; - - beforeEach(async () => { - const tokens = [testPool.address]; - const slots = [testPool.slot]; - const balances = [parseFixed('0.02', testPool.decimals).toString()]; - await forkSetup( - signer, - tokens, - slots, - balances, - jsonRpcUrl as string, - blockNumber - ); - }); - await runTests([ { - signer, description: 'exit pool', pool: { id: testPool.id, address: testPool.address, + slot: testPool.slot, }, amount: parseFixed('0.01', testPool.decimals).toString(), - authorisation, }, ]); }); context('composable stable pool - boosted', async () => { - let authorisation: string | undefined; const testPool = addresses.bbgusd; - - beforeEach(async () => { - const tokens = [testPool.address]; - const slots = [testPool.slot]; - const balances = [parseFixed('0.02', testPool.decimals).toString()]; - await forkSetup( - signer, - tokens, - slots, - balances, - jsonRpcUrl as string, - blockNumber - ); - }); - await runTests([ { - signer, description: 'exit pool', pool: { id: testPool.id, address: testPool.address, + slot: testPool.slot, }, amount: parseFixed('0.01', testPool.decimals).toString(), - authorisation, }, ]); }); context('weighted with boosted', async () => { - let authorisation: string | undefined; const testPool = addresses.STG_BBAUSD; - - beforeEach(async () => { - const tokens = [testPool.address]; - const slots = [testPool.slot]; - const balances = [parseFixed('25.111', testPool.decimals).toString()]; - await forkSetup( - signer, - tokens, - slots, - balances, - jsonRpcUrl as string, - blockNumber - ); - }); - await runTests([ { - signer, description: 'exit pool', pool: { id: testPool.id, address: testPool.address, + slot: testPool.slot, }, amount: parseFixed('25.111', testPool.decimals).toString(), - authorisation, }, ]); }); diff --git a/balancer-js/src/modules/exits/testHelper.ts b/balancer-js/src/modules/exits/testHelper.ts new file mode 100644 index 000000000..bbc3fc63c --- /dev/null +++ b/balancer-js/src/modules/exits/testHelper.ts @@ -0,0 +1,203 @@ +import { expect } from 'chai'; +import { + BalancerSDK, + GraphQLQuery, + GraphQLArgs, + Network, + truncateAddresses, + subSlippage, + removeItem, +} from '@/.'; +import { BigNumber } from '@ethersproject/bignumber'; +import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; +import { + TxResult, + accuracy, + forkSetup, + sendTransactionGetBalances, + FORK_NODES, + RPC_URLS, +} from '@/test/lib/utils'; +import { Relayer } from '@/modules/relayer/relayer.module'; +import { SimulationType } from '../simulation/simulation.module'; +import { GeneralisedExitOutput, ExitInfo } from '../exits/exits.module'; + +export interface Pool { + id: string; + address: string; + slot: number; +} + +export const testFlow = async ( + pool: Pool, + slippage: string, + exitAmount: string, + expectToUnwrap: string[], + network: Network, + blockNumber: number, + poolAddressesToConsider: string[] +): Promise<{ + expectedAmountsOut: string[]; + gasUsed: BigNumber; +}> => { + const { sdk, signer } = await setUpForkAndSdk( + network, + blockNumber, + poolAddressesToConsider, + [pool.address], + [pool.slot], + [exitAmount] + ); + // Follows similar flow to a front end implementation + const { exitOutput, txResult, exitInfo } = await userFlow( + pool, + sdk, + signer, + exitAmount, + slippage + ); + const tokensOutDeltas = removeItem(txResult.balanceDeltas, 0); + console.table({ + tokensOut: truncateAddresses(exitOutput.tokensOut), + estimateAmountsOut: exitInfo.estimatedAmountsOut, + minAmountsOut: exitOutput.minAmountsOut, + expectedAmountsOut: exitOutput.expectedAmountsOut, + balanceDeltas: tokensOutDeltas.map((b) => b.toString()), + }); + console.log('Gas used', txResult.gasUsed.toString()); + console.log(`Tokens to unwrap: `, exitInfo.tokensToUnwrap); + + expect(txResult.transactionReceipt.status).to.eq(1); + expect(txResult.balanceDeltas[0].toString()).to.eq(exitAmount.toString()); + expect(exitInfo.tokensToUnwrap).to.deep.eq(expectToUnwrap); + tokensOutDeltas.forEach((b, i) => { + const minOut = BigNumber.from(exitOutput.minAmountsOut[i]); + expect(b.gte(minOut)).to.be.true; + expect( + accuracy(b, BigNumber.from(exitOutput.expectedAmountsOut[i])) + ).to.be.closeTo(1, 1e-2); // inaccuracy should be less than 1% + }); + const expectedMins = exitOutput.expectedAmountsOut.map((a) => + subSlippage(BigNumber.from(a), BigNumber.from(slippage)).toString() + ); + expect(expectedMins).to.deep.eq(exitOutput.minAmountsOut); + return { + expectedAmountsOut: exitOutput.expectedAmountsOut, + gasUsed: txResult.gasUsed, + }; +}; + +async function userFlow( + pool: Pool, + sdk: BalancerSDK, + signer: JsonRpcSigner, + exitAmount: string, + slippage: string +): Promise<{ + exitOutput: GeneralisedExitOutput; + exitInfo: ExitInfo; + txResult: TxResult; +}> { + const signerAddress = await signer.getAddress(); + // Replicating UI user flow: + // 1. Gets exitInfo + // - this helps user to decide if they will approve relayer, etc by returning estimated amounts out/pi. + // - also returns tokensOut and whether or not unwrap should be used + const exitInfo = await sdk.pools.getExitInfo( + pool.id, + exitAmount, + signerAddress, + signer + ); + const authorisation = await Relayer.signRelayerApproval( + sdk.contracts.relayer.address, + signerAddress, + signer, + sdk.contracts.vault + ); + // 2. Get call data and expected/min amounts out + // - Uses a Static/Tenderly call to simulate tx then applies slippage + const output = await sdk.pools.generalisedExit( + pool.id, + exitAmount, + signerAddress, + slippage, + signer, + SimulationType.Static, + authorisation, + exitInfo.tokensToUnwrap + ); + // 3. Sends tx + const txResult = await sendTransactionGetBalances( + [pool.address, ...output.tokensOut], + signer, + signerAddress, + output.to, + output.encodedCall + ); + return { + exitOutput: output, + txResult, + exitInfo, + }; +} + +async function setUpForkAndSdk( + network: Network, + blockNumber: number, + pools: string[], + tokens: string[], + slots: number[], + balances: string[] +): Promise<{ + sdk: BalancerSDK; + signer: JsonRpcSigner; +}> { + // Set tenderly config blockNumber and use default values for other parameters + const tenderlyConfig = { + blockNumber, + }; + + // Only queries minimal set of addresses + const subgraphQuery = createSubgraphQuery(pools, blockNumber); + + const sdk = new BalancerSDK({ + network, + rpcUrl: RPC_URLS[network], + tenderly: tenderlyConfig, + subgraphQuery, + }); + const provider = new JsonRpcProvider(RPC_URLS[network], network); + const signer = provider.getSigner(); + + await forkSetup( + signer, + tokens, + slots, + balances, + FORK_NODES[network], + blockNumber + ); + return { sdk, signer }; +} + +function createSubgraphQuery(pools: string[], blockNo: number): GraphQLQuery { + const subgraphArgs: GraphQLArgs = { + where: { + swapEnabled: { + eq: true, + }, + totalShares: { + gt: 0.000000000001, + }, + address: { + in: pools, + }, + }, + orderBy: 'totalLiquidity', + orderDirection: 'desc', + block: { number: blockNo }, + }; + const subgraphQuery: GraphQLQuery = { args: subgraphArgs, attrs: {} }; + return subgraphQuery; +} diff --git a/balancer-js/src/modules/graph/graph.module.spec.ts b/balancer-js/src/modules/graph/graph.module.spec.ts index 4745c9d26..c8cdf0654 100644 --- a/balancer-js/src/modules/graph/graph.module.spec.ts +++ b/balancer-js/src/modules/graph/graph.module.spec.ts @@ -16,6 +16,7 @@ import { LinearInfo, } from '@/test/factories/pools'; import { Pool as SdkPool } from '@/types'; +import { formatAddress } from '@/test/lib/utils'; function checkNode( node: Node, @@ -52,7 +53,7 @@ function checkLinearNode( wrappedTokens: SubgraphToken[], mainTokens: SubgraphToken[], expectedOutPutReference: number, - wrapMainTokens: boolean + tokensToUnwrap: string[] ): void { checkNode( linearNode, @@ -65,14 +66,18 @@ function checkLinearNode( expectedOutPutReference.toString(), linearPools[poolIndex].proportionOfParent ); - if (wrapMainTokens) { + const mainToken = + linearPools[poolIndex].tokensList[ + linearPools[poolIndex].mainIndex as number + ]; + if (tokensToUnwrap.includes(mainToken)) { checkNode( linearNode.children[0], 'N/A', wrappedTokens[poolIndex].address, 'WrappedToken', - 'wrapAaveDynamicToken', - 'unwrapAaveStaticToken', + 'wrap', + 'unwrap', 1, (expectedOutPutReference + 1).toString(), linearPools[poolIndex].proportionOfParent @@ -112,7 +117,7 @@ function checkBoosted( boostedPoolInfo: BoostedInfo, boostedIndex: number, expectedProportionOfParent: string, - wrapMainTokens: boolean + tokensToUnwrap: string[] ): void { checkNode( boostedNode, @@ -127,7 +132,7 @@ function checkBoosted( ); boostedNode.children.forEach((linearNode, i) => { let linearInputRef; - if (wrapMainTokens) linearInputRef = boostedIndex + 1 + i * 3; + if (tokensToUnwrap.length > 0) linearInputRef = boostedIndex + 1 + i * 3; else linearInputRef = boostedIndex + 1 + i * 2; checkLinearNode( linearNode, @@ -136,7 +141,7 @@ function checkBoosted( boostedPoolInfo.wrappedTokens, boostedPoolInfo.mainTokens, linearInputRef, - wrapMainTokens + tokensToUnwrap ); }); } @@ -147,7 +152,7 @@ Checks a boostedMeta, a phantomStable with one Linear and one boosted. function checkBoostedMeta( rootNode: Node, boostedMetaInfo: BoostedMetaInfo, - wrapMainTokens: boolean + tokensToUnwrap: string[] ): void { // Check parent node checkNode( @@ -168,10 +173,10 @@ function checkBoostedMeta( boostedMetaInfo.childBoostedInfo, 1, boostedMetaInfo.childBoostedInfo.proportion, - wrapMainTokens + tokensToUnwrap ); let expectedOutputReference = 11; - if (!wrapMainTokens) expectedOutputReference = 8; + if (tokensToUnwrap.length === 0) expectedOutputReference = 8; // Check child Linear node checkLinearNode( rootNode.children[1], @@ -180,7 +185,7 @@ function checkBoostedMeta( boostedMetaInfo.childLinearInfo.wrappedTokens, boostedMetaInfo.childLinearInfo.mainTokens, expectedOutputReference, - wrapMainTokens + tokensToUnwrap ); } @@ -190,7 +195,7 @@ Checks a boostedBig, a phantomStable with two Boosted. function checkBoostedMetaBig( rootNode: Node, boostedMetaBigInfo: BoostedMetaBigInfo, - wrapMainTokens: boolean + tokensToUnwrap: string[] ): void { // Check parent node checkNode( @@ -212,9 +217,9 @@ function checkBoostedMetaBig( boostedMetaBigInfo.childPoolsInfo[i], numberOfNodes, boostedMetaBigInfo.childPoolsInfo[i].proportion, - wrapMainTokens + tokensToUnwrap ); - if (wrapMainTokens) + if (tokensToUnwrap.length > 0) numberOfNodes = boostedMetaBigInfo.childPoolsInfo[i].linearPools.length * 3 + 2; else @@ -247,27 +252,32 @@ describe('Graph', () => { linearInfo.linearPools as unknown as SdkPool[] ); poolsGraph = new PoolGraph(poolProvider); - rootNode = await poolsGraph.buildGraphFromRootPool( - linearInfo.linearPools[0].id - ); - }); - it('should build single linearPool graph', async () => { - checkLinearNode( - rootNode, - 0, - linearInfo.linearPools, - linearInfo.wrappedTokens, - linearInfo.mainTokens, - 0, - false - ); }); + context('using non-wrapped tokens', () => { + before(async () => { + rootNode = await poolsGraph.buildGraphFromRootPool( + linearInfo.linearPools[0].id, + [] + ); + }); + it('should build single linearPool graph', async () => { + checkLinearNode( + rootNode, + 0, + linearInfo.linearPools, + linearInfo.wrappedTokens, + linearInfo.mainTokens, + 0, + [] + ); + }); - it('should sort in breadth first order', async () => { - const orderedNodes = PoolGraph.orderByBfs(rootNode).reverse(); - expect(orderedNodes.length).to.eq(2); - expect(orderedNodes[0].type).to.eq('Input'); - expect(orderedNodes[1].type).to.eq('AaveLinear'); + it('should sort in breadth first order', async () => { + const orderedNodes = PoolGraph.orderByBfs(rootNode).reverse(); + expect(orderedNodes.length).to.eq(2); + expect(orderedNodes[0].type).to.eq('Input'); + expect(orderedNodes[1].type).to.eq('AaveLinear'); + }); }); }); @@ -318,40 +328,89 @@ describe('Graph', () => { pools as unknown as SdkPool[] ); poolsGraph = new PoolGraph(poolProvider); - boostedNode = await poolsGraph.buildGraphFromRootPool(boostedPool.id); }); it('should throw when pool doesnt exist', async () => { let errorMessage = ''; try { - await poolsGraph.buildGraphFromRootPool('thisisntapool'); + await poolsGraph.buildGraphFromRootPool('thisisntapool', []); } catch (error) { errorMessage = (error as Error).message; } expect(errorMessage).to.eq('balancer pool does not exist'); }); - it('should build boostedPool graph', async () => { - checkBoosted( - boostedNode, - boostedPoolInfo.rootPool, - boostedPoolInfo, - 0, - '1', - false - ); + context('using wrapped tokens', () => { + let tokensToUnwrap: string[]; + before(async () => { + tokensToUnwrap = [ + '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC + '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT + ]; + boostedNode = await poolsGraph.buildGraphFromRootPool( + boostedPool.id, + tokensToUnwrap + ); + }); + + it('should build boostedPool graph', async () => { + checkBoosted( + boostedNode, + boostedPoolInfo.rootPool, + boostedPoolInfo, + 0, + '1', + tokensToUnwrap + ); + }); + + it('should sort in breadth first order', async () => { + const orderedNodes = PoolGraph.orderByBfs(boostedNode).reverse(); + expect(orderedNodes.length).to.eq(10); + expect(orderedNodes[0].type).to.eq('Input'); + expect(orderedNodes[1].type).to.eq('Input'); + expect(orderedNodes[2].type).to.eq('Input'); + expect(orderedNodes[3].type).to.eq('WrappedToken'); + expect(orderedNodes[4].type).to.eq('WrappedToken'); + expect(orderedNodes[5].type).to.eq('WrappedToken'); + expect(orderedNodes[6].type).to.eq('AaveLinear'); + expect(orderedNodes[7].type).to.eq('AaveLinear'); + expect(orderedNodes[8].type).to.eq('AaveLinear'); + expect(orderedNodes[9].type).to.eq('ComposableStable'); + }); }); - it('should sort in breadth first order', async () => { - const orderedNodes = PoolGraph.orderByBfs(boostedNode).reverse(); - expect(orderedNodes.length).to.eq(7); - expect(orderedNodes[0].type).to.eq('Input'); - expect(orderedNodes[1].type).to.eq('Input'); - expect(orderedNodes[2].type).to.eq('Input'); - expect(orderedNodes[3].type).to.eq('AaveLinear'); - expect(orderedNodes[4].type).to.eq('AaveLinear'); - expect(orderedNodes[5].type).to.eq('AaveLinear'); - expect(orderedNodes[6].type).to.eq('ComposableStable'); + context('using non-wrapped tokens', () => { + before(async () => { + boostedNode = await poolsGraph.buildGraphFromRootPool( + boostedPool.id, + [] + ); + }); + + it('should build boostedPool graph', async () => { + checkBoosted( + boostedNode, + boostedPoolInfo.rootPool, + boostedPoolInfo, + 0, + '1', + [] + ); + }); + + it('should sort in breadth first order', async () => { + const orderedNodes = PoolGraph.orderByBfs(boostedNode).reverse(); + expect(orderedNodes.length).to.eq(7); + expect(orderedNodes[0].type).to.eq('Input'); + expect(orderedNodes[1].type).to.eq('Input'); + expect(orderedNodes[2].type).to.eq('Input'); + expect(orderedNodes[3].type).to.eq('AaveLinear'); + expect(orderedNodes[4].type).to.eq('AaveLinear'); + expect(orderedNodes[5].type).to.eq('AaveLinear'); + expect(orderedNodes[6].type).to.eq('ComposableStable'); + }); }); }); @@ -429,26 +488,70 @@ describe('Graph', () => { pools as unknown as SdkPool[] ); poolsGraph = new PoolGraph(poolProvider); - boostedNode = await poolsGraph.buildGraphFromRootPool(rootPool.id); }); - it('should build boostedPool graph', async () => { - checkBoostedMeta(boostedNode, boostedMetaInfo, false); + context('using wrapped tokens', () => { + let tokensToUnwrap: string[]; + before(async () => { + tokensToUnwrap = [ + '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC + '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT + formatAddress(`address_STABLE`), + ]; + boostedNode = await poolsGraph.buildGraphFromRootPool( + rootPool.id, + tokensToUnwrap + ); + }); + + it('should build boostedPool graph', async () => { + checkBoostedMeta(boostedNode, boostedMetaInfo, tokensToUnwrap); + }); + + it('should sort in breadth first order', async () => { + const orderedNodes = PoolGraph.orderByBfs(boostedNode).reverse(); + expect(orderedNodes.length).to.eq(14); + expect(orderedNodes[0].type).to.eq('Input'); + expect(orderedNodes[1].type).to.eq('Input'); + expect(orderedNodes[2].type).to.eq('Input'); + expect(orderedNodes[3].type).to.eq('Input'); + expect(orderedNodes[4].type).to.eq('WrappedToken'); + expect(orderedNodes[5].type).to.eq('WrappedToken'); + expect(orderedNodes[6].type).to.eq('WrappedToken'); + expect(orderedNodes[7].type).to.eq('WrappedToken'); + expect(orderedNodes[8].type).to.eq('AaveLinear'); + expect(orderedNodes[9].type).to.eq('AaveLinear'); + expect(orderedNodes[10].type).to.eq('AaveLinear'); + expect(orderedNodes[11].type).to.eq('AaveLinear'); + expect(orderedNodes[12].type).to.eq('ComposableStable'); + expect(orderedNodes[13].type).to.eq('ComposableStable'); + }); }); - it('should sort in breadth first order', async () => { - const orderedNodes = PoolGraph.orderByBfs(boostedNode).reverse(); - expect(orderedNodes.length).to.eq(10); - expect(orderedNodes[0].type).to.eq('Input'); - expect(orderedNodes[1].type).to.eq('Input'); - expect(orderedNodes[2].type).to.eq('Input'); - expect(orderedNodes[3].type).to.eq('Input'); - expect(orderedNodes[4].type).to.eq('AaveLinear'); - expect(orderedNodes[5].type).to.eq('AaveLinear'); - expect(orderedNodes[6].type).to.eq('AaveLinear'); - expect(orderedNodes[7].type).to.eq('AaveLinear'); - expect(orderedNodes[8].type).to.eq('ComposableStable'); - expect(orderedNodes[9].type).to.eq('ComposableStable'); + context('using non-wrapped tokens', () => { + before(async () => { + boostedNode = await poolsGraph.buildGraphFromRootPool(rootPool.id, []); + }); + + it('should build boostedPool graph', async () => { + checkBoostedMeta(boostedNode, boostedMetaInfo, []); + }); + + it('should sort in breadth first order', async () => { + const orderedNodes = PoolGraph.orderByBfs(boostedNode).reverse(); + expect(orderedNodes.length).to.eq(10); + expect(orderedNodes[0].type).to.eq('Input'); + expect(orderedNodes[1].type).to.eq('Input'); + expect(orderedNodes[2].type).to.eq('Input'); + expect(orderedNodes[3].type).to.eq('Input'); + expect(orderedNodes[4].type).to.eq('AaveLinear'); + expect(orderedNodes[5].type).to.eq('AaveLinear'); + expect(orderedNodes[6].type).to.eq('AaveLinear'); + expect(orderedNodes[7].type).to.eq('AaveLinear'); + expect(orderedNodes[8].type).to.eq('ComposableStable'); + expect(orderedNodes[9].type).to.eq('ComposableStable'); + }); }); }); @@ -545,31 +648,84 @@ describe('Graph', () => { pools as unknown as SdkPool[] ); poolsGraph = new PoolGraph(poolProvider); - boostedNode = await poolsGraph.buildGraphFromRootPool(boostedPool.id); }); - it('should build boostedPool graph', async () => { - checkBoostedMetaBig(boostedNode, boostedMetaBigInfo, false); + context('using wrapped tokens', () => { + let tokensToUnwrap: string[]; + before(async () => { + tokensToUnwrap = [ + '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC + '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT + ]; + boostedNode = await poolsGraph.buildGraphFromRootPool( + boostedPool.id, + tokensToUnwrap + ); + }); + + it('should build boostedPool graph', async () => { + checkBoostedMetaBig(boostedNode, boostedMetaBigInfo, tokensToUnwrap); + }); + + it('should sort in breadth first order', async () => { + const orderedNodes = PoolGraph.orderByBfs(boostedNode).reverse(); + expect(orderedNodes.length).to.eq(21); + expect(orderedNodes[0].type).to.eq('Input'); + expect(orderedNodes[1].type).to.eq('Input'); + expect(orderedNodes[2].type).to.eq('Input'); + expect(orderedNodes[3].type).to.eq('Input'); + expect(orderedNodes[4].type).to.eq('Input'); + expect(orderedNodes[5].type).to.eq('Input'); + expect(orderedNodes[6].type).to.eq('WrappedToken'); + expect(orderedNodes[7].type).to.eq('WrappedToken'); + expect(orderedNodes[8].type).to.eq('WrappedToken'); + expect(orderedNodes[9].type).to.eq('WrappedToken'); + expect(orderedNodes[10].type).to.eq('WrappedToken'); + expect(orderedNodes[11].type).to.eq('WrappedToken'); + expect(orderedNodes[12].type).to.eq('AaveLinear'); + expect(orderedNodes[13].type).to.eq('AaveLinear'); + expect(orderedNodes[14].type).to.eq('AaveLinear'); + expect(orderedNodes[15].type).to.eq('AaveLinear'); + expect(orderedNodes[16].type).to.eq('AaveLinear'); + expect(orderedNodes[17].type).to.eq('AaveLinear'); + expect(orderedNodes[18].type).to.eq('ComposableStable'); + expect(orderedNodes[19].type).to.eq('ComposableStable'); + expect(orderedNodes[20].type).to.eq('ComposableStable'); + }); }); - it('should sort in breadth first order', async () => { - const orderedNodes = PoolGraph.orderByBfs(boostedNode).reverse(); - expect(orderedNodes.length).to.eq(15); - expect(orderedNodes[0].type).to.eq('Input'); - expect(orderedNodes[1].type).to.eq('Input'); - expect(orderedNodes[2].type).to.eq('Input'); - expect(orderedNodes[3].type).to.eq('Input'); - expect(orderedNodes[4].type).to.eq('Input'); - expect(orderedNodes[5].type).to.eq('Input'); - expect(orderedNodes[6].type).to.eq('AaveLinear'); - expect(orderedNodes[7].type).to.eq('AaveLinear'); - expect(orderedNodes[8].type).to.eq('AaveLinear'); - expect(orderedNodes[9].type).to.eq('AaveLinear'); - expect(orderedNodes[10].type).to.eq('AaveLinear'); - expect(orderedNodes[11].type).to.eq('AaveLinear'); - expect(orderedNodes[12].type).to.eq('ComposableStable'); - expect(orderedNodes[13].type).to.eq('ComposableStable'); - expect(orderedNodes[14].type).to.eq('ComposableStable'); + context('using non-wrapped tokens', () => { + before(async () => { + boostedNode = await poolsGraph.buildGraphFromRootPool( + boostedPool.id, + [] + ); + }); + + it('should build boostedPool graph', async () => { + checkBoostedMetaBig(boostedNode, boostedMetaBigInfo, []); + }); + + it('should sort in breadth first order', async () => { + const orderedNodes = PoolGraph.orderByBfs(boostedNode).reverse(); + expect(orderedNodes.length).to.eq(15); + expect(orderedNodes[0].type).to.eq('Input'); + expect(orderedNodes[1].type).to.eq('Input'); + expect(orderedNodes[2].type).to.eq('Input'); + expect(orderedNodes[3].type).to.eq('Input'); + expect(orderedNodes[4].type).to.eq('Input'); + expect(orderedNodes[5].type).to.eq('Input'); + expect(orderedNodes[6].type).to.eq('AaveLinear'); + expect(orderedNodes[7].type).to.eq('AaveLinear'); + expect(orderedNodes[8].type).to.eq('AaveLinear'); + expect(orderedNodes[9].type).to.eq('AaveLinear'); + expect(orderedNodes[10].type).to.eq('AaveLinear'); + expect(orderedNodes[11].type).to.eq('AaveLinear'); + expect(orderedNodes[12].type).to.eq('ComposableStable'); + expect(orderedNodes[13].type).to.eq('ComposableStable'); + expect(orderedNodes[14].type).to.eq('ComposableStable'); + }); }); }); }); diff --git a/balancer-js/src/modules/graph/graph.ts b/balancer-js/src/modules/graph/graph.ts index def10ab68..24e8b88a4 100644 --- a/balancer-js/src/modules/graph/graph.ts +++ b/balancer-js/src/modules/graph/graph.ts @@ -1,8 +1,11 @@ +import { BigNumber, parseFixed } from '@ethersproject/bignumber'; +import { Zero, WeiPerEther } from '@ethersproject/constants'; + import { BalancerError, BalancerErrorCode } from '@/balancerErrors'; import { isSameAddress, parsePoolInfo } from '@/lib/utils'; +import { _downscaleDown } from '@/lib/utils/solidityMaths'; import { Pool, PoolAttribute, PoolType } from '@/types'; -import { Zero, WeiPerEther } from '@ethersproject/constants'; -import { BigNumber, parseFixed } from '@ethersproject/bignumber'; + import { Findable } from '../data/types'; import { PoolTypeConcerns } from '../pools/pool-type-concerns'; @@ -24,9 +27,11 @@ export interface Node { isLeaf: boolean; spotPrices: SpotPrices; decimals: number; + balance: string; + priceRate: string; } -type JoinAction = 'input' | 'batchSwap' | 'joinPool'; +type JoinAction = 'input' | 'batchSwap' | 'wrap' | 'joinPool'; const joinActions = new Map(); supportedPoolTypes.forEach((type) => { if (type.includes('Linear') && supportedPoolTypes.includes(type)) @@ -41,7 +46,7 @@ joinActions.set(PoolType.StablePhantom, 'batchSwap'); joinActions.set(PoolType.Weighted, 'joinPool'); joinActions.set(PoolType.ComposableStable, 'joinPool'); -type ExitAction = 'output' | 'batchSwap' | 'exitPool' | 'exitPoolProportional'; +type ExitAction = 'output' | 'batchSwap' | 'unwrap' | 'exitPool'; const exitActions = new Map(); supportedPoolTypes.forEach((type) => { if (type.includes('Linear') && supportedPoolTypes.includes(type)) @@ -59,7 +64,10 @@ exitActions.set(PoolType.ComposableStable, 'exitPool'); export class PoolGraph { constructor(private pools: Findable) {} - async buildGraphFromRootPool(poolId: string): Promise { + async buildGraphFromRootPool( + poolId: string, + tokensToUnwrap: string[] + ): Promise { const rootPool = await this.pools.find(poolId); if (!rootPool) throw new BalancerError(BalancerErrorCode.POOL_DOESNT_EXIST); const nodeIndex = 0; @@ -67,7 +75,8 @@ export class PoolGraph { rootPool.address, nodeIndex, undefined, - WeiPerEther + WeiPerEther, + tokensToUnwrap ); return rootNode[0]; } @@ -89,7 +98,8 @@ export class PoolGraph { address: string, nodeIndex: number, parent: Node | undefined, - proportionOfParent: BigNumber + proportionOfParent: BigNumber, + tokensToUnwrap: string[] ): Promise<[Node, number]> { const pool = await this.pools.findBy('address', address); @@ -99,21 +109,21 @@ export class PoolGraph { throw new BalancerError(BalancerErrorCode.POOL_DOESNT_EXIST); } else { // If pool not found by address, but it has parent, assume it's a leaf token and add a leafTokenNode - // TODO: maybe it's a safety issue? Can we be safer? const parentPool = (await this.pools.findBy( 'address', parent.address )) as Pool; - const leafTokenDecimals = - parentPool.tokens[parentPool.tokensList.indexOf(address)].decimals ?? - 18; + const tokenIndex = parentPool.tokensList.indexOf(address); + const leafTokenDecimals = parentPool.tokens[tokenIndex].decimals ?? 18; + const { balancesEvm } = parsePoolInfo(parentPool); const nodeInfo = PoolGraph.createInputTokenNode( nodeIndex, address, leafTokenDecimals, parent, - proportionOfParent + proportionOfParent, + balancesEvm[tokenIndex].toString() ); return nodeInfo; } @@ -159,6 +169,8 @@ export class PoolGraph { isLeaf: false, spotPrices, decimals, + balance: pool.totalShares, + priceRate: WeiPerEther.toString(), }; this.updateNodeIfProportionalExit(pool, poolNode); nodeIndex++; @@ -166,7 +178,8 @@ export class PoolGraph { [poolNode, nodeIndex] = this.createLinearNodeChildren( poolNode, nodeIndex, - pool + pool, + tokensToUnwrap ); } else { const { balancesEvm } = parsePoolInfo(pool); @@ -190,7 +203,8 @@ export class PoolGraph { pool.tokens[i].address, nodeIndex, poolNode, - finalProportion + finalProportion, + tokensToUnwrap ); nodeIndex = childNode[1]; if (childNode[0]) poolNode.children.push(childNode[0]); @@ -216,25 +230,104 @@ export class PoolGraph { createLinearNodeChildren( linearPoolNode: Node, nodeIndex: number, - linearPool: Pool + linearPool: Pool, + tokensToUnwrap: string[] ): [Node, number] { // Main token if (linearPool.mainIndex === undefined) throw new Error('Issue With Linear Pool'); + if ( + tokensToUnwrap + .map((t) => t.toLowerCase()) + .includes(linearPool.tokensList[linearPool.mainIndex].toLowerCase()) + ) { + // Linear pool will be joined via wrapped token. This will be the child node. + const wrappedNodeInfo = this.createWrappedTokenNode( + linearPool, + nodeIndex, + linearPoolNode, + linearPoolNode.proportionOfParent + ); + linearPoolNode.children.push(wrappedNodeInfo[0]); + return [linearPoolNode, wrappedNodeInfo[1]]; + } else { + const { balancesEvm } = parsePoolInfo(linearPool); + const mainTokenDecimals = + linearPool.tokens[linearPool.mainIndex].decimals ?? 18; + + const nodeInfo = PoolGraph.createInputTokenNode( + nodeIndex, + linearPool.tokensList[linearPool.mainIndex], + mainTokenDecimals, + linearPoolNode, + linearPoolNode.proportionOfParent, + balancesEvm[linearPool.mainIndex].toString() + ); + linearPoolNode.children.push(nodeInfo[0]); + nodeIndex = nodeInfo[1]; + return [linearPoolNode, nodeIndex]; + } + } + + createWrappedTokenNode( + linearPool: Pool, + nodeIndex: number, + parent: Node | undefined, + proportionOfParent: BigNumber + ): [Node, number] { + if ( + linearPool.wrappedIndex === undefined || + linearPool.mainIndex === undefined + ) + throw new Error('Issue With Linear Pool'); + + const { balancesEvm, upScaledBalances, scalingFactorsRaw, priceRates } = + parsePoolInfo(linearPool); + + const wrappedTokenNode: Node = { + type: 'WrappedToken', + address: linearPool.tokensList[linearPool.wrappedIndex], + id: 'N/A', + children: [], + marked: false, + joinAction: 'wrap', + exitAction: 'unwrap', + isProportionalExit: false, + index: nodeIndex.toString(), + parent, + proportionOfParent, + isLeaf: false, + spotPrices: {}, + decimals: 18, + balance: balancesEvm[linearPool.wrappedIndex].toString(), + priceRate: priceRates[linearPool.wrappedIndex].toString(), + }; + nodeIndex++; + const mainTokenDecimals = linearPool.tokens[linearPool.mainIndex].decimals ?? 18; - const nodeInfo = PoolGraph.createInputTokenNode( + /** + * - upscaledBalances takes price rate into account, which is equivalent to unwrapping tokens + * - downscaling with scalingFactorsRaw will downscale the unwrapped balance to the main token decimals + */ + const unwrappedBalance = _downscaleDown( + upScaledBalances[linearPool.wrappedIndex], + scalingFactorsRaw[linearPool.mainIndex] + ).toString(); + + const inputNode = PoolGraph.createInputTokenNode( nodeIndex, linearPool.tokensList[linearPool.mainIndex], mainTokenDecimals, - linearPoolNode, - linearPoolNode.proportionOfParent + wrappedTokenNode, + proportionOfParent, + unwrappedBalance ); - linearPoolNode.children.push(nodeInfo[0]); - nodeIndex = nodeInfo[1]; - return [linearPoolNode, nodeIndex]; + wrappedTokenNode.children = [inputNode[0]]; + nodeIndex = inputNode[1]; + return [wrappedTokenNode, nodeIndex]; } static createInputTokenNode( @@ -242,7 +335,8 @@ export class PoolGraph { address: string, decimals: number, parent: Node | undefined, - proportionOfParent: BigNumber + proportionOfParent: BigNumber, + balance: string ): [Node, number] { return [ { @@ -260,6 +354,8 @@ export class PoolGraph { isLeaf: true, spotPrices: {}, decimals, + balance, + priceRate: WeiPerEther.toString(), }, nodeIndex + 1, ]; @@ -302,11 +398,15 @@ export class PoolGraph { } // Get full graph from root pool and return ordered nodes - getGraphNodes = async (isJoin: boolean, poolId: string): Promise => { + getGraphNodes = async ( + isJoin: boolean, + poolId: string, + tokensToUnwrap: string[] + ): Promise => { const rootPool = await this.pools.find(poolId); if (!rootPool) throw new BalancerError(BalancerErrorCode.POOL_DOESNT_EXIST); - const rootNode = await this.buildGraphFromRootPool(poolId); + const rootNode = await this.buildGraphFromRootPool(poolId, tokensToUnwrap); if (rootNode.id !== poolId) throw new Error('Error creating graph nodes'); diff --git a/balancer-js/src/modules/joins/joins.module.ts b/balancer-js/src/modules/joins/joins.module.ts index 1c4e6d01a..d97c81f07 100644 --- a/balancer-js/src/modules/joins/joins.module.ts +++ b/balancer-js/src/modules/joins/joins.module.ts @@ -67,7 +67,7 @@ export class Join { throw new BalancerError(BalancerErrorCode.INPUT_LENGTH_MISMATCH); // Create nodes for each pool/token interaction and order by breadth first - const orderedNodes = await this.poolGraph.getGraphNodes(true, poolId); + const orderedNodes = await this.poolGraph.getGraphNodes(true, poolId, []); const joinPaths = Join.getJoinPaths(orderedNodes, tokensIn, amountsIn); @@ -229,7 +229,8 @@ export class Join { nonLeafInputNode.address, nonLeafInputNode.decimals, nonLeafInputNode.parent, - WeiPerEther + WeiPerEther, + nonLeafInputNode.balance ); // Update index to be actual amount in inputTokenNode.index = proportionalNonLeafAmountIn; @@ -529,7 +530,6 @@ export class Join { // Sender's rule // 1. If any child node is an input node, tokens are coming from the user - // 2. Wrapped tokens have to come from user (Relayer has no approval for wrapped tokens) const hasChildInput = node.children .filter((c) => this.shouldBeConsidered(c)) .some((c) => c.joinAction === 'input'); @@ -677,7 +677,7 @@ export class Join { assets: string[]; amounts: string[]; } => { - // We only need swaps for main/wrapped > linearBpt so shouldn't be more than token > token + // We only need swaps for main > linearBpt so shouldn't be more than token > token if (node.children.length !== 1) throw new Error('Unsupported swap'); const tokenIn = node.children[0].address; const amountIn = this.getOutputRefValue(joinPathIndex, node.children[0]); diff --git a/balancer-js/src/modules/liquidity-managment/migrations.integrations.spec.ts b/balancer-js/src/modules/liquidity-managment/migrations.integrations.spec.ts index bcce554a4..37df2b2dc 100644 --- a/balancer-js/src/modules/liquidity-managment/migrations.integrations.spec.ts +++ b/balancer-js/src/modules/liquidity-managment/migrations.integrations.spec.ts @@ -263,7 +263,7 @@ describe('Migrations', function () { ); beforeEach(async () => { - await reset('https://rpc.ankr.com/polygon', provider, 41098000); + await reset('https://rpc.ankr.com/polygon', provider, 42462957); signer = await impersonateAccount(address, provider); // approve relayer @@ -274,7 +274,7 @@ describe('Migrations', function () { context('ComposableStable to ComposableStable', () => { before(() => { - address = '0x92a0b2c089733bef43ac367d2ce7783526aea590'; + address = '0xe80a6a7b4fdadf0aa59f3f669a8d394d1d4da86b'; }); it('should build a migration using exit / join', async () => { diff --git a/balancer-js/src/modules/pools/index.ts b/balancer-js/src/modules/pools/index.ts index 33a4b6872..22d152a24 100644 --- a/balancer-js/src/modules/pools/index.ts +++ b/balancer-js/src/modules/pools/index.ts @@ -14,7 +14,7 @@ import { PoolTypeConcerns } from './pool-type-concerns'; import { PoolApr } from './apr/apr'; import { Liquidity } from '../liquidity/liquidity.module'; import { Join } from '../joins/joins.module'; -import { Exit } from '../exits/exits.module'; +import { Exit, GeneralisedExitOutput, ExitInfo } from '../exits/exits.module'; import { PoolVolume } from './volume/volume'; import { PoolFees } from './fees/fees'; import { Simulation, SimulationType } from '../simulation/simulation.module'; @@ -356,8 +356,9 @@ export class Pools implements Findable { * @param userAddress User address * @param slippage Maximum slippage tolerance in bps i.e. 50 = 0.5%. * @param signer JsonRpcSigner that will sign the staticCall transaction if Static simulation chosen - * @param simulationType Simulation type (VaultModel, Tenderly or Static) + * @param simulationType Simulation type (Tenderly or Static) - VaultModel should not be used to build exit transaction * @param authorisation Optional auhtorisation call to be added to the chained transaction + * @param tokensToUnwrap List all tokens that requires exit by unwrapping - info provided by getExitInfo * @returns transaction data ready to be sent to the network along with tokens, min and expected amounts out. */ async generalisedExit( @@ -366,24 +367,42 @@ export class Pools implements Findable { userAddress: string, slippage: string, signer: JsonRpcSigner, - simulationType: SimulationType, - authorisation?: string - ): Promise<{ - to: string; - encodedCall: string; - tokensOut: string[]; - expectedAmountsOut: string[]; - minAmountsOut: string[]; - priceImpact: string; - }> { - return this.exitService.exitPool( + simulationType: SimulationType.Static | SimulationType.Tenderly, + authorisation?: string, + tokensToUnwrap?: string[] + ): Promise { + return this.exitService.buildExitCall( poolId, amount, userAddress, slippage, signer, simulationType, - authorisation + authorisation, + tokensToUnwrap + ); + } + + /** + * Gets info required to build generalised exit transaction + * + * @param poolId Pool id + * @param amountBptIn BPT amount in EVM scale + * @param userAddress User address + * @param signer JsonRpcSigner that will sign the staticCall transaction if Static simulation chosen + * @returns info required to build a generalised exit transaction including whether tokens need to be unwrapped + */ + async getExitInfo( + poolId: string, + amountBptIn: string, + userAddress: string, + signer: JsonRpcSigner + ): Promise { + return this.exitService.getExitInfo( + poolId, + amountBptIn, + userAddress, + signer ); } diff --git a/balancer-js/src/modules/pools/pool-type-concerns.ts b/balancer-js/src/modules/pools/pool-type-concerns.ts index 774c798bd..5e6d739bd 100644 --- a/balancer-js/src/modules/pools/pool-type-concerns.ts +++ b/balancer-js/src/modules/pools/pool-type-concerns.ts @@ -8,6 +8,7 @@ import { Linear } from './pool-types/linear.module'; import { BalancerError, BalancerErrorCode } from '@/balancerErrors'; import { isLinearish } from '@/lib/utils'; import { FX } from '@/modules/pools/pool-types/fx.module'; +import { Gyro } from '@/modules/pools/pool-types/gyro.module'; /** * Wrapper around pool type specific methods. @@ -36,25 +37,30 @@ export class PoolTypeConcerns { | Linear { // Calculate spot price using pool type switch (poolType) { - case 'Weighted': - case 'Investment': - case 'LiquidityBootstrapping': { - return new Weighted(); - } - case 'Stable': { - return new Stable(); - } case 'ComposableStable': { return new ComposableStable(); } + case 'FX': { + return new FX(); + } + case 'GyroE': + case 'Gyro2': + case 'Gyro3': { + return new Gyro(); + } case 'MetaStable': { return new MetaStable(); } + case 'Stable': { + return new Stable(); + } case 'StablePhantom': { return new StablePhantom(); } - case 'FX': { - return new FX(); + case 'Investment': + case 'LiquidityBootstrapping': + case 'Weighted': { + return new Weighted(); } default: { // Handles all Linear pool types diff --git a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exit.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exit.concern.ts index c7e9d1b52..b73a1eba4 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exit.concern.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exit.concern.ts @@ -304,7 +304,7 @@ export class ComposableStablePoolExit implements ExitConcern { ); // Check if there's any relevant stable pool info missing - if (pool.tokens.some((token) => !token.decimals)) + if (pool.tokens.some((token) => token.decimals === undefined)) throw new BalancerError(BalancerErrorCode.MISSING_DECIMALS); if (!pool.amp) throw new BalancerError(BalancerErrorCode.MISSING_AMP); }; @@ -346,7 +346,7 @@ export class ComposableStablePoolExit implements ExitConcern { } // Check if there's any relevant stable pool info missing - if (pool.tokens.some((token) => !token.decimals)) + if (pool.tokens.some((token) => token.decimals === undefined)) throw new BalancerError(BalancerErrorCode.MISSING_DECIMALS); if (!pool.amp) throw new BalancerError(BalancerErrorCode.MISSING_AMP); }; diff --git a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exitV2.concern.integration.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exitV2.concern.integration.spec.ts index c9c2808e3..dfa67fa2a 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exitV2.concern.integration.spec.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exitV2.concern.integration.spec.ts @@ -30,7 +30,7 @@ const blockNumber = 40818844; let pool: PoolWithMethods; describe('ComposableStableV2 Exits', () => { - // We have to rest the fork between each test as pool value changes after tx is submitted + // We have to reset the fork between each test as pool value changes after tx is submitted beforeEach(async () => { // Setup forked network, set initial token balances and allowances await forkSetup( diff --git a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exitV3.concern.integration.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exitV3.concern.integration.spec.ts index 31b035a13..928b4ea0d 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exitV3.concern.integration.spec.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exitV3.concern.integration.spec.ts @@ -1,110 +1,64 @@ // yarn test:only ./src/modules/pools/pool-types/concerns/composableStable/exitV3.concern.integration.spec.ts import dotenv from 'dotenv'; -import { expect } from 'chai'; -import { ethers } from 'hardhat'; -import { BigNumber, parseFixed } from '@ethersproject/bignumber'; -import { insert, Network, PoolWithMethods, removeItem } from '@/.'; -import { subSlippage, addSlippage } from '@/lib/utils/slippageHelper'; +import { parseFixed } from '@ethersproject/bignumber'; +import { JsonRpcProvider } from '@ethersproject/providers'; import { - forkSetup, - TestPoolHelper, - sendTransactionGetBalances, -} from '@/test/lib/utils'; + BALANCER_NETWORK_CONFIG, + getPoolAddress, + Network, + Pools, + PoolWithMethods, + removeItem, +} from '@/.'; +import { forkSetup, getPoolFromFile, updateFromChain } from '@/test/lib/utils'; +import { testExactBptIn, testExactTokensOut } from '@/test/lib/exitHelper'; dotenv.config(); const network = Network.MAINNET; const { ALCHEMY_URL: jsonRpcUrl } = process.env; const rpcUrl = 'http://127.0.0.1:8545'; -const provider = new ethers.providers.JsonRpcProvider(rpcUrl, network); +const provider = new JsonRpcProvider(rpcUrl, network); const signer = provider.getSigner(); const blockNumber = 16649181; // wstETH-rETH-sfrxETH-BPT const testPoolId = '0x5aee1e99fe86960377de9f88689616916d5dcabe000000000000000000000467'; - -let signerAddress: string; let pool: PoolWithMethods; -// TODO Add these tests back once Protocol Fees are handled - check V1 and V2 tests to be used as reference -describe.skip('ComposableStableV3 Exits', () => { - // We have to rest the fork between each test as pool value changes after tx is submitted +describe('ComposableStableV3 Exits', () => { beforeEach(async () => { - signerAddress = await signer.getAddress(); - - const testPool = new TestPoolHelper( - testPoolId, - network, - rpcUrl, - blockNumber - ); - - // Gets initial pool info from Subgraph - pool = await testPool.getPool(); - // Setup forked network, set initial token balances and allowances await forkSetup( signer, - pool.tokensList, - Array(pool.tokensList.length).fill(0), - Array(pool.tokensList.length).fill(parseFixed('10', 18).toString()), + [getPoolAddress(testPoolId)], + [0], + [parseFixed('10000', 18).toString()], jsonRpcUrl as string, blockNumber ); + let testPool = await getPoolFromFile(testPoolId, network); // Updatate pool info with onchain state from fork block no - pool = await testPool.getPool(); - }); + testPool = await updateFromChain(testPool, network, provider); + pool = Pools.wrap(testPool, BALANCER_NETWORK_CONFIG[network]); + }); context('exitExactBPTIn', async () => { it('single token max out', async () => { const bptIn = parseFixed('0.1', 18).toString(); - const slippage = '10'; - const { to, data, minAmountsOut, expectedAmountsOut } = - pool.buildExitExactBPTIn( - signerAddress, - bptIn, - slippage, - false, - pool.tokensList[1] - ); - const { transactionReceipt, balanceDeltas } = - await sendTransactionGetBalances( - pool.tokensList, - signer, - signerAddress, - to, - data - ); - expect(transactionReceipt.status).to.eq(1); - const expectedDeltas = insert(expectedAmountsOut, pool.bptIndex, bptIn); - expect(expectedDeltas).to.deep.eq(balanceDeltas.map((a) => a.toString())); - const expectedMins = expectedAmountsOut.map((a) => - subSlippage(BigNumber.from(a), BigNumber.from(slippage)).toString() - ); - expect(expectedMins).to.deep.eq(minAmountsOut); + const tokenOut = pool.tokensList[0]; + await testExactBptIn(pool, signer, bptIn, tokenOut); + }); + it('single token max out, token out after BPT index', async () => { + const bptIn = parseFixed('0.1', 18).toString(); + const tokenOut = pool.tokensList[2]; + await testExactBptIn(pool, signer, bptIn, tokenOut); }); it('proportional exit', async () => { - const bptIn = parseFixed('0.01', 18).toString(); - const slippage = '10'; - const { to, data, minAmountsOut, expectedAmountsOut } = - pool.buildExitExactBPTIn(signerAddress, bptIn, slippage, false); - const { transactionReceipt, balanceDeltas } = - await sendTransactionGetBalances( - pool.tokensList, - signer, - signerAddress, - to, - data - ); - expect(transactionReceipt.status).to.eq(1); - const expectedDeltas = insert(expectedAmountsOut, pool.bptIndex, bptIn); - expect(expectedDeltas).to.deep.eq(balanceDeltas.map((a) => a.toString())); - const expectedMins = expectedAmountsOut.map((a) => - subSlippage(BigNumber.from(a), BigNumber.from(slippage)).toString() - ); - expect(expectedMins).to.deep.eq(minAmountsOut); + const bptIn = parseFixed('0.1', 18).toString(); + await testExactBptIn(pool, signer, bptIn); }); }); @@ -112,61 +66,21 @@ describe.skip('ComposableStableV3 Exits', () => { it('all tokens with value', async () => { const tokensOut = removeItem(pool.tokensList, pool.bptIndex); const amountsOut = tokensOut.map((_, i) => - parseFixed((i * 1).toString(), 18).toString() + parseFixed(((i + 1) * 0.1).toString(), 18).toString() ); - const slippage = '7'; - const { to, data, maxBPTIn, expectedBPTIn } = - pool.buildExitExactTokensOut( - signerAddress, - tokensOut, - amountsOut, - slippage - ); - const { transactionReceipt, balanceDeltas } = - await sendTransactionGetBalances( - pool.tokensList, - signer, - signerAddress, - to, - data - ); - expect(transactionReceipt.status).to.eq(1); - const expectedDeltas = insert(amountsOut, pool.bptIndex, expectedBPTIn); - expect(expectedDeltas).to.deep.eq(balanceDeltas.map((a) => a.toString())); - const expectedMaxBpt = addSlippage( - BigNumber.from(expectedBPTIn), - BigNumber.from(slippage) - ).toString(); - expect(expectedMaxBpt).to.deep.eq(maxBPTIn); + await testExactTokensOut(pool, signer, tokensOut, amountsOut); }); it('single token with value', async () => { const tokensOut = removeItem(pool.tokensList, pool.bptIndex); const amountsOut = Array(tokensOut.length).fill('0'); - amountsOut[0] = parseFixed('2', 18).toString(); - const slippage = '7'; - const { to, data, maxBPTIn, expectedBPTIn } = - pool.buildExitExactTokensOut( - signerAddress, - tokensOut, - amountsOut, - slippage - ); - const { transactionReceipt, balanceDeltas } = - await sendTransactionGetBalances( - pool.tokensList, - signer, - signerAddress, - to, - data - ); - expect(transactionReceipt.status).to.eq(1); - const expectedDeltas = insert(amountsOut, pool.bptIndex, expectedBPTIn); - expect(expectedDeltas).to.deep.eq(balanceDeltas.map((a) => a.toString())); - const expectedMaxBpt = addSlippage( - BigNumber.from(expectedBPTIn), - BigNumber.from(slippage) - ).toString(); - expect(expectedMaxBpt).to.deep.eq(maxBPTIn); + amountsOut[0] = parseFixed('0.1', 18).toString(); + await testExactTokensOut(pool, signer, tokensOut, amountsOut); + }); + it('single token with value, token out after BPT index', async () => { + const tokensOut = removeItem(pool.tokensList, pool.bptIndex); + const amountsOut = Array(tokensOut.length).fill('0'); + amountsOut[2] = parseFixed('0.1', 18).toString(); + await testExactTokensOut(pool, signer, tokensOut, amountsOut); }); }); }); diff --git a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exitV4.concern.integration.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exitV4.concern.integration.spec.ts new file mode 100644 index 000000000..cf561fe17 --- /dev/null +++ b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exitV4.concern.integration.spec.ts @@ -0,0 +1,86 @@ +// yarn test:only ./src/modules/pools/pool-types/concerns/composableStable/exitV4.concern.integration.spec.ts +import dotenv from 'dotenv'; +import { parseFixed } from '@ethersproject/bignumber'; +import { JsonRpcProvider } from '@ethersproject/providers'; +import { + BALANCER_NETWORK_CONFIG, + getPoolAddress, + Network, + Pools, + PoolWithMethods, + removeItem, +} from '@/.'; +import { forkSetup, getPoolFromFile, updateFromChain } from '@/test/lib/utils'; +import { testExactBptIn, testExactTokensOut } from '@/test/lib/exitHelper'; + +dotenv.config(); + +const network = Network.MAINNET; +const { ALCHEMY_URL: jsonRpcUrl } = process.env; +const rpcUrl = 'http://127.0.0.1:8545'; +const provider = new JsonRpcProvider(rpcUrl, network); +const signer = provider.getSigner(); +const blockNumber = 17280000; + +// wstETH-rETH-sfrxETH-BPT +const testPoolId = + '0xec3626fee40ef95e7c0cbb1d495c8b67b34d398300000000000000000000053d'; +let pool: PoolWithMethods; + +describe('ComposableStableV4 Exits', () => { + beforeEach(async () => { + // Setup forked network, set initial token balances and allowances + await forkSetup( + signer, + [getPoolAddress(testPoolId)], + [0], + [parseFixed('10000', 18).toString()], + jsonRpcUrl as string, + blockNumber + ); + + let testPool = await getPoolFromFile(testPoolId, network); + // Updatate pool info with onchain state from fork block no + testPool = await updateFromChain(testPool, network, provider); + + pool = Pools.wrap(testPool, BALANCER_NETWORK_CONFIG[network]); + }); + context('exitExactBPTIn', async () => { + it('single token max out', async () => { + const bptIn = parseFixed('0.1', 18).toString(); + const tokenOut = pool.tokensList[0]; + await testExactBptIn(pool, signer, bptIn, tokenOut); + }); + it('single token max out, token out after BPT index', async () => { + const bptIn = parseFixed('0.1', 18).toString(); + const tokenOut = pool.tokensList[2]; + await testExactBptIn(pool, signer, bptIn, tokenOut); + }); + it('proportional exit', async () => { + const bptIn = parseFixed('0.1', 18).toString(); + await testExactBptIn(pool, signer, bptIn); + }); + }); + + context('exitExactTokensOut', async () => { + it('all tokens with value', async () => { + const tokensOut = removeItem(pool.tokensList, pool.bptIndex); + const amountsOut = tokensOut.map((_, i) => + parseFixed(((i + 1) * 0.1).toString(), 18).toString() + ); + await testExactTokensOut(pool, signer, tokensOut, amountsOut); + }); + it('single token with value', async () => { + const tokensOut = removeItem(pool.tokensList, pool.bptIndex); + const amountsOut = Array(tokensOut.length).fill('0'); + amountsOut[0] = parseFixed('0.1', 18).toString(); + await testExactTokensOut(pool, signer, tokensOut, amountsOut); + }); + it('single token with value, token out after BPT index', async () => { + const tokensOut = removeItem(pool.tokensList, pool.bptIndex); + const amountsOut = Array(tokensOut.length).fill('0'); + amountsOut[1] = parseFixed('0.1', 18).toString(); + await testExactTokensOut(pool, signer, tokensOut, amountsOut); + }); + }); +}); diff --git a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.integration.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.integration.spec.ts index ee3fee103..e92d1de8d 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.integration.spec.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.integration.spec.ts @@ -1,7 +1,6 @@ // yarn test:only ./src/modules/pools/pool-types/concerns/composableStable/join.concern.integration.spec.ts -import dotenv from 'dotenv'; -import { ethers } from 'hardhat'; import { parseFixed } from '@ethersproject/bignumber'; +import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; import { removeItem, @@ -10,7 +9,12 @@ import { replace, BALANCER_NETWORK_CONFIG, } from '@/.'; -import { forkSetup, TestPoolHelper } from '@/test/lib/utils'; +import { + FORK_NODES, + forkSetup, + RPC_URLS, + TestPoolHelper, +} from '@/test/lib/utils'; import { testExactTokensIn, testAttributes, @@ -18,36 +22,41 @@ import { } from '@/test/lib/joinHelper'; import { AddressZero } from '@ethersproject/constants'; -dotenv.config(); - -const network = Network.POLYGON; -const { ALCHEMY_URL_POLYGON: jsonRpcUrl } = process.env; -const rpcUrl = 'http://127.0.0.1:8137'; -const provider = new ethers.providers.JsonRpcProvider(rpcUrl, network); -const signer = provider.getSigner(); -const blockNumber = 41400000; -const testPoolId = - '0x02d2e2d7a89d6c5cb3681cfcb6f7dac02a55eda400000000000000000000088f'; - describe('ComposableStable Pool - Join Functions', async () => { let signerAddress: string; let pool: PoolWithMethods; let testPoolHelper: TestPoolHelper; - before(async () => { - signerAddress = await signer.getAddress(); - - testPoolHelper = new TestPoolHelper( - testPoolId, - network, - rpcUrl, - blockNumber - ); - - // Gets initial pool info from Subgraph - pool = await testPoolHelper.getPool(); - }); + let network: Network; + let jsonRpcUrl: string; + let rpcUrl: string; + let provider: JsonRpcProvider; + let signer: JsonRpcSigner; + let blockNumber: number; + let testPoolId: string; + + context('Integration Tests - Join V1', async () => { + before(async () => { + network = Network.POLYGON; + rpcUrl = RPC_URLS[network]; + provider = new JsonRpcProvider(rpcUrl, network); + signer = provider.getSigner(); + signerAddress = await signer.getAddress(); + jsonRpcUrl = FORK_NODES[network]; + blockNumber = 42462957; + testPoolId = + '0x02d2e2d7a89d6c5cb3681cfcb6f7dac02a55eda400000000000000000000088f'; + + testPoolHelper = new TestPoolHelper( + testPoolId, + network, + rpcUrl, + blockNumber + ); + + // Gets initial pool info from Subgraph + pool = await testPoolHelper.getPool(); + }); - context('Integration Tests', async () => { // We have to rest the fork between each test as pool value changes after tx is submitted beforeEach(async () => { // Setup forked network, set initial token balances and allowances @@ -56,7 +65,7 @@ describe('ComposableStable Pool - Join Functions', async () => { pool.tokensList, [0, 3, 0], Array(pool.tokensList.length).fill(parseFixed('100000', 18).toString()), - jsonRpcUrl as string, + jsonRpcUrl, blockNumber // holds the same state as the static repository ); @@ -95,11 +104,67 @@ describe('ComposableStable Pool - Join Functions', async () => { }); }); + context('Integration Tests - Join V4', async () => { + beforeEach(async () => { + network = Network.MAINNET; + rpcUrl = RPC_URLS[network]; + provider = new JsonRpcProvider(rpcUrl, network); + signer = provider.getSigner(); + signerAddress = await signer.getAddress(); + jsonRpcUrl = FORK_NODES[network]; + blockNumber = 17280000; + testPoolId = + '0xd61e198e139369a40818fe05f5d5e6e045cd6eaf000000000000000000000540'; + + testPoolHelper = new TestPoolHelper( + testPoolId, + network, + rpcUrl, + blockNumber + ); + + // Gets initial pool info from Subgraph + pool = await testPoolHelper.getPool(); + }); + + // We have to rest the fork between each test as pool value changes after tx is submitted + beforeEach(async () => { + // Setup forked network, set initial token balances and allowances + await forkSetup( + signer, + pool.tokensList, + [0, 5, 0], + Array(pool.tokensList.length).fill(parseFixed('10', 18).toString()), + jsonRpcUrl, + blockNumber, // holds the same state as the static repository + [false, true, false] + ); + + // Updatate pool info with onchain state from fork block no + pool = await testPoolHelper.getPool(); + }); + + it('should join - all tokens have value', async () => { + const tokensIn = removeItem(pool.tokensList, pool.bptIndex); + const amountsIn = tokensIn.map((_, i) => + parseFixed(((i + 1) * 0.1).toString(), 18).toString() + ); + await testExactTokensIn(pool, signer, signerAddress, tokensIn, amountsIn); + }); + + it('should join - single token has value', async () => { + const tokensIn = removeItem(pool.tokensList, pool.bptIndex); + const amountsIn = Array(tokensIn.length).fill('0'); + amountsIn[0] = parseFixed('0.202', 18).toString(); + await testExactTokensIn(pool, signer, signerAddress, tokensIn, amountsIn); + }); + }); + context('Unit Tests', () => { it('should return correct attributes for joining', () => { const tokensIn = removeItem(pool.tokensList, pool.bptIndex); const amountsIn = tokensIn.map((_, i) => - parseFixed(((i + 1) * 100).toString(), 18).toString() + parseFixed(((i + 1) * 0.1).toString(), 18).toString() ); testAttributes(pool, testPoolId, signerAddress, tokensIn, amountsIn); }); @@ -107,7 +172,7 @@ describe('ComposableStable Pool - Join Functions', async () => { it('should automatically sort tokens/amounts in correct order', () => { const tokensIn = removeItem(pool.tokensList, pool.bptIndex); const amountsIn = tokensIn.map((_, i) => - parseFixed(((i + 1) * 100).toString(), 18).toString() + parseFixed(((i + 1) * 0.1).toString(), 18).toString() ); testSortingInputs(pool, signerAddress, tokensIn, amountsIn); }); diff --git a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.ts index 4b49ca39b..a438d06b1 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.ts @@ -106,11 +106,12 @@ export class ComposableStablePoolJoin implements JoinConcern { * V1: Does not have proportional exits. * V2: Reintroduced proportional exits. Has vulnerability. * V3: Fixed vulnerability. Functionally the same as V2. + * V4: Update to use new create method with new salt parameter */ - if (pool.poolTypeVersion < 4) + if (pool.poolTypeVersion <= 4) return this.sortV1(wrappedNativeAsset, tokensIn, amountsIn, pool); // Not release yet and needs tests to confirm - // else if (values.pool.poolTypeVersion === 4) + // else if (values.pool.poolTypeVersion === 5) // sortedValues = this.sortV4( // values.tokensIn, // values.amountsIn, diff --git a/balancer-js/src/modules/pools/pool-types/concerns/fx/liquidity.concern.integration.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/fx/liquidity.concern.integration.spec.ts index 8446a0bb4..84d7b91ca 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/fx/liquidity.concern.integration.spec.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/fx/liquidity.concern.integration.spec.ts @@ -21,7 +21,7 @@ const signer = provider.getSigner(); const testPoolId = '0x726e324c29a1e49309672b244bdc4ff62a270407000200000000000000000702'; let pool: PoolWithMethods; -const blockNumber = 41400000; +const blockNumber = 43015527; describe('FX Pool - Calculate Liquidity', () => { const sdkConfig = { @@ -54,12 +54,6 @@ describe('FX Pool - Calculate Liquidity', () => { ).total_.toBigInt(); const liquidityBigInt = parseFixed(liquidity, 18).toBigInt(); // expecting 5% of margin error - console.log( - formatFixed( - SolidityMaths.divDownFixed(liquidityBigInt, liquidityFromContract), - 18 - ).toString() - ); expect( parseFloat( formatFixed( diff --git a/balancer-js/src/modules/pools/pool-types/concerns/gyro/exit.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/gyro/exit.concern.ts new file mode 100644 index 000000000..098c23f12 --- /dev/null +++ b/balancer-js/src/modules/pools/pool-types/concerns/gyro/exit.concern.ts @@ -0,0 +1,41 @@ +import { + ExitConcern, + ExitExactBPTInAttributes, + ExitExactBPTInParameters, + ExitExactTokensOutAttributes, + ExitExactTokensOutParameters, +} from '@/modules/pools/pool-types/concerns/types'; + +export class GyroExitConcern implements ExitConcern { + buildExitExactTokensOut({ + exiter, + pool, + tokensOut, + amountsOut, + slippage, + wrappedNativeAsset, + }: ExitExactTokensOutParameters): ExitExactTokensOutAttributes { + console.log( + exiter, + pool, + tokensOut, + amountsOut, + slippage, + wrappedNativeAsset + ); + throw new Error('Not implemented'); + } + + buildRecoveryExit({ + exiter, + pool, + bptIn, + slippage, + }: Pick< + ExitExactBPTInParameters, + 'exiter' | 'pool' | 'bptIn' | 'slippage' + >): ExitExactBPTInAttributes { + console.log(exiter, pool, bptIn, slippage); + throw new Error('Not implemented'); + } +} diff --git a/balancer-js/src/modules/pools/pool-types/concerns/gyro/join.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/gyro/join.concern.ts new file mode 100644 index 000000000..1c4f528d9 --- /dev/null +++ b/balancer-js/src/modules/pools/pool-types/concerns/gyro/join.concern.ts @@ -0,0 +1,26 @@ +import { + JoinConcern, + JoinPoolAttributes, + JoinPoolParameters, +} from '@/modules/pools/pool-types/concerns/types'; + +export class GyroJoinConcern implements JoinConcern { + buildJoin({ + joiner, + pool, + tokensIn, + amountsIn, + slippage, + wrappedNativeAsset, + }: JoinPoolParameters): JoinPoolAttributes { + console.log( + joiner, + pool, + tokensIn, + amountsIn, + slippage, + wrappedNativeAsset + ); + throw new Error('Not implemented'); + } +} diff --git a/balancer-js/src/modules/pools/pool-types/concerns/gyro/liquidity.concern.integration.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/gyro/liquidity.concern.integration.spec.ts new file mode 100644 index 000000000..b74ea53b5 --- /dev/null +++ b/balancer-js/src/modules/pools/pool-types/concerns/gyro/liquidity.concern.integration.spec.ts @@ -0,0 +1,141 @@ +// yarn test:only ./src/modules/pools/pool-types/concerns/gyro/liquidity.concern.integration.spec.ts +import dotenv from 'dotenv'; +import { Network, PoolWithMethods } from '@/types'; +import { forkSetup, TestPoolHelper } from '@/test/lib/utils'; +import { ethers } from 'hardhat'; +import { BalancerSDK } from '@/modules/sdk.module'; +import { expect } from 'chai'; +import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { SolidityMaths } from '@/lib/utils/solidityMaths'; + +dotenv.config(); + +const network = Network.POLYGON; +const { ALCHEMY_URL_POLYGON: rpcUrlArchive } = process.env; +const rpcUrlLocal = 'http://127.0.0.1:8137'; + +const provider = new ethers.providers.JsonRpcProvider(rpcUrlLocal, network); +const signer = provider.getSigner(); +const blockNumber = 43015527; + +describe('Gyro Pools - Calculate Liquidity', () => { + const sdkConfig = { + network, + rpcUrl: rpcUrlLocal, + }; + const balancer = new BalancerSDK(sdkConfig); + context('GyroE Pools', () => { + const testPoolId = + '0x97469e6236bd467cd147065f77752b00efadce8a0002000000000000000008c0'; + let pool: PoolWithMethods; + before(async () => { + const testPool = new TestPoolHelper( + testPoolId, + network, + rpcUrlLocal, + blockNumber + ); + // Gets initial pool info from Subgraph + pool = await testPool.getPool(); + + // Setup forked network, set initial token balances and allowances + await forkSetup(signer, [], [], [], rpcUrlArchive as string, undefined); + + // Update pool info with onchain state from fork block no + pool = await testPool.getPool(); + }); + it('calculating liquidity', async () => { + const liquidity = await balancer.pools.liquidity(pool); + const liquidityFromContract = parseFixed( + parseFloat(pool.totalLiquidity).toFixed(18).toString(), + 18 + ).toBigInt(); + const liquidityBigInt = parseFixed(liquidity, 18).toBigInt(); + // expecting 5% of margin error + expect( + parseFloat( + formatFixed( + SolidityMaths.divDownFixed(liquidityBigInt, liquidityFromContract), + 18 + ).toString() + ) + ).to.be.closeTo(1, 0.05); + }); + }); + context('Gyro V2 Pools', () => { + const testPoolId = + '0xdac42eeb17758daa38caf9a3540c808247527ae3000200000000000000000a2b'; + let pool: PoolWithMethods; + before(async () => { + const testPool = new TestPoolHelper( + testPoolId, + network, + rpcUrlLocal, + blockNumber + ); + // Gets initial pool info from Subgraph + pool = await testPool.getPool(); + + // Setup forked network, set initial token balances and allowances + await forkSetup(signer, [], [], [], rpcUrlArchive as string, undefined); + + // Update pool info with onchain state from fork block no + pool = await testPool.getPool(); + }); + it('calculating liquidity', async () => { + const liquidity = await balancer.pools.liquidity(pool); + const liquidityFromContract = parseFixed( + parseFloat(pool.totalLiquidity).toFixed(18).toString(), + 18 + ).toBigInt(); + const liquidityBigInt = parseFixed(liquidity, 18).toBigInt(); + // expecting 5% of margin error + expect( + parseFloat( + formatFixed( + SolidityMaths.divDownFixed(liquidityBigInt, liquidityFromContract), + 18 + ).toString() + ) + ).to.be.closeTo(1, 0.05); + }); + }); + context('Gyro V3 Pools', () => { + const testPoolId = + '0x17f1ef81707811ea15d9ee7c741179bbe2a63887000100000000000000000799'; + let pool: PoolWithMethods; + before(async () => { + const testPool = new TestPoolHelper( + testPoolId, + network, + rpcUrlLocal, + blockNumber + ); + // Gets initial pool info from Subgraph + pool = await testPool.getPool(); + + // Setup forked network, set initial token balances and allowances + await forkSetup(signer, [], [], [], rpcUrlArchive as string, undefined); + + // Update pool info with onchain state from fork block no + pool = await testPool.getPool(); + }); + it('calculating liquidity', async () => { + const liquidity = await balancer.pools.liquidity(pool); + const liquidityFromContract = parseFixed( + parseFloat(pool.totalLiquidity).toFixed(18).toString(), + 18 + ).toBigInt(); + const liquidityBigInt = parseFixed(liquidity, 18).toBigInt(); + // expecting 5% of margin error + expect( + parseFloat( + formatFixed( + SolidityMaths.divDownFixed(liquidityBigInt, liquidityFromContract), + 18 + ).toString() + ) + ).to.be.closeTo(1, 0.05); + }); + }); +}); diff --git a/balancer-js/src/modules/pools/pool-types/concerns/gyro/liquidity.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/gyro/liquidity.concern.ts new file mode 100644 index 000000000..782b07d42 --- /dev/null +++ b/balancer-js/src/modules/pools/pool-types/concerns/gyro/liquidity.concern.ts @@ -0,0 +1,54 @@ +import { LiquidityConcern } from '../types'; +import { PoolToken } from '@/types'; +import { formatFixed } from '@ethersproject/bignumber'; +import { parseFixed } from '@/lib/utils/math'; +import { SolidityMaths } from '@/lib/utils/solidityMaths'; + +const SCALING_FACTOR = 18; + +export class GyroLiquidityConcern implements LiquidityConcern { + calcTotal(tokens: PoolToken[]): string { + let sumBalance = BigInt(0); + let sumValue = BigInt(0); + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + + // if a token's price is unknown, ignore it + // it will be computed at the next step + if (!token.price?.usd) { + continue; + } + + const price = parseFixed( + token.price.usd.toString(), + SCALING_FACTOR + ).toBigInt(); + const balance = parseFixed(token.balance, SCALING_FACTOR).toBigInt(); + + const value = SolidityMaths.mulDownFixed(balance, price); + sumValue = SolidityMaths.add(sumValue, value); + sumBalance = SolidityMaths.add(sumBalance, balance); + } + // if at least the partial value of the pool is known + // then compute the rest of the value of tokens with unknown prices + if (sumBalance > BigInt(0)) { + const avgPrice = SolidityMaths.divDownFixed(sumValue, sumBalance); + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + + if (token.price?.usd) { + continue; + } + + const balance = parseFixed(token.balance, SCALING_FACTOR).toBigInt(); + + const value = SolidityMaths.mulDownFixed(balance, avgPrice); + sumValue = SolidityMaths.add(sumValue, value); + sumBalance = SolidityMaths.add(sumBalance, balance); + } + } + return formatFixed(sumValue.toString(), SCALING_FACTOR).toString(); + } +} diff --git a/balancer-js/src/modules/pools/pool-types/concerns/gyro/priceImpact.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/gyro/priceImpact.concern.ts new file mode 100644 index 000000000..cf892b581 --- /dev/null +++ b/balancer-js/src/modules/pools/pool-types/concerns/gyro/priceImpact.concern.ts @@ -0,0 +1,19 @@ +import { PriceImpactConcern } from '@/modules/pools/pool-types/concerns/types'; +import { Pool } from '@/types'; + +export class GyroPriceImpactConcern implements PriceImpactConcern { + bptZeroPriceImpact(pool: Pool, tokenAmounts: bigint[]): bigint { + console.log(pool, tokenAmounts); + throw new Error('Not implemented'); + } + + calcPriceImpact( + pool: Pool, + tokenAmounts: bigint[], + bptAmount: bigint, + isJoin: boolean + ): string { + console.log(pool, tokenAmounts, bptAmount, isJoin); + throw new Error('Not implemented'); + } +} diff --git a/balancer-js/src/modules/pools/pool-types/concerns/gyro/spotPrice.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/gyro/spotPrice.concern.ts new file mode 100644 index 000000000..e8c2724a5 --- /dev/null +++ b/balancer-js/src/modules/pools/pool-types/concerns/gyro/spotPrice.concern.ts @@ -0,0 +1,9 @@ +import { SpotPriceConcern } from '@/modules/pools/pool-types/concerns/types'; +import { Pool } from '@/types'; + +export class GyroSpotPriceConcern implements SpotPriceConcern { + calcPoolSpotPrice(tokenIn: string, tokenOut: string, pool: Pool): string { + console.log(tokenIn, tokenOut, pool); + throw new Error('Not implemented'); + } +} diff --git a/balancer-js/src/modules/pools/pool-types/concerns/linear/exit.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/linear/exit.concern.ts index fab93215f..6b0946a6c 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/linear/exit.concern.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/linear/exit.concern.ts @@ -144,7 +144,7 @@ export class LinearPoolExit implements ExitConcern { } // Check if there's any relevant stable pool info missing - if (pool.tokens.some((token) => !token.decimals)) + if (pool.tokens.some((token) => token.decimals === undefined)) throw new BalancerError(BalancerErrorCode.MISSING_DECIMALS); }; diff --git a/balancer-js/src/modules/pools/pool-types/concerns/stable/exit.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/stable/exit.concern.ts index bfb54ca3a..7681b0eda 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/stable/exit.concern.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/stable/exit.concern.ts @@ -278,7 +278,7 @@ export class StablePoolExit implements ExitConcern { ); // Check if there's any relevant stable pool info missing - if (pool.tokens.some((token) => !token.decimals)) + if (pool.tokens.some((token) => token.decimals === undefined)) throw new BalancerError(BalancerErrorCode.MISSING_DECIMALS); if (!pool.amp) throw new BalancerError(BalancerErrorCode.MISSING_AMP); }; @@ -300,7 +300,7 @@ export class StablePoolExit implements ExitConcern { throw new BalancerError(BalancerErrorCode.INPUT_LENGTH_MISMATCH); } // Check if there's any relevant stable pool info missing - if (pool.tokens.some((token) => !token.decimals)) + if (pool.tokens.some((token) => token.decimals === undefined)) throw new BalancerError(BalancerErrorCode.MISSING_DECIMALS); if (!pool.amp) throw new BalancerError(BalancerErrorCode.MISSING_AMP); }; diff --git a/balancer-js/src/modules/pools/pool-types/concerns/stable/join.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/stable/join.concern.ts index fa930b2ad..1d7848ce7 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/stable/join.concern.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/stable/join.concern.ts @@ -98,7 +98,7 @@ export class StablePoolJoin implements JoinConcern { } // Check if there's any relevant stable pool info missing - if (pool.tokens.some((token) => !token.decimals)) + if (pool.tokens.some((token) => token.decimals === undefined)) throw new BalancerError(BalancerErrorCode.MISSING_DECIMALS); if (!pool.amp) throw new BalancerError(BalancerErrorCode.MISSING_AMP); }; diff --git a/balancer-js/src/modules/pools/pool-types/concerns/weighted/exit.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/weighted/exit.concern.ts index 62cae623e..8d76b66c2 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/weighted/exit.concern.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/weighted/exit.concern.ts @@ -278,7 +278,7 @@ export class WeightedPoolExit implements ExitConcern { ); // Check if there's any relevant weighted pool info missing - if (pool.tokens.some((token) => !token.decimals)) + if (pool.tokens.some((token) => token.decimals === undefined)) throw new BalancerError(BalancerErrorCode.MISSING_DECIMALS); }; /** @@ -299,7 +299,7 @@ export class WeightedPoolExit implements ExitConcern { throw new BalancerError(BalancerErrorCode.INPUT_LENGTH_MISMATCH); } // Check if there's any important weighted pool info missing - if (pool.tokens.some((token) => !token.decimals)) + if (pool.tokens.some((token) => token.decimals === undefined)) throw new BalancerError(BalancerErrorCode.MISSING_DECIMALS); }; sortValuesExitExactBptIn = ({ diff --git a/balancer-js/src/modules/pools/pool-types/concerns/weighted/join.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/weighted/join.concern.ts index a521f2778..0c9346789 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/weighted/join.concern.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/weighted/join.concern.ts @@ -91,7 +91,7 @@ export class WeightedPoolJoin implements JoinConcern { } // Check if there's any relevant weighted pool info missing - if (pool.tokens.some((token) => !token.decimals)) + if (pool.tokens.some((token) => token.decimals === undefined)) throw new BalancerError(BalancerErrorCode.MISSING_DECIMALS); if (pool.tokens.some((token) => !token.weight)) throw new BalancerError(BalancerErrorCode.MISSING_WEIGHT); diff --git a/balancer-js/src/modules/pools/pool-types/gyro.module.ts b/balancer-js/src/modules/pools/pool-types/gyro.module.ts new file mode 100644 index 000000000..0ac1a4ec0 --- /dev/null +++ b/balancer-js/src/modules/pools/pool-types/gyro.module.ts @@ -0,0 +1,23 @@ +import { PoolType } from './pool-type.interface'; +import { + ExitConcern, + JoinConcern, + LiquidityConcern, + PriceImpactConcern, + SpotPriceConcern, +} from '@/modules/pools/pool-types/concerns/types'; +import { GyroExitConcern } from '@/modules/pools/pool-types/concerns/gyro/exit.concern'; +import { GyroLiquidityConcern } from '@/modules/pools/pool-types/concerns/gyro/liquidity.concern'; +import { GyroSpotPriceConcern } from '@/modules/pools/pool-types/concerns/gyro/spotPrice.concern'; +import { GyroPriceImpactConcern } from '@/modules/pools/pool-types/concerns/gyro/priceImpact.concern'; +import { GyroJoinConcern } from '@/modules/pools/pool-types/concerns/gyro/join.concern'; + +export class Gyro implements PoolType { + constructor( + public exit: ExitConcern = new GyroExitConcern(), + public liquidity: LiquidityConcern = new GyroLiquidityConcern(), + public spotPriceCalculator: SpotPriceConcern = new GyroSpotPriceConcern(), + public priceImpactCalculator: PriceImpactConcern = new GyroPriceImpactConcern(), + public join: JoinConcern = new GyroJoinConcern() + ) {} +} diff --git a/balancer-js/src/modules/pools/pools.integration.spec.ts b/balancer-js/src/modules/pools/pools.integration.spec.ts index 8820f7d5a..cc89c7c56 100644 --- a/balancer-js/src/modules/pools/pools.integration.spec.ts +++ b/balancer-js/src/modules/pools/pools.integration.spec.ts @@ -1,5 +1,13 @@ import { expect } from 'chai'; -import { BalancerSDK, Network, Pool, PoolWithMethods, Pools } from '@/.'; +import { + BalancerSDK, + Network, + Pool, + PoolWithMethods, + Pools, + GraphQLQuery, + GraphQLArgs, +} from '@/.'; import { AddressZero, Zero } from '@ethersproject/constants'; import { bn } from '@/lib/utils'; import { poolFactory } from '@/test/factories/sdk'; @@ -7,12 +15,27 @@ import { BALANCER_NETWORK_CONFIG } from '@/lib/constants/config'; const rpcUrl = 'http://127.0.0.1:8545'; const network = Network.MAINNET; -const sdk = new BalancerSDK({ network, rpcUrl }); -const { pools, contracts } = sdk; -const { balancerHelpers } = contracts; - const ethStEth = '0x32296969ef14eb0c6d29669c550d4a0449130230000200000000000000000080'; +const subgraphArgs: GraphQLArgs = { + where: { + swapEnabled: { + eq: true, + }, + totalShares: { + gt: 0.000000000001, + }, + address: { + in: [ethStEth], + }, + }, + orderBy: 'totalLiquidity', + orderDirection: 'desc', +}; +const subgraphQuery: GraphQLQuery = { args: subgraphArgs, attrs: {} }; +const sdk = new BalancerSDK({ network, rpcUrl, subgraphQuery }); +const { pools, contracts } = sdk; +const { balancerHelpers } = contracts; describe('pools module', () => { describe('methods', () => { diff --git a/balancer-js/src/modules/relayer/relayer.module.ts b/balancer-js/src/modules/relayer/relayer.module.ts index c41a15118..8550adaf8 100644 --- a/balancer-js/src/modules/relayer/relayer.module.ts +++ b/balancer-js/src/modules/relayer/relayer.module.ts @@ -7,6 +7,10 @@ import { EncodeBatchSwapInput, EncodeExitPoolInput, EncodeJoinPoolInput, + EncodeUnwrapAaveStaticTokenInput, + EncodeUnwrapInput, + EncodeUnwrapWstETHInput, + EncodeWrapAaveDynamicTokenInput, ExitPoolData, JoinPoolData, } from './types'; @@ -118,6 +122,105 @@ export class Relayer { ]); } + static encodeWrapAaveDynamicToken( + params: EncodeWrapAaveDynamicTokenInput + ): string { + return relayerLibrary.encodeFunctionData('wrapAaveDynamicToken', [ + params.staticToken, + params.sender, + params.recipient, + params.amount, + params.fromUnderlying, + params.outputReference, + ]); + } + + static encodeUnwrapAaveStaticToken( + params: EncodeUnwrapAaveStaticTokenInput + ): string { + return relayerLibrary.encodeFunctionData('unwrapAaveStaticToken', [ + params.staticToken, + params.sender, + params.recipient, + params.amount, + params.toUnderlying, + params.outputReference, + ]); + } + + static encodeUnwrapWstETH(params: EncodeUnwrapWstETHInput): string { + return relayerLibrary.encodeFunctionData('unwrapWstETH', [ + params.sender, + params.recipient, + params.amount, + params.outputReference, + ]); + } + + static encodeUnwrap( + params: EncodeUnwrapInput, + linearPoolType: string + ): string { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let unwrapType: any; + + /** + * Other unwrap types available on BatchRelayerLibrary that does not seem to + * have a respective Linear pool type in the SDK: + * - unwrapUnbuttonToken + * - unwrapWstETH + */ + + switch (linearPoolType) { + case 'AaveLinear': + return this.encodeUnwrapAaveStaticToken({ + staticToken: params.wrappedToken, + sender: params.sender, + recipient: params.recipient, + amount: params.amount, + toUnderlying: true, + outputReference: params.outputReference, + }); + case 'BeefyLinear': + case 'ERC4626Linear': + unwrapType = 'unwrapERC4626'; + break; + case 'EulerLinear': + unwrapType = 'unwrapEuler'; + break; + case 'GearboxLinear': + unwrapType = 'unwrapGearbox'; + break; + case 'ReaperLinear': + unwrapType = 'unwrapReaperVaultToken'; + break; + case 'TetuLinear': + unwrapType = 'unwrapTetu'; + break; + case 'YearnLinear': + unwrapType = 'unwrapYearn'; + break; + case 'MidasLinear': + unwrapType = 'unwrapCompoundV2'; + break; + case 'SiloLinear': + unwrapType = 'unwrapShareToken'; + break; + default: + throw new Error( + 'Unwrapping not supported for this pool type: ' + linearPoolType + ); + } + + return relayerLibrary.encodeFunctionData(unwrapType, [ + params.wrappedToken, + params.sender, + params.recipient, + params.amount, + params.outputReference, + ]); + } + static encodePeekChainedReferenceValue(reference: BigNumberish): string { return relayerLibrary.encodeFunctionData('peekChainedReferenceValue', [ reference, diff --git a/balancer-js/src/modules/relayer/types.ts b/balancer-js/src/modules/relayer/types.ts index 48613b6d7..9dc57c46a 100644 --- a/balancer-js/src/modules/relayer/types.ts +++ b/balancer-js/src/modules/relayer/types.ts @@ -45,5 +45,38 @@ export interface EncodeJoinPoolInput { outputReference: string; } +export interface EncodeWrapAaveDynamicTokenInput { + staticToken: string; + sender: string; + recipient: string; + amount: BigNumberish; + fromUnderlying: boolean; + outputReference: BigNumberish; +} + +export interface EncodeUnwrapAaveStaticTokenInput { + staticToken: string; + sender: string; + recipient: string; + amount: BigNumberish; + toUnderlying: boolean; + outputReference: BigNumberish; +} + +export interface EncodeUnwrapInput { + wrappedToken: string; + sender: string; + recipient: string; + amount: BigNumberish; + outputReference: BigNumberish; +} + +export interface EncodeUnwrapWstETHInput { + sender: string; + recipient: string; + amount: BigNumberish; + outputReference: BigNumberish; +} + export type ExitPoolData = ExitPoolRequest & EncodeExitPoolInput; export type JoinPoolData = JoinPoolRequest & EncodeJoinPoolInput; diff --git a/balancer-js/src/modules/simulation/simulation.module.ts b/balancer-js/src/modules/simulation/simulation.module.ts index 016a21db9..dd1d639d5 100644 --- a/balancer-js/src/modules/simulation/simulation.module.ts +++ b/balancer-js/src/modules/simulation/simulation.module.ts @@ -118,11 +118,9 @@ export class Simulation { break; } case SimulationType.Static: { - const gasLimit = 8e6; const staticResult = await signer.call({ to, data: encodedCall, - gasLimit, }); amountsOut.push(...this.decodeResult(staticResult, outputIndexes)); break; diff --git a/balancer-js/src/modules/sor/pool-data/onChainData.ts b/balancer-js/src/modules/sor/pool-data/onChainData.ts index 94bcea1da..23018ac4d 100644 --- a/balancer-js/src/modules/sor/pool-data/onChainData.ts +++ b/balancer-js/src/modules/sor/pool-data/onChainData.ts @@ -14,6 +14,7 @@ import { StablePool__factory, StaticATokenRateProvider__factory, WeightedPool__factory, + GyroEV2__factory, } from '@/contracts'; import { JsonFragment } from '@ethersproject/abi'; @@ -41,6 +42,7 @@ export async function getOnChainBalances< ...(ConvergentCurvePool__factory.abi as readonly JsonFragment[]), ...(LinearPool__factory.abi as readonly JsonFragment[]), ...(ComposableStablePool__factory.abi as readonly JsonFragment[]), + ...(GyroEV2__factory.abi as readonly JsonFragment[]), ].map((row) => [row.name, row]) ) ); @@ -139,6 +141,9 @@ export async function getOnChainBalances< 'getSwapFeePercentage' ); } + if (pool.poolType.toString() === 'GyroE' && pool.poolTypeVersion === 2) { + multiPool.call(`${pool.id}.tokenRates`, pool.address, 'getTokenRates'); + } }); let pools = {} as Record< @@ -156,6 +161,7 @@ export async function getOnChainBalances< virtualSupply?: string; rate?: string; actualSupply?: string; + tokenRates?: string[]; } >; @@ -174,6 +180,7 @@ export async function getOnChainBalances< virtualSupply?: string; rate?: string; actualSupply?: string; + tokenRates?: string[]; } >; } catch (err) { @@ -191,6 +198,7 @@ export async function getOnChainBalances< totalSupply, virtualSupply, actualSupply, + tokenRates, } = onchainData; if ( @@ -274,6 +282,21 @@ export async function getOnChainBalances< subgraphPools[index].totalShares = formatFixed(totalSupply, 18); } + if ( + subgraphPools[index].poolType === 'GyroE' && + subgraphPools[index].poolTypeVersion == 2 + ) { + if (!Array.isArray(tokenRates) || tokenRates.length !== 2) { + console.error( + `GyroEV2 pool with missing or invalid tokenRates: ${poolId}` + ); + return; + } + subgraphPools[index].tokenRates = tokenRates.map((rate) => + formatFixed(rate, 18) + ); + } + onChainPools.push(subgraphPools[index]); } catch (err) { throw new Error(`Issue with pool onchain data: ${err}`); diff --git a/balancer-js/src/modules/vaultModel/poolModel/poolModel.ts b/balancer-js/src/modules/vaultModel/poolModel/poolModel.ts index 764e49b65..df9d0cd5d 100644 --- a/balancer-js/src/modules/vaultModel/poolModel/poolModel.ts +++ b/balancer-js/src/modules/vaultModel/poolModel/poolModel.ts @@ -3,16 +3,19 @@ import { RelayerModel } from '../relayer'; import { JoinModel, JoinPoolRequest } from './join'; import { ExitModel, ExitPoolRequest } from './exit'; import { SwapModel, BatchSwapRequest, SwapRequest } from './swap'; +import { UnwrapModel, UnwrapRequest } from './unwrap'; export class PoolModel { joinModel: JoinModel; exitModel: ExitModel; swapModel: SwapModel; + unwrapModel: UnwrapModel; constructor(private relayerModel: RelayerModel) { this.joinModel = new JoinModel(relayerModel); this.exitModel = new ExitModel(relayerModel); this.swapModel = new SwapModel(relayerModel); + this.unwrapModel = new UnwrapModel(relayerModel); } async doJoin( @@ -42,4 +45,11 @@ export class PoolModel { ): Promise { return this.swapModel.doSingleSwap(swapRequest, pools); } + + async doUnwrap( + unwrapRequest: UnwrapRequest, + pools: PoolDictionary + ): Promise<[string[], string[]]> { + return this.unwrapModel.doUnwrap(unwrapRequest, pools); + } } diff --git a/balancer-js/src/modules/vaultModel/poolModel/unwrap.ts b/balancer-js/src/modules/vaultModel/poolModel/unwrap.ts new file mode 100644 index 000000000..e13dfb93f --- /dev/null +++ b/balancer-js/src/modules/vaultModel/poolModel/unwrap.ts @@ -0,0 +1,58 @@ +import { LinearPool } from '@balancer-labs/sor'; +import { parseFixed } from '@ethersproject/bignumber'; + +import { EncodeUnwrapAaveStaticTokenInput } from '@/modules/relayer/types'; + +import { PoolDictionary } from '../poolSource'; +import { RelayerModel } from '../relayer'; +import { ActionType } from '../vaultModel.module'; +import { WeiPerEther, Zero } from '@ethersproject/constants'; +import { SolidityMaths } from '@/lib/utils/solidityMaths'; + +export interface UnwrapRequest + extends Pick { + poolId: string; + actionType: ActionType.Unwrap; +} + +export class UnwrapModel { + constructor(private relayerModel: RelayerModel) {} + + /** + * Perform the specified unwrap type. + * @param unwrapRequest + * @param pools + * @returns tokens out and their respective deltas + */ + async doUnwrap( + unwrapRequest: UnwrapRequest, + pools: PoolDictionary + ): Promise<[string[], string[]]> { + const pool = pools[unwrapRequest.poolId] as LinearPool; + const wrappedToken = pool.tokens[pool.wrappedIndex]; + const underlyingToken = pool.tokens[pool.mainIndex]; + + const amountIn = this.relayerModel.doChainedRefReplacement( + unwrapRequest.amount.toString() + ); + + // must be negative because is leaving the vault + const amountOut = SolidityMaths.divDownFixed( + SolidityMaths.mulDownFixed( + BigInt(amountIn), + parseFixed(wrappedToken.priceRate, 18).toBigInt() + ), + WeiPerEther.toBigInt() + ).toString(); + + // Save chained references + this.relayerModel.setChainedReferenceValue( + unwrapRequest.outputReference.toString(), + amountOut + ); + + const tokens = [wrappedToken.address, underlyingToken.address]; + const deltas = [amountIn, Zero.sub(amountOut).toString()]; + return [tokens, deltas]; + } +} diff --git a/balancer-js/src/modules/vaultModel/vaultModel.module.ts b/balancer-js/src/modules/vaultModel/vaultModel.module.ts index 1deefc81b..9903ed1b5 100644 --- a/balancer-js/src/modules/vaultModel/vaultModel.module.ts +++ b/balancer-js/src/modules/vaultModel/vaultModel.module.ts @@ -1,4 +1,4 @@ -import { BigNumber } from '@ethersproject/bignumber'; +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; import { Zero } from '@ethersproject/constants'; import { PoolDataService } from '@balancer-labs/sor'; @@ -6,6 +6,7 @@ import { PoolModel } from './poolModel/poolModel'; import { JoinPoolRequest } from './poolModel/join'; import { ExitPoolRequest } from './poolModel/exit'; import { BatchSwapRequest, SwapRequest } from './poolModel/swap'; +import { UnwrapRequest } from './poolModel/unwrap'; import { RelayerModel } from './relayer'; import { PoolsSource } from './poolSource'; import { @@ -20,13 +21,15 @@ export enum ActionType { Join, Exit, Swap, + Unwrap, } export type Requests = | BatchSwapRequest | JoinPoolRequest | ExitPoolRequest - | SwapRequest; + | SwapRequest + | UnwrapRequest; /** * Controller / use-case layer for interacting with pools data. @@ -59,24 +62,35 @@ export class VaultModel { const pools = await this.poolsSource.poolsDictionary(refresh); const deltas: Record = {}; for (const call of rawCalls) { - if (call.actionType === ActionType.Join) { - const [tokens, amounts] = await poolModel.doJoin(call, pools); - // const [tokens, amounts] = await this.doJoinPool(call); - this.updateDeltas(deltas, tokens, amounts); - } else if (call.actionType === ActionType.Exit) { - const [tokens, amounts] = await poolModel.doExit(call, pools); - this.updateDeltas(deltas, tokens, amounts); - } else if (call.actionType === ActionType.BatchSwap) { - const swapDeltas = await poolModel.doBatchSwap(call, pools); - this.updateDeltas(deltas, call.assets, swapDeltas); - } else { - const swapDeltas = await poolModel.doSingleSwap(call, pools); - this.updateDeltas( - deltas, - [call.request.assetOut, call.request.assetIn], - swapDeltas - ); + let tokens: string[] = []; + let amounts: string[] = []; + switch (call.actionType) { + case ActionType.Join: { + [tokens, amounts] = await poolModel.doJoin(call, pools); + break; + } + case ActionType.Exit: { + [tokens, amounts] = await poolModel.doExit(call, pools); + break; + } + case ActionType.BatchSwap: { + tokens = call.assets; + amounts = await poolModel.doBatchSwap(call, pools); + break; + } + case ActionType.Swap: { + tokens = [call.request.assetOut, call.request.assetIn]; + amounts = await poolModel.doSingleSwap(call, pools); + break; + } + case ActionType.Unwrap: { + [tokens, amounts] = await poolModel.doUnwrap(call, pools); + break; + } + default: + break; } + this.updateDeltas(deltas, tokens, amounts); } return deltas; } @@ -122,4 +136,18 @@ export class VaultModel { }; return exitPoolRequest; } + + static mapUnwrapRequest( + amount: BigNumberish, + outputReference: BigNumberish, + poolId: string + ): UnwrapRequest { + const unwrapRequest: UnwrapRequest = { + actionType: ActionType.Unwrap, + poolId, + amount, + outputReference, + }; + return unwrapRequest; + } } diff --git a/balancer-js/src/test/fixtures/pools-mainnet.json b/balancer-js/src/test/fixtures/pools-mainnet.json index 89fa0dc5d..4c5f636ac 100644 --- a/balancer-js/src/test/fixtures/pools-mainnet.json +++ b/balancer-js/src/test/fixtures/pools-mainnet.json @@ -8058,7 +8058,107 @@ "strategyType": 1, "swapsCount": "723", "holdersCount": "4" + }, + { + "id": "0xec3626fee40ef95e7c0cbb1d495c8b67b34d398300000000000000000000053d", + "name": "Balancer UZD/bb-a-USD stableswap", + "symbol": "B-S-UZD-BB-A-USD", + "address": "0xec3626fee40ef95e7c0cbb1d495c8b67b34d3983", + "poolType": "ComposableStable", + "poolTypeVersion": 4, + "swapFee": "0.0004", + "swapEnabled": true, + "protocolYieldFeeCache": "0.5", + "protocolSwapFeeCache": "0.5", + "amp": "500", + "owner": "0xba1ba1ba1ba1ba1ba1ba1ba1ba1ba1ba1ba1ba1b", + "factory": "0xfada0f4547ab2de89d1304a668c39b3e09aa7c76", + "tokensList": [ + "0xb40b6608b2743e691c9b54ddbdee7bf03cd79f1c", + "0xec3626fee40ef95e7c0cbb1d495c8b67b34d3983", + "0xfebb0bbf162e64fb9d0dfe186e517d84c395f016" + ], + "tokens": [ + { + "id": "0xec3626fee40ef95e7c0cbb1d495c8b67b34d398300000000000000000000053d-0xb40b6608b2743e691c9b54ddbdee7bf03cd79f1c", + "symbol": "UZD", + "name": "UZD Zunami Stable", + "decimals": 18, + "address": "0xb40b6608b2743e691c9b54ddbdee7bf03cd79f1c", + "balance": "5000", + "managedBalance": "0", + "weight": null, + "priceRate": "1", + "token": { + "latestUSDPrice": null, + "pool": null + }, + "isExemptFromYieldProtocolFee": false + }, + { + "id": "0xec3626fee40ef95e7c0cbb1d495c8b67b34d398300000000000000000000053d-0xec3626fee40ef95e7c0cbb1d495c8b67b34d3983", + "symbol": "B-S-UZD-BB-A-USD", + "name": "Balancer UZD/bb-a-USD stableswap", + "decimals": 18, + "address": "0xec3626fee40ef95e7c0cbb1d495c8b67b34d3983", + "balance": "2596148429257413.218796159909145654", + "managedBalance": "0", + "weight": null, + "priceRate": "1", + "token": { + "latestUSDPrice": "0.4999798622425397962243462434197151", + "pool": { + "id": "0xec3626fee40ef95e7c0cbb1d495c8b67b34d398300000000000000000000053d", + "address": "0xec3626fee40ef95e7c0cbb1d495c8b67b34d3983", + "totalShares": "10000.595469088255464394" + } + }, + "isExemptFromYieldProtocolFee": false + }, + { + "id": "0xec3626fee40ef95e7c0cbb1d495c8b67b34d398300000000000000000000053d-0xfebb0bbf162e64fb9d0dfe186e517d84c395f016", + "symbol": "bb-a-USD", + "name": "Balancer Aave v3 Boosted StablePool", + "decimals": 18, + "address": "0xfebb0bbf162e64fb9d0dfe186e517d84c395f016", + "balance": "5000", + "managedBalance": "0", + "weight": null, + "priceRate": "1.000119093824728186", + "token": { + "latestUSDPrice": "1.000034802450382120947007057396189", + "pool": { + "id": "0xfebb0bbf162e64fb9d0dfe186e517d84c395f016000000000000000000000502", + "address": "0xfebb0bbf162e64fb9d0dfe186e517d84c395f016", + "totalShares": "21811046.145758434854302198" + } + }, + "isExemptFromYieldProtocolFee": true + } + ], + "totalLiquidity": "5000.09634497811362010324384540818", + "totalShares": "10000.595469088255464394", + "totalSwapFee": "0", + "totalSwapVolume": "0", + "priceRateProviders": [ + { + "address": "0xfebb0bbf162e64fb9d0dfe186e517d84c395f016", + "token": { + "address": "0xfebb0bbf162e64fb9d0dfe186e517d84c395f016" + } + } + ], + "createTime": 1683442295, + "mainIndex": null, + "wrappedIndex": null, + "totalWeight": "0", + "lowerTarget": null, + "upperTarget": null, + "isInRecoveryMode": null, + "strategyType": 0, + "swapsCount": "0", + "holdersCount": "3" } ] } -} \ No newline at end of file +} diff --git a/balancer-js/src/test/lib/constants.ts b/balancer-js/src/test/lib/constants.ts index 4d9ed1320..9fa4ceb2a 100644 --- a/balancer-js/src/test/lib/constants.ts +++ b/balancer-js/src/test/lib/constants.ts @@ -10,6 +10,7 @@ export const PROVIDER_URLS = { [Network.KOVAN]: `https://kovan.infura.io/v3/${process.env.INFURA}`, [Network.POLYGON]: `https://polygon-mainnet.infura.io/v3/${process.env.INFURA}`, [Network.ARBITRUM]: `https://arbitrum-mainnet.infura.io/v3/${process.env.INFURA}`, + [Network.SEPOLIA]: `https://sepolia.infura.io/v3/${process.env.INFURA}`, }; export const ADDRESSES = { @@ -323,6 +324,20 @@ export const ADDRESSES = { symbol: 'STG', slot: 0, }, + wstEthBoostedApe: { + id: '0x959216bb492b2efa72b15b7aacea5b5c984c3cca000200000000000000000472', + address: '0x959216BB492B2efa72b15B7AAcEa5B5C984c3ccA', + decimals: 18, + symbol: '50wstETH-50stk-APE', + slot: 0, + }, + bbtape: { + id: '0x126e7643235ec0ab9c103c507642dc3f4ca23c66000000000000000000000468', + address: '0x126e7643235ec0ab9c103c507642dC3F4cA23C66', + decimals: 18, + symbol: 'bb-t-stkAPE', + slot: 0, + }, }, [Network.KOVAN]: { // Visit https://balancer-faucet.on.fleek.co/#/faucet for test tokens @@ -545,6 +560,16 @@ export const ADDRESSES = { decimals: 6, symbol: 'XSGD', }, + WMATIC: { + address: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270', + decimals: 18, + symbol: 'WMATIC', + }, + stMATIC: { + address: '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', + decimals: 18, + symbol: 'stMATIC', + }, }, [Network.ARBITRUM]: { WETH: { @@ -625,6 +650,25 @@ export const ADDRESSES = { symbol: 'bb-rf-dai', slot: 52, }, + bbUSD_PLUS: { + id: '0x519cce718fcd11ac09194cff4517f12d263be067000000000000000000000382', + address: '0x519cCe718FCD11AC09194CFf4517F12D263BE067', + decimals: 18, + symbol: 'bbUSD_PLUS', + slot: 0, + }, + bbDAI_PLUS: { + address: '0x117a3d474976274b37b7b94af5dcade5c90c6e85', + decimals: 18, + symbol: 'bbDAI_PLUS', + slot: 52, + }, + bbUSDC_PLUS: { + address: '0x284eb68520c8fa83361c1a3a5910aec7f873c18b', + decimals: 18, + symbol: 'bbUSDC_PLUS', + slot: 52, + }, }, [Network.GNOSIS]: { WETH: { @@ -645,13 +689,25 @@ export const ADDRESSES = { WXDAI: { address: '0xe91d153e0b41518a2ce8dd3d7944fa863463a97d', decimals: 18, - symbol: 'DAI', + symbol: 'WXDAI', + slot: 3, }, USDT: { address: '0x4ECaBa5870353805a9F068101A40E0f32ed605C6', decimals: 6, symbol: 'USDT', }, + WXDAI_MPS: { + id: '0x4bcf6b48906fa0f68bea1fc255869a41241d4851000200000000000000000021', + address: '0x4bcf6b48906fa0f68bea1fc255869a41241d4851', + decimals: 18, + symbol: 'WXDAI_MPS', + }, + MPS: { + address: '0xfa57aa7beed63d03aaf85ffd1753f5f6242588fb', + decimals: 0, + symbol: 'MPS', + }, }, [Network.GOERLI]: { USDC_old: { @@ -855,4 +911,16 @@ export const ADDRESSES = { slot: 0, }, }, + [Network.SEPOLIA]: { + WETH: { + address: '0x7b79995e5f793a07bc00c21412e50ecae098e7f9', + decimals: 18, + symbol: 'WETH', + }, + BAL: { + address: '0xb19382073c7a0addbb56ac6af1808fa49e377b75', + decimals: 18, + symbol: 'BAL', + }, + }, }; diff --git a/balancer-js/src/test/lib/exitHelper.ts b/balancer-js/src/test/lib/exitHelper.ts index 6951a9311..4e30a43fe 100644 --- a/balancer-js/src/test/lib/exitHelper.ts +++ b/balancer-js/src/test/lib/exitHelper.ts @@ -1,11 +1,11 @@ -import { PoolWithMethods } from '@/types'; import { JsonRpcSigner } from '@ethersproject/providers'; import { BigNumber } from '@ethersproject/bignumber'; -import { expect } from 'chai'; import { formatFixed } from '@ethersproject/bignumber'; -import { addSlippage, subSlippage } from '@/lib/utils/slippageHelper'; +import { expect } from 'chai'; + +import { insert, addSlippage, subSlippage } from '@/lib/utils'; import { accuracy, sendTransactionGetBalances } from '@/test/lib/utils'; -import { insert } from '@/lib/utils'; +import { PoolWithMethods } from '@/types'; export const testExactBptIn = async ( pool: PoolWithMethods, diff --git a/balancer-js/src/test/lib/mainnetPools.ts b/balancer-js/src/test/lib/mainnetPools.ts index e12c443a2..34d88cfc9 100644 --- a/balancer-js/src/test/lib/mainnetPools.ts +++ b/balancer-js/src/test/lib/mainnetPools.ts @@ -61,6 +61,25 @@ export const B_50auraBAL_50wstETH = factories.subgraphPoolBase.build({ ], }); +export const wstETH_rETH_sfrxETH = factories.subgraphPoolBase.build({ + id: '0x5aee1e99fe86960377de9f88689616916d5dcabe000000000000000000000467', + address: '0x5aee1e99fe86960377de9f88689616916d5dcabe'.toLowerCase(), + tokens: [ + factories.subgraphToken.transient({ symbol: 'wstETH' }).build(), + factories.subgraphToken.transient({ symbol: 'rETH' }).build(), + factories.subgraphToken.transient({ symbol: 'sfrxETH' }).build(), + ], +}); + +export const UZD_bbausd3 = factories.subgraphPoolBase.build({ + id: '0xec3626fee40ef95e7c0cbb1d495c8b67b34d398300000000000000000000053d', + address: '0xec3626fee40ef95e7c0cbb1d495c8b67b34d3983'.toLowerCase(), + tokens: [ + factories.subgraphToken.transient({ symbol: 'UZD' }).build(), + factories.subgraphToken.transient({ symbol: 'bb-a-USD' }).build(), + ], +}); + export const getForkedPools = async ( provider: JsonRpcProvider, pools: SubgraphPoolBase[] = [B_50WBTC_50WETH] diff --git a/balancer-js/src/test/lib/utils.ts b/balancer-js/src/test/lib/utils.ts index 6dce020ce..dfd17709b 100644 --- a/balancer-js/src/test/lib/utils.ts +++ b/balancer-js/src/test/lib/utils.ts @@ -1,3 +1,5 @@ +import dotenv from 'dotenv'; + import { BigNumber, BigNumberish, formatFixed } from '@ethersproject/bignumber'; import { hexlify, zeroPad } from '@ethersproject/bytes'; import { AddressZero, MaxUint256, WeiPerEther } from '@ethersproject/constants'; @@ -40,11 +42,34 @@ import polygonPools from '../fixtures/pools-polygon.json'; import { PoolsJsonRepository } from './pools-json-repository'; import { Contracts } from '@/modules/contracts/contracts.module'; +dotenv.config(); + +export interface TxResult { + transactionReceipt: TransactionReceipt; + balanceDeltas: BigNumber[]; + internalBalanceDeltas: BigNumber[]; + gasUsed: BigNumber; +} + const jsonPools = { [Network.MAINNET]: mainnetPools, [Network.POLYGON]: polygonPools, }; +export const RPC_URLS: Record = { + [Network.MAINNET]: `http://127.0.0.1:8545`, + [Network.GOERLI]: `http://127.0.0.1:8000`, + [Network.POLYGON]: `http://127.0.0.1:8137`, + [Network.ARBITRUM]: `http://127.0.0.1:8161`, +}; + +export const FORK_NODES: Record = { + [Network.MAINNET]: `${process.env.ALCHEMY_URL}`, + [Network.GOERLI]: `${process.env.ALCHEMY_URL_GOERLI}`, + [Network.POLYGON]: `${process.env.ALCHEMY_URL_POLYGON}`, + [Network.ARBITRUM]: `${process.env.ALCHEMY_URL_ARBITRUM}`, +}; + /** * Setup local fork with approved token balance for a given account * @@ -62,7 +87,7 @@ export const forkSetup = async ( balances: string[], jsonRpcUrl: string, blockNumber?: number, - isVyperMapping = false + isVyperMapping: boolean[] = Array(tokens.length).fill(false) ): Promise => { await signer.provider.send('hardhat_reset', [ { @@ -85,7 +110,7 @@ export const forkSetup = async ( tokens[i], slots[i], balances[i], - isVyperMapping + isVyperMapping[i] ); // Approve appropriate allowances so that vault contract can move tokens @@ -378,12 +403,7 @@ export async function sendTransactionGetBalances( to: string, data: string, value?: BigNumberish -): Promise<{ - transactionReceipt: TransactionReceipt; - balanceDeltas: BigNumber[]; - internalBalanceDeltas: BigNumber[]; - gasUsed: BigNumber; -}> { +): Promise { const balanceBefore = await getBalances( tokensForBalanceCheck, signer, diff --git a/balancer-js/src/types.ts b/balancer-js/src/types.ts index 6d7944df9..a0382c19a 100644 --- a/balancer-js/src/types.ts +++ b/balancer-js/src/types.ts @@ -330,6 +330,7 @@ export interface Pool { lastJoinExitInvariant?: string; isInRecoveryMode?: boolean; isPaused?: boolean; + tokenRates?: string[]; } export interface PriceRateProvider { diff --git a/balancer-js/yarn.lock b/balancer-js/yarn.lock index 823763377..70c7b70d0 100644 --- a/balancer-js/yarn.lock +++ b/balancer-js/yarn.lock @@ -507,10 +507,10 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" -"@balancer-labs/sor@^4.1.1-beta.8": - version "4.1.1-beta.8" - resolved "https://registry.yarnpkg.com/@balancer-labs/sor/-/sor-4.1.1-beta.8.tgz#75b2f40d8083499a1552900c0e2489f6e0a16452" - integrity sha512-By0k3apw/8MLu7UJAsV1xj3kxp9WON8cZI3REFKfOl4xvJq+yB1qR7BHbtR0p7bYZSwUucdiPMevSAlcAXNAsA== +"@balancer-labs/sor@^4.1.1-beta.9": + version "4.1.1-beta.9" + resolved "https://registry.yarnpkg.com/@balancer-labs/sor/-/sor-4.1.1-beta.9.tgz#68ea312f57d43595a0156642b56e7713f87cf4bb" + integrity sha512-jwqEkjOgc9qTnuQGne/ZnG+ynx4ca4qEIgRClJVH2tCCf+pXL7jVPWv/v3I+7dJs2aCyet4uIsXwU/vi2jK+/Q== dependencies: isomorphic-fetch "^2.2.1"