diff --git a/.changeset/red-buttons-check.md b/.changeset/red-buttons-check.md new file mode 100644 index 00000000..db50964f --- /dev/null +++ b/.changeset/red-buttons-check.md @@ -0,0 +1,5 @@ +--- +"@balancer/sdk": major +--- + +new composite router for boosted operations with ability to wrap/unwrap single token diff --git a/examples/addLiquidity/addLiquidityNested.V3.ts b/examples/addLiquidity/addLiquidityNested.V3.ts index cd8cd72f..da5f0814 100644 --- a/examples/addLiquidity/addLiquidityNested.V3.ts +++ b/examples/addLiquidity/addLiquidityNested.V3.ts @@ -24,7 +24,7 @@ import { AddLiquidityNestedInput, TEST_API_ENDPOINT, PERMIT2, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED, } from '../../src'; import { ANVIL_NETWORKS, @@ -133,7 +133,7 @@ const addLiquidityNested = async () => { client, userAccount, amount.token.address, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[chainId], ); } diff --git a/src/abi/balancerCompositeLiquidityRouterBoosted.ts b/src/abi/balancerCompositeLiquidityRouterBoosted.ts new file mode 100644 index 00000000..c3435434 --- /dev/null +++ b/src/abi/balancerCompositeLiquidityRouterBoosted.ts @@ -0,0 +1,499 @@ +export const balancerCompositeLiquidityRouterBoostedAbi = [ + { + inputs: [ + { internalType: 'contract IVault', name: 'vault', type: 'address' }, + { internalType: 'contract IWETH', name: 'weth', type: 'address' }, + { + internalType: 'contract IPermit2', + name: 'permit2', + type: 'address', + }, + { internalType: 'string', name: 'routerVersion', type: 'string' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + inputs: [{ internalType: 'address', name: 'target', type: 'address' }], + name: 'AddressEmptyCode', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'AddressInsufficientBalance', + type: 'error', + }, + { + inputs: [ + { + internalType: 'contract IERC20', + name: 'tokenIn', + type: 'address', + }, + { internalType: 'uint256', name: 'amountIn', type: 'uint256' }, + { internalType: 'uint256', name: 'maxAmountIn', type: 'uint256' }, + ], + name: 'AmountInAboveMax', + type: 'error', + }, + { + inputs: [ + { + internalType: 'contract IERC20', + name: 'tokenOut', + type: 'address', + }, + { internalType: 'uint256', name: 'amountOut', type: 'uint256' }, + { internalType: 'uint256', name: 'minAmountOut', type: 'uint256' }, + ], + name: 'AmountOutBelowMin', + type: 'error', + }, + { + inputs: [ + { + internalType: 'contract IERC4626', + name: 'wrappedToken', + type: 'address', + }, + ], + name: 'BufferNotInitialized', + type: 'error', + }, + { inputs: [], name: 'ErrorSelectorNotFound', type: 'error' }, + { inputs: [], name: 'EthTransfer', type: 'error' }, + { inputs: [], name: 'FailedInnerCall', type: 'error' }, + { inputs: [], name: 'InputLengthMismatch', type: 'error' }, + { inputs: [], name: 'InsufficientEth', type: 'error' }, + { inputs: [], name: 'ReentrancyGuardReentrantCall', type: 'error' }, + { + inputs: [ + { internalType: 'uint8', name: 'bits', type: 'uint8' }, + { internalType: 'uint256', name: 'value', type: 'uint256' }, + ], + name: 'SafeCastOverflowedUintDowncast', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'token', type: 'address' }], + name: 'SafeERC20FailedOperation', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'sender', type: 'address' }], + name: 'SenderIsNotVault', + type: 'error', + }, + { inputs: [], name: 'SwapDeadline', type: 'error' }, + { + inputs: [ + { + internalType: 'address[]', + name: 'expectedTokensOut', + type: 'address[]', + }, + { internalType: 'address[]', name: 'tokensOut', type: 'address[]' }, + ], + name: 'WrongTokensOut', + type: 'error', + }, + { + inputs: [ + { + components: [ + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { internalType: 'address', name: 'pool', type: 'address' }, + { + internalType: 'uint256[]', + name: 'maxAmountsIn', + type: 'uint256[]', + }, + { + internalType: 'uint256', + name: 'minBptAmountOut', + type: 'uint256', + }, + { + internalType: 'enum AddLiquidityKind', + name: 'kind', + type: 'uint8', + }, + { internalType: 'bool', name: 'wethIsEth', type: 'bool' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IRouterCommon.AddLiquidityHookParams', + name: 'params', + type: 'tuple', + }, + { internalType: 'bool[]', name: 'wrapUnderlying', type: 'bool[]' }, + ], + name: 'addLiquidityERC4626PoolProportionalHook', + outputs: [ + { internalType: 'address[]', name: 'tokensIn', type: 'address[]' }, + { internalType: 'uint256[]', name: 'amountsIn', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { internalType: 'address', name: 'pool', type: 'address' }, + { + internalType: 'uint256[]', + name: 'maxAmountsIn', + type: 'uint256[]', + }, + { + internalType: 'uint256', + name: 'minBptAmountOut', + type: 'uint256', + }, + { + internalType: 'enum AddLiquidityKind', + name: 'kind', + type: 'uint8', + }, + { internalType: 'bool', name: 'wethIsEth', type: 'bool' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IRouterCommon.AddLiquidityHookParams', + name: 'params', + type: 'tuple', + }, + { internalType: 'bool[]', name: 'wrapUnderlying', type: 'bool[]' }, + ], + name: 'addLiquidityERC4626PoolUnbalancedHook', + outputs: [ + { internalType: 'uint256', name: 'bptAmountOut', type: 'uint256' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'pool', type: 'address' }, + { internalType: 'bool[]', name: 'wrapUnderlying', type: 'bool[]' }, + { + internalType: 'uint256[]', + name: 'maxAmountsIn', + type: 'uint256[]', + }, + { + internalType: 'uint256', + name: 'exactBptAmountOut', + type: 'uint256', + }, + { internalType: 'bool', name: 'wethIsEth', type: 'bool' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'addLiquidityProportionalToERC4626Pool', + outputs: [ + { internalType: 'address[]', name: 'tokensIn', type: 'address[]' }, + { internalType: 'uint256[]', name: 'amountsIn', type: 'uint256[]' }, + ], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'pool', type: 'address' }, + { internalType: 'bool[]', name: 'wrapUnderlying', type: 'bool[]' }, + { + internalType: 'uint256[]', + name: 'exactAmountsIn', + type: 'uint256[]', + }, + { + internalType: 'uint256', + name: 'minBptAmountOut', + type: 'uint256', + }, + { internalType: 'bool', name: 'wethIsEth', type: 'bool' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'addLiquidityUnbalancedToERC4626Pool', + outputs: [ + { internalType: 'uint256', name: 'bptAmountOut', type: 'uint256' }, + ], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [], + name: 'getPermit2', + outputs: [ + { internalType: 'contract IPermit2', name: '', type: 'address' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getSender', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getWeth', + outputs: [ + { internalType: 'contract IWETH', name: '', type: 'address' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes[]', name: 'data', type: 'bytes[]' }], + name: 'multicall', + outputs: [ + { internalType: 'bytes[]', name: 'results', type: 'bytes[]' }, + ], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'address', name: 'token', type: 'address' }, + { internalType: 'address', name: 'owner', type: 'address' }, + { + internalType: 'address', + name: 'spender', + type: 'address', + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { internalType: 'uint256', name: 'nonce', type: 'uint256' }, + { + internalType: 'uint256', + name: 'deadline', + type: 'uint256', + }, + ], + internalType: 'struct IRouterCommon.PermitApproval[]', + name: 'permitBatch', + type: 'tuple[]', + }, + { + internalType: 'bytes[]', + name: 'permitSignatures', + type: 'bytes[]', + }, + { + components: [ + { + components: [ + { + internalType: 'address', + name: 'token', + type: 'address', + }, + { + internalType: 'uint160', + name: 'amount', + type: 'uint160', + }, + { + internalType: 'uint48', + name: 'expiration', + type: 'uint48', + }, + { + internalType: 'uint48', + name: 'nonce', + type: 'uint48', + }, + ], + internalType: + 'struct IAllowanceTransfer.PermitDetails[]', + name: 'details', + type: 'tuple[]', + }, + { + internalType: 'address', + name: 'spender', + type: 'address', + }, + { + internalType: 'uint256', + name: 'sigDeadline', + type: 'uint256', + }, + ], + internalType: 'struct IAllowanceTransfer.PermitBatch', + name: 'permit2Batch', + type: 'tuple', + }, + { internalType: 'bytes', name: 'permit2Signature', type: 'bytes' }, + { internalType: 'bytes[]', name: 'multicallData', type: 'bytes[]' }, + ], + name: 'permitBatchAndCall', + outputs: [ + { internalType: 'bytes[]', name: 'results', type: 'bytes[]' }, + ], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'pool', type: 'address' }, + { internalType: 'bool[]', name: 'wrapUnderlying', type: 'bool[]' }, + { + internalType: 'uint256', + name: 'exactBptAmountOut', + type: 'uint256', + }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'queryAddLiquidityProportionalToERC4626Pool', + outputs: [ + { internalType: 'address[]', name: 'tokensIn', type: 'address[]' }, + { internalType: 'uint256[]', name: 'amountsIn', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'pool', type: 'address' }, + { internalType: 'bool[]', name: 'wrapUnderlying', type: 'bool[]' }, + { + internalType: 'uint256[]', + name: 'exactAmountsIn', + type: 'uint256[]', + }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'queryAddLiquidityUnbalancedToERC4626Pool', + outputs: [ + { internalType: 'uint256', name: 'bptAmountOut', type: 'uint256' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'pool', type: 'address' }, + { internalType: 'bool[]', name: 'unwrapWrapped', type: 'bool[]' }, + { + internalType: 'uint256', + name: 'exactBptAmountIn', + type: 'uint256', + }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'queryRemoveLiquidityProportionalFromERC4626Pool', + outputs: [ + { internalType: 'address[]', name: 'tokensOut', type: 'address[]' }, + { + internalType: 'uint256[]', + name: 'amountsOut', + type: 'uint256[]', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { internalType: 'address', name: 'pool', type: 'address' }, + { + internalType: 'uint256[]', + name: 'minAmountsOut', + type: 'uint256[]', + }, + { + internalType: 'uint256', + name: 'maxBptAmountIn', + type: 'uint256', + }, + { + internalType: 'enum RemoveLiquidityKind', + name: 'kind', + type: 'uint8', + }, + { internalType: 'bool', name: 'wethIsEth', type: 'bool' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IRouterCommon.RemoveLiquidityHookParams', + name: 'params', + type: 'tuple', + }, + { internalType: 'bool[]', name: 'unwrapWrapped', type: 'bool[]' }, + ], + name: 'removeLiquidityERC4626PoolProportionalHook', + outputs: [ + { internalType: 'address[]', name: 'tokensOut', type: 'address[]' }, + { + internalType: 'uint256[]', + name: 'amountsOut', + type: 'uint256[]', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'pool', type: 'address' }, + { internalType: 'bool[]', name: 'unwrapWrapped', type: 'bool[]' }, + { + internalType: 'uint256', + name: 'exactBptAmountIn', + type: 'uint256', + }, + { + internalType: 'uint256[]', + name: 'minAmountsOut', + type: 'uint256[]', + }, + { internalType: 'bool', name: 'wethIsEth', type: 'bool' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'removeLiquidityProportionalFromERC4626Pool', + outputs: [ + { internalType: 'address[]', name: 'tokensOut', type: 'address[]' }, + { + internalType: 'uint256[]', + name: 'amountsOut', + type: 'uint256[]', + }, + ], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [], + name: 'version', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { stateMutability: 'payable', type: 'receive' }, +] as const; diff --git a/src/abi/balancerCompositeLiquidityRouter.ts b/src/abi/balancerCompositeLiquidityRouterNested.ts similarity index 99% rename from src/abi/balancerCompositeLiquidityRouter.ts rename to src/abi/balancerCompositeLiquidityRouterNested.ts index d057502e..33f1f71f 100644 --- a/src/abi/balancerCompositeLiquidityRouter.ts +++ b/src/abi/balancerCompositeLiquidityRouterNested.ts @@ -1,4 +1,4 @@ -export const balancerCompositeLiquidityRouterAbi = [ +export const balancerCompositeLiquidityRouterNestedAbi = [ { inputs: [ { internalType: 'contract IVault', name: 'vault', type: 'address' }, diff --git a/src/abi/index.ts b/src/abi/index.ts index 653e47d5..9b38f3ab 100644 --- a/src/abi/index.ts +++ b/src/abi/index.ts @@ -17,4 +17,5 @@ export * from './weightedPoolV4.V2'; export * from './weightedPool.V3'; export * from './vaultAdmin.V3'; export * from './stablePoolFactory.V3'; -export * from './balancerCompositeLiquidityRouter'; +export * from './balancerCompositeLiquidityRouterNested'; +export * from './balancerCompositeLiquidityRouterBoosted'; diff --git a/src/entities/addLiquidityBoosted/doAddLiquidityPropotionalQuery.ts b/src/entities/addLiquidityBoosted/doAddLiquidityPropotionalQuery.ts index 353408d1..7aaf8b48 100644 --- a/src/entities/addLiquidityBoosted/doAddLiquidityPropotionalQuery.ts +++ b/src/entities/addLiquidityBoosted/doAddLiquidityPropotionalQuery.ts @@ -1,8 +1,12 @@ import { createPublicClient, Hex, http } from 'viem'; -import { BALANCER_COMPOSITE_LIQUIDITY_ROUTER, ChainId, CHAINS } from '@/utils'; +import { + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED, + ChainId, + CHAINS, +} from '@/utils'; import { Address } from '@/types'; import { - balancerCompositeLiquidityRouterAbi, + balancerCompositeLiquidityRouterBoostedAbi, permit2Abi, vaultExtensionAbi_V3, vaultV3Abi, @@ -15,24 +19,34 @@ export const doAddLiquidityProportionalQuery = async ( userData: Hex, poolAddress: Address, exactBptAmountOut: bigint, + wrapUnderlying: boolean[], block?: bigint, -): Promise => { +): Promise<[Address[], bigint[]]> => { const client = createPublicClient({ transport: http(rpcUrl), chain: CHAINS[chainId], }); - const { result: exactAmountsIn } = await client.simulateContract({ - address: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + const { + result: [tokensIn, exactAmountsIn], + } = await client.simulateContract({ + address: BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[chainId], abi: [ - ...balancerCompositeLiquidityRouterAbi, + ...balancerCompositeLiquidityRouterBoostedAbi, ...vaultV3Abi, ...vaultExtensionAbi_V3, ...permit2Abi, ], functionName: 'queryAddLiquidityProportionalToERC4626Pool', - args: [poolAddress, exactBptAmountOut, sender, userData], + args: [ + poolAddress, + wrapUnderlying, + exactBptAmountOut, + sender, + userData, + ], blockNumber: block, }); - return [...exactAmountsIn]; + + return [[...tokensIn], [...exactAmountsIn]]; }; diff --git a/src/entities/addLiquidityBoosted/doAddLiquidityUnbalancedQuery.ts b/src/entities/addLiquidityBoosted/doAddLiquidityUnbalancedQuery.ts index 2537da24..caf7f730 100644 --- a/src/entities/addLiquidityBoosted/doAddLiquidityUnbalancedQuery.ts +++ b/src/entities/addLiquidityBoosted/doAddLiquidityUnbalancedQuery.ts @@ -1,11 +1,15 @@ import { createPublicClient, Hex, http } from 'viem'; -import { BALANCER_COMPOSITE_LIQUIDITY_ROUTER, ChainId, CHAINS } from '@/utils'; +import { + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED, + ChainId, + CHAINS, +} from '@/utils'; import { Address } from '@/types'; import { - balancerCompositeLiquidityRouterAbi, + balancerCompositeLiquidityRouterBoostedAbi, permit2Abi, vaultExtensionAbi_V3, vaultV3Abi, @@ -17,6 +21,7 @@ export const doAddLiquidityUnbalancedQuery = async ( sender: Address, userData: Hex, poolAddress: Address, + wrapUnderlying: boolean[], exactUnderlyingAmountsIn: bigint[], block?: bigint, ): Promise => { @@ -26,15 +31,21 @@ export const doAddLiquidityUnbalancedQuery = async ( }); const { result: bptAmountOut } = await client.simulateContract({ - address: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + address: BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[chainId], abi: [ - ...balancerCompositeLiquidityRouterAbi, + ...balancerCompositeLiquidityRouterBoostedAbi, ...vaultV3Abi, ...vaultExtensionAbi_V3, ...permit2Abi, ], functionName: 'queryAddLiquidityUnbalancedToERC4626Pool', - args: [poolAddress, exactUnderlyingAmountsIn, sender, userData], + args: [ + poolAddress, + wrapUnderlying, + exactUnderlyingAmountsIn, + sender, + userData, + ], blockNumber: block, }); return bptAmountOut; diff --git a/src/entities/addLiquidityBoosted/index.ts b/src/entities/addLiquidityBoosted/index.ts index cff586b3..9f697293 100644 --- a/src/entities/addLiquidityBoosted/index.ts +++ b/src/entities/addLiquidityBoosted/index.ts @@ -2,41 +2,40 @@ // available: // 1. Unbalanced - addLiquidityUnbalancedToERC4626Pool // 2. Proportional - addLiquidityProportionalToERC4626Pool -import { encodeFunctionData, zeroAddress } from 'viem'; +import { encodeFunctionData, zeroAddress, Address } from 'viem'; import { TokenAmount } from '@/entities/tokenAmount'; - import { Permit2 } from '@/entities/permit2Helper'; - import { getAmountsCall } from '../addLiquidity/helpers'; - import { PoolStateWithUnderlyings } from '@/entities/types'; - import { getAmounts, getBptAmountFromReferenceAmountBoosted, getSortedTokens, getValue, } from '@/entities/utils'; - import { AddLiquidityBuildCallOutput, AddLiquidityKind, } from '../addLiquidity/types'; - import { doAddLiquidityUnbalancedQuery } from './doAddLiquidityUnbalancedQuery'; import { doAddLiquidityProportionalQuery } from './doAddLiquidityPropotionalQuery'; import { Token } from '../token'; -import { BALANCER_COMPOSITE_LIQUIDITY_ROUTER } from '@/utils'; -import { balancerCompositeLiquidityRouterAbi, balancerRouterAbi } from '@/abi'; - -import { InputValidator } from '../inputValidator/inputValidator'; - +import { BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED } from '@/utils'; +import { + balancerCompositeLiquidityRouterBoostedAbi, + balancerRouterAbi, +} from '@/abi'; import { Hex } from '@/types'; import { AddLiquidityBoostedBuildCallInput, AddLiquidityBoostedInput, AddLiquidityBoostedQueryOutput, } from './types'; +import { InputValidator } from '../inputValidator/inputValidator'; +import { + buildPoolStateTokenMap, + MinimalTokenWithIsUnderlyingFlag, +} from '@/entities/utils'; export class AddLiquidityBoostedV3 { private readonly inputValidator: InputValidator = new InputValidator(); @@ -52,31 +51,58 @@ export class AddLiquidityBoostedV3 { }); const bptToken = new Token(input.chainId, poolState.address, 18); + const wrapUnderlying: boolean[] = new Array( + poolState.tokens.length, + ).fill(false); let bptOut: TokenAmount; let amountsIn: TokenAmount[]; - // Child tokens are the lowest most tokens. This will be underlying if it exists. - const childTokens = poolState.tokens.map((t) => { - if (t.underlyingToken) { - return t.underlyingToken; - } - return { - address: t.address, - decimals: t.decimals, - index: t.index, - }; - }); + const poolStateTokenMap = buildPoolStateTokenMap(poolState); switch (input.kind) { case AddLiquidityKind.Unbalanced: { + // Use amountsIn provided by use to infer if token should be wrapped + const tokensIn: MinimalTokenWithIsUnderlyingFlag[] = + input.amountsIn.map((amountIn) => { + const amountInAddress = amountIn.address.toLowerCase(); + const token = poolStateTokenMap[amountInAddress]; + if (!token) { + throw new Error( + `Token not found in poolState: ${amountInAddress}`, + ); + } + return token; + }); + + // if user provides fewer than the number of pool tokens, + // fill remaining indexes because composite router requires + // length of pool tokens to match maxAmountsIn and wrapUnderlying + if (tokensIn.length < poolState.tokens.length) { + const existingIndices = new Set( + tokensIn.map((t) => t.index), + ); + poolState.tokens.forEach((poolToken) => { + if (!existingIndices.has(poolToken.index)) { + tokensIn.push({ + index: poolToken.index, + decimals: poolToken.decimals, + address: poolToken.address, + isUnderlyingToken: false, + }); + } + }); + } + + // wrap if token is underlying + tokensIn.forEach((t) => { + wrapUnderlying[t.index] = t.isUnderlyingToken; + }); + // It is allowed not not provide the same amount of TokenAmounts as inputs // as the pool has tokens, in this case, the input tokens are filled with // a default value ( 0 in this case ) to assure correct amounts in as the pool has tokens. - const sortedTokens = getSortedTokens( - childTokens, - input.chainId, - ); + const sortedTokens = getSortedTokens(tokensIn, input.chainId); const maxAmountsIn = getAmounts(sortedTokens, input.amountsIn); const bptAmountOut = await doAddLiquidityUnbalancedQuery( @@ -85,6 +111,7 @@ export class AddLiquidityBoostedV3 { input.sender ?? zeroAddress, input.userData ?? '0x', poolState.address, + wrapUnderlying, maxAmountsIn, block, ); @@ -93,15 +120,27 @@ export class AddLiquidityBoostedV3 { amountsIn = sortedTokens.map((t, i) => TokenAmount.fromRawAmount(t, maxAmountsIn[i]), ); + break; } case AddLiquidityKind.Proportional: { + // User provides addresses via input.tokensIn so we can infer if they need to be wrapped + input.tokensIn.forEach((t) => { + const tokenIn = + poolStateTokenMap[t.toLowerCase() as Address]; + if (!tokenIn) { + throw new Error(`Invalid token address: ${t}`); + } + wrapUnderlying[tokenIn.index] = tokenIn.isUnderlyingToken; + }); + const bptAmount = await getBptAmountFromReferenceAmountBoosted( input, poolState, + wrapUnderlying, ); - const exactAmountsInNumbers = + const [tokensIn, exactAmountsInNumbers] = await doAddLiquidityProportionalQuery( input.rpcUrl, input.chainId, @@ -109,16 +148,24 @@ export class AddLiquidityBoostedV3 { input.userData ?? '0x', poolState.address, bptAmount.rawAmount, + wrapUnderlying, block, ); - // Amounts are mapped to child tokens of the pool - amountsIn = childTokens.map((t, i) => - TokenAmount.fromRawAmount( - new Token(input.chainId, t.address, t.decimals), + amountsIn = tokensIn.map((t, i) => { + const tokenInAddress = t.toLowerCase() as Address; + const { decimals } = poolStateTokenMap[tokenInAddress]; + const token = new Token( + input.chainId, + tokenInAddress, + decimals, + ); + + return TokenAmount.fromRawAmount( + token, exactAmountsInNumbers[i], - ), - ); + ); + }); bptOut = TokenAmount.fromRawAmount( bptToken, @@ -132,12 +179,13 @@ export class AddLiquidityBoostedV3 { poolId: poolState.id, poolType: poolState.type, addLiquidityKind: input.kind, + wrapUnderlying, bptOut, amountsIn, chainId: input.chainId, protocolVersion: 3, userData: input.userData ?? '0x', - to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[input.chainId], + to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[input.chainId], }; return output; @@ -150,6 +198,7 @@ export class AddLiquidityBoostedV3 { const wethIsEth = input.wethIsEth ?? false; const args = [ input.poolId, + input.wrapUnderlying, amounts.maxAmountsIn, amounts.minimumBpt, wethIsEth, @@ -159,7 +208,7 @@ export class AddLiquidityBoostedV3 { switch (input.addLiquidityKind) { case AddLiquidityKind.Unbalanced: { callData = encodeFunctionData({ - abi: balancerCompositeLiquidityRouterAbi, + abi: balancerCompositeLiquidityRouterBoostedAbi, functionName: 'addLiquidityUnbalancedToERC4626Pool', args, }); @@ -167,7 +216,7 @@ export class AddLiquidityBoostedV3 { } case AddLiquidityKind.Proportional: { callData = encodeFunctionData({ - abi: balancerCompositeLiquidityRouterAbi, + abi: balancerCompositeLiquidityRouterBoostedAbi, functionName: 'addLiquidityProportionalToERC4626Pool', args, }); @@ -182,7 +231,7 @@ export class AddLiquidityBoostedV3 { return { callData, - to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[input.chainId], + to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[input.chainId], value, minBptOut: TokenAmount.fromRawAmount( input.bptOut.token, diff --git a/src/entities/addLiquidityBoosted/types.ts b/src/entities/addLiquidityBoosted/types.ts index d01ae642..03eee442 100644 --- a/src/entities/addLiquidityBoosted/types.ts +++ b/src/entities/addLiquidityBoosted/types.ts @@ -7,6 +7,7 @@ import { Slippage } from '../slippage'; export type AddLiquidityBoostedProportionalInput = { chainId: number; rpcUrl: string; + tokensIn: Address[]; referenceAmount: InputAmount; kind: AddLiquidityKind.Proportional; sender?: Address; @@ -30,6 +31,7 @@ export type AddLiquidityBoostedQueryOutput = { poolId: Hex; poolType: string; addLiquidityKind: AddLiquidityKind; + wrapUnderlying: boolean[]; bptOut: TokenAmount; amountsIn: TokenAmount[]; chainId: number; diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV2/types.ts b/src/entities/addLiquidityNested/addLiquidityNestedV2/types.ts index a3af9225..201cdca9 100644 --- a/src/entities/addLiquidityNested/addLiquidityNestedV2/types.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV2/types.ts @@ -1,15 +1,13 @@ import { Address, Hex } from 'viem'; -import { InputAmount, PoolType } from '../../../types'; +import { PoolType } from '../../../types'; import { ChainId } from '../../../utils'; import { Token } from '../../token'; import { TokenAmount } from '../../tokenAmount'; import { PoolKind } from '../../types'; import { Slippage } from '@/entities/slippage'; +import { AddLiquidityNestedBaseInput } from '../types'; -export type AddLiquidityNestedInputV2 = { - amountsIn: InputAmount[]; - chainId: ChainId; - rpcUrl: string; +export type AddLiquidityNestedInputV2 = AddLiquidityNestedBaseInput & { fromInternalBalance?: boolean; }; diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV3/index.ts b/src/entities/addLiquidityNested/addLiquidityNestedV3/index.ts index cdb13d46..f5c0f5b2 100644 --- a/src/entities/addLiquidityNested/addLiquidityNestedV3/index.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV3/index.ts @@ -14,9 +14,9 @@ import { import { Token } from '@/entities/token'; import { getAmounts, getValue } from '@/entities/utils'; import { TokenAmount } from '@/entities/tokenAmount'; -import { BALANCER_COMPOSITE_LIQUIDITY_ROUTER, CHAINS } from '@/utils'; +import { BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED, CHAINS } from '@/utils'; import { - balancerCompositeLiquidityRouterAbi, + balancerCompositeLiquidityRouterNestedAbi, permit2Abi, vaultExtensionAbi_V3, vaultV3Abi, @@ -70,7 +70,7 @@ export class AddLiquidityNestedV3 { const bptToken = new Token(input.chainId, parentPool.address, 18); return { - to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[input.chainId], + to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[input.chainId], parentPool: parentPool.address, chainId: input.chainId, amountsIn: mainTokens.map((t, i) => @@ -90,7 +90,7 @@ export class AddLiquidityNestedV3 { const minBptOut = input.slippage.applyTo(input.bptOut.amount, -1); const wethIsEth = input.wethIsEth ?? false; const callData = encodeFunctionData({ - abi: balancerCompositeLiquidityRouterAbi, + abi: balancerCompositeLiquidityRouterNestedAbi, functionName: 'addLiquidityUnbalancedNestedPool', args: [ input.parentPool, @@ -103,7 +103,7 @@ export class AddLiquidityNestedV3 { }); return { callData, - to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[input.chainId], + to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[input.chainId], value: getValue(input.amountsIn, wethIsEth), minBptOut, }; @@ -124,7 +124,7 @@ export class AddLiquidityNestedV3 { ] as const; const callData = encodeFunctionData({ - abi: balancerCompositeLiquidityRouterAbi, + abi: balancerCompositeLiquidityRouterNestedAbi, functionName: 'permitBatchAndCall', args, }); @@ -150,9 +150,9 @@ export class AddLiquidityNestedV3 { }); const { result: bptAmountOut } = await client.simulateContract({ - address: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + address: BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[chainId], abi: [ - ...balancerCompositeLiquidityRouterAbi, + ...balancerCompositeLiquidityRouterNestedAbi, ...vaultV3Abi, ...vaultExtensionAbi_V3, ...permit2Abi, diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV3/types.ts b/src/entities/addLiquidityNested/addLiquidityNestedV3/types.ts index 890a9494..5f7ce051 100644 --- a/src/entities/addLiquidityNested/addLiquidityNestedV3/types.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV3/types.ts @@ -1,13 +1,10 @@ import { Slippage } from '@/entities/slippage'; import { TokenAmount } from '@/entities/tokenAmount'; -import { InputAmount } from '@/types'; import { ChainId } from '@/utils'; import { Address, Hex } from 'viem'; +import { AddLiquidityNestedBaseInput } from '../types'; -export type AddLiquidityNestedInputV3 = { - amountsIn: InputAmount[]; - chainId: ChainId; - rpcUrl: string; +export type AddLiquidityNestedInputV3 = AddLiquidityNestedBaseInput & { sender?: Address; userData?: Hex; }; diff --git a/src/entities/addLiquidityNested/types.ts b/src/entities/addLiquidityNested/types.ts index e72baba8..14814a91 100644 --- a/src/entities/addLiquidityNested/types.ts +++ b/src/entities/addLiquidityNested/types.ts @@ -9,6 +9,14 @@ import { AddLiquidityNestedInputV3, AddLiquidityNestedQueryOutputV3, } from './addLiquidityNestedV3/types'; +import { InputAmount } from '@/types'; +import { ChainId } from '@/utils'; + +export type AddLiquidityNestedBaseInput = { + amountsIn: InputAmount[]; + chainId: ChainId; + rpcUrl: string; +}; export type AddLiquidityNestedInput = | AddLiquidityNestedInputV2 diff --git a/src/entities/inputValidator/boosted/inputValidatorBoosted.ts b/src/entities/inputValidator/boosted/inputValidatorBoosted.ts index 81e8b0d8..0b2af9ae 100644 --- a/src/entities/inputValidator/boosted/inputValidatorBoosted.ts +++ b/src/entities/inputValidator/boosted/inputValidatorBoosted.ts @@ -1,7 +1,9 @@ +import { Address } from 'viem'; import { PoolStateWithUnderlyings } from '@/entities/types'; import { InputValidatorBase } from '../inputValidatorBase'; import { AddLiquidityKind } from '@/entities/addLiquidity/types'; import { AddLiquidityBoostedInput } from '@/entities/addLiquidityBoosted/types'; +import { isSameAddress } from '@/utils'; export class InputValidatorBoosted extends InputValidatorBase { validateAddLiquidityBoosted( @@ -14,23 +16,44 @@ export class InputValidatorBoosted extends InputValidatorBase { } if (addLiquidityInput.kind === AddLiquidityKind.Unbalanced) { - // Child tokens are the lower most tokens of the pool, this will be the underlying token if it exists - const childTokens = poolState.tokens.map((t) => { - if (t.underlyingToken) - return t.underlyingToken.address.toLowerCase(); - return t.address.toLowerCase(); - }); + // List of all tokens that can be added to the pool + const poolTokens = poolState.tokens + .flatMap((token) => [ + token.address.toLowerCase(), + token.underlyingToken?.address.toLowerCase(), + ]) + .filter(Boolean); + addLiquidityInput.amountsIn.forEach((a) => { + if (!poolTokens.includes(a.address.toLowerCase() as Address)) { + throw new Error( + `Address ${a.address} is not contained in the pool's parent or child tokens.`, + ); + } + }); + } + + if (addLiquidityInput.kind === AddLiquidityKind.Proportional) { + // if referenceAmount is not the BPT, it must be included in tokensIn + if ( + !isSameAddress( + addLiquidityInput.referenceAmount.address, + poolState.address, + ) + ) { if ( - !childTokens.includes( - a.address.toLowerCase() as `0x${string}`, - ) + addLiquidityInput.tokensIn.findIndex((tokenIn) => + isSameAddress( + tokenIn, + addLiquidityInput.referenceAmount.address, + ), + ) === -1 ) { throw new Error( - `Address ${a.address} is not contained in the pool's child tokens.`, + 'tokensIn must contain referenceAmount token address', ); } - }); + } } } } diff --git a/src/entities/permit2Helper/index.ts b/src/entities/permit2Helper/index.ts index 4bb5d983..89273127 100644 --- a/src/entities/permit2Helper/index.ts +++ b/src/entities/permit2Helper/index.ts @@ -8,7 +8,8 @@ import { import { BALANCER_BATCH_ROUTER, BALANCER_BUFFER_ROUTER, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED, BALANCER_ROUTER, ChainId, PERMIT2, @@ -119,7 +120,8 @@ export class Permit2Helper { input.amountsIn.length, ); const maxAmountsIn = input.amountsIn.map((a) => a.amount); - const spender = BALANCER_COMPOSITE_LIQUIDITY_ROUTER[input.chainId]; + const spender = + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[input.chainId]; const details: PermitDetails[] = []; for (let i = 0; i < input.amountsIn.length; i++) { details.push( @@ -151,7 +153,8 @@ export class Permit2Helper { input.amountsIn.length, ); const amounts = getAmountsCall(input); - const spender = BALANCER_COMPOSITE_LIQUIDITY_ROUTER[input.chainId]; + const spender = + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[input.chainId]; const details: PermitDetails[] = []; for (let i = 0; i < input.amountsIn.length; i++) { diff --git a/src/entities/permitHelper/index.ts b/src/entities/permitHelper/index.ts index 2ade8915..f53421c7 100644 --- a/src/entities/permitHelper/index.ts +++ b/src/entities/permitHelper/index.ts @@ -1,7 +1,8 @@ import { weightedPoolAbi_V3 } from '@/abi'; import { Hex } from '@/types'; import { - BALANCER_COMPOSITE_LIQUIDITY_ROUTER, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED, BALANCER_ROUTER, ChainId, MAX_UINT256, @@ -80,7 +81,7 @@ export class PermitHelper { input.client, input.bptAmountIn.token.address, input.owner, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[input.chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[input.chainId], nonce, input.bptAmountIn.amount, // maxBptIn input.deadline, @@ -108,7 +109,7 @@ export class PermitHelper { input.client, input.bptIn.token.address, input.owner, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[input.chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[input.chainId], nonce, amounts.maxBptAmountIn, input.deadline, diff --git a/src/entities/priceImpact/addLiquidityUnbalancedBoosted.ts b/src/entities/priceImpact/addLiquidityUnbalancedBoosted.ts index 648e34aa..ac9a26e0 100644 --- a/src/entities/priceImpact/addLiquidityUnbalancedBoosted.ts +++ b/src/entities/priceImpact/addLiquidityUnbalancedBoosted.ts @@ -66,6 +66,7 @@ export async function addLiquidityUnbalancedBoosted( chainId: input.chainId, rpcUrl: input.rpcUrl, bptIn: bptOut.toInputAmount(), + tokensOut: poolTokens.map((t) => t.address), kind: RemoveLiquidityKind.Proportional, }; const { amountsOut } = await removeLiquidity.query( diff --git a/src/entities/removeLiquidityBoosted/doRemoveLiquidityProportionalQuery.ts b/src/entities/removeLiquidityBoosted/doRemoveLiquidityProportionalQuery.ts index d91ec316..843ac083 100644 --- a/src/entities/removeLiquidityBoosted/doRemoveLiquidityProportionalQuery.ts +++ b/src/entities/removeLiquidityBoosted/doRemoveLiquidityProportionalQuery.ts @@ -1,7 +1,16 @@ import { createPublicClient, Hex, http } from 'viem'; -import { BALANCER_COMPOSITE_LIQUIDITY_ROUTER, ChainId, CHAINS } from '@/utils'; +import { + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED, + ChainId, + CHAINS, +} from '@/utils'; import { Address } from '@/types'; -import { balancerCompositeLiquidityRouterAbi } from '@/abi'; +import { + balancerCompositeLiquidityRouterBoostedAbi, + permit2Abi, + vaultExtensionAbi_V3, + vaultV3Abi, +} from '@/abi'; export const doRemoveLiquidityProportionalQuery = async ( rpcUrl: string, @@ -10,20 +19,28 @@ export const doRemoveLiquidityProportionalQuery = async ( sender: Address, userData: Hex, poolAddress: Address, + unwrapWrapped: boolean[], block?: bigint, -): Promise => { +): Promise<[Address[], bigint[]]> => { const client = createPublicClient({ transport: http(rpcUrl), chain: CHAINS[chainId], }); - const { result: underlyingAmountsOut } = await client.simulateContract({ - address: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], - abi: balancerCompositeLiquidityRouterAbi, + const { + result: [tokensOut, underlyingAmountsOut], + } = await client.simulateContract({ + address: BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[chainId], + abi: [ + ...balancerCompositeLiquidityRouterBoostedAbi, + ...vaultV3Abi, + ...vaultExtensionAbi_V3, + ...permit2Abi, + ], functionName: 'queryRemoveLiquidityProportionalFromERC4626Pool', - args: [poolAddress, exactBptAmountIn, sender, userData], + args: [poolAddress, unwrapWrapped, exactBptAmountIn, sender, userData], blockNumber: block, }); // underlying amounts (not pool token amounts) - return [...underlyingAmountsOut]; + return [[...tokensOut], [...underlyingAmountsOut]]; }; diff --git a/src/entities/removeLiquidityBoosted/index.ts b/src/entities/removeLiquidityBoosted/index.ts index 13f781cd..3524e519 100644 --- a/src/entities/removeLiquidityBoosted/index.ts +++ b/src/entities/removeLiquidityBoosted/index.ts @@ -1,4 +1,4 @@ -import { encodeFunctionData, zeroAddress } from 'viem'; +import { Address, encodeFunctionData, zeroAddress } from 'viem'; import { RemoveLiquidityBase, @@ -8,7 +8,7 @@ import { import { Permit } from '@/entities/permitHelper'; -import { balancerCompositeLiquidityRouterAbi } from '@/abi'; +import { balancerCompositeLiquidityRouterBoostedAbi } from '@/abi'; import { PoolStateWithUnderlyings } from '@/entities/types'; @@ -18,14 +18,14 @@ import { Token } from '@/entities/token'; import { getAmountsCall } from '../removeLiquidity/helper'; import { doRemoveLiquidityProportionalQuery } from './doRemoveLiquidityProportionalQuery'; -import { BALANCER_COMPOSITE_LIQUIDITY_ROUTER } from '@/utils'; +import { BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED } from '@/utils'; import { RemoveLiquidityBoostedBuildCallInput, RemoveLiquidityBoostedProportionalInput, RemoveLiquidityBoostedQueryOutput, } from './types'; import { InputValidator } from '../inputValidator/inputValidator'; -import { getSortedTokens } from '../utils'; +import { buildPoolStateTokenMap } from '@/entities/utils'; export class RemoveLiquidityBoostedV3 implements RemoveLiquidityBase { private readonly inputValidator: InputValidator = new InputValidator(); @@ -39,46 +39,49 @@ export class RemoveLiquidityBoostedV3 implements RemoveLiquidityBase { ...poolState, type: 'Boosted', }); - const underlyingAmountsOut = await doRemoveLiquidityProportionalQuery( - input.rpcUrl, - input.chainId, - input.bptIn.rawAmount, - input.sender ?? zeroAddress, - input.userData ?? '0x', - poolState.address, - block, - ); - - // Child tokens are the lowest most tokens. This will be underlying if it exists. - const childTokens = poolState.tokens.map((t) => { - if (t.underlyingToken) { - return t.underlyingToken; - } - return { - address: t.address, - decimals: t.decimals, - index: t.index, - }; - }); - - const sortedChildTokens = getSortedTokens(childTokens, input.chainId); - // amountsOut are in child tokens sorted in token registration order of wrapped tokens in the pool - const amountsOut = underlyingAmountsOut.map((amount, i) => { - const token = new Token( + const poolStateTokenMap = buildPoolStateTokenMap(poolState); + + // Infer if token should be unwrapped using the tokensOut provided by user + const unwrapWrapped = input.tokensOut + .map((t) => { + const tokenOut = poolStateTokenMap[t.toLowerCase() as Address]; + if (!tokenOut) { + throw new Error(`Invalid token address: ${t}`); + } + return tokenOut; + }) + .sort((a, b) => a.index - b.index) // sort by index to match the order of the pool tokens + .map((t) => t.isUnderlyingToken); + + const [tokensOut, underlyingAmountsOut] = + await doRemoveLiquidityProportionalQuery( + input.rpcUrl, input.chainId, - sortedChildTokens[i].address, - sortedChildTokens[i].decimals, + input.bptIn.rawAmount, + input.sender ?? zeroAddress, + input.userData ?? '0x', + poolState.address, + unwrapWrapped, + block, ); + + const amountsOut = underlyingAmountsOut.map((amount, i) => { + const tokenOut = tokensOut[i]; + const { decimals } = + poolStateTokenMap[tokenOut.toLowerCase() as Address]; + + const token = new Token(input.chainId, tokenOut, decimals); return TokenAmount.fromRawAmount(token, amount); }); const bptToken = new Token(input.chainId, poolState.address, 18); const output: RemoveLiquidityBoostedQueryOutput = { - to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[input.chainId], + to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[input.chainId], poolType: poolState.type, poolId: poolState.address, + unwrapWrapped, removeLiquidityKind: RemoveLiquidityKind.Proportional, bptIn: TokenAmount.fromRawAmount(bptToken, input.bptIn.rawAmount), amountsOut, @@ -98,10 +101,11 @@ export class RemoveLiquidityBoostedV3 implements RemoveLiquidityBase { const amounts = getAmountsCall(input); const callData = encodeFunctionData({ - abi: balancerCompositeLiquidityRouterAbi, + abi: balancerCompositeLiquidityRouterBoostedAbi, functionName: 'removeLiquidityProportionalFromERC4626Pool', args: [ input.poolId, + input.unwrapWrapped, input.bptIn.amount, amounts.minAmountsOut, input.wethIsEth ?? false, @@ -111,7 +115,7 @@ export class RemoveLiquidityBoostedV3 implements RemoveLiquidityBase { return { callData: callData, - to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[input.chainId], + to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[input.chainId], value: 0n, // always has 0 value maxBptIn: input.bptIn, minAmountsOut: amounts.minAmountsOut.map((amount, i) => { @@ -138,7 +142,7 @@ export class RemoveLiquidityBoostedV3 implements RemoveLiquidityBase { ] as const; const callData = encodeFunctionData({ - abi: balancerCompositeLiquidityRouterAbi, + abi: balancerCompositeLiquidityRouterBoostedAbi, functionName: 'permitBatchAndCall', args, }); diff --git a/src/entities/removeLiquidityBoosted/types.ts b/src/entities/removeLiquidityBoosted/types.ts index 3a4108ff..4fcfbf9c 100644 --- a/src/entities/removeLiquidityBoosted/types.ts +++ b/src/entities/removeLiquidityBoosted/types.ts @@ -8,6 +8,7 @@ export type RemoveLiquidityBoostedProportionalInput = { chainId: number; rpcUrl: string; bptIn: InputAmount; + tokensOut: Address[]; kind: RemoveLiquidityKind.Proportional; sender?: Address; userData?: Hex; @@ -17,6 +18,7 @@ export type RemoveLiquidityBoostedQueryOutput = { poolType: string; poolId: Address; removeLiquidityKind: RemoveLiquidityKind.Proportional; + unwrapWrapped: boolean[]; bptIn: TokenAmount; amountsOut: TokenAmount[]; protocolVersion: 3; @@ -32,6 +34,7 @@ export type RemoveLiquidityBoostedBuildCallInput = { removeLiquidityKind: RemoveLiquidityKind.Proportional; bptIn: TokenAmount; amountsOut: TokenAmount[]; + unwrapWrapped: boolean[]; protocolVersion: 3; chainId: number; slippage: Slippage; diff --git a/src/entities/removeLiquidityNested/index.ts b/src/entities/removeLiquidityNested/index.ts index 0f006869..bd0c504b 100644 --- a/src/entities/removeLiquidityNested/index.ts +++ b/src/entities/removeLiquidityNested/index.ts @@ -10,7 +10,7 @@ import { import { RemoveLiquidityNestedV3 } from './removeLiquidityNestedV3'; import { validateBuildCallInput } from './removeLiquidityNestedV2/validateInputs'; import { Address, encodeFunctionData, Hex, zeroAddress } from 'viem'; -import { balancerCompositeLiquidityRouterAbi } from '@/abi'; +import { balancerCompositeLiquidityRouterNestedAbi } from '@/abi'; export class RemoveLiquidityNested { async query( @@ -66,7 +66,7 @@ export class RemoveLiquidityNested { [buildCallOutput.callData], ] as const; const callData = encodeFunctionData({ - abi: balancerCompositeLiquidityRouterAbi, + abi: balancerCompositeLiquidityRouterNestedAbi, functionName: 'permitBatchAndCall', args, }); diff --git a/src/entities/removeLiquidityNested/removeLiquidityNestedV3/index.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV3/index.ts index 65eb714c..b28d51e6 100644 --- a/src/entities/removeLiquidityNested/removeLiquidityNestedV3/index.ts +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV3/index.ts @@ -13,9 +13,9 @@ import { RemoveLiquidityNestedQueryOutputV3, } from './types'; import { RemoveLiquidityNestedBuildCallOutput } from '../types'; -import { BALANCER_COMPOSITE_LIQUIDITY_ROUTER, CHAINS } from '@/utils'; +import { BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED, CHAINS } from '@/utils'; import { - balancerCompositeLiquidityRouterAbi, + balancerCompositeLiquidityRouterNestedAbi, permit2Abi, vaultExtensionAbi_V3, vaultV3Abi, @@ -53,7 +53,7 @@ export class RemoveLiquidityNestedV3 { ); return { - to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[input.chainId], + to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[input.chainId], protocolVersion: 3, bptAmountIn: TokenAmount.fromRawAmount(bptToken, input.bptAmountIn), chainId: input.chainId, @@ -79,7 +79,7 @@ export class RemoveLiquidityNestedV3 { ); const callData = encodeFunctionData({ - abi: balancerCompositeLiquidityRouterAbi, + abi: balancerCompositeLiquidityRouterNestedAbi, functionName: 'removeLiquidityProportionalNestedPool', args: [ input.parentPool, @@ -92,7 +92,7 @@ export class RemoveLiquidityNestedV3 { }); return { callData, - to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[input.chainId], + to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[input.chainId], minAmountsOut, } as RemoveLiquidityNestedBuildCallOutput; } @@ -112,9 +112,9 @@ export class RemoveLiquidityNestedV3 { }); const { result: amountsOut } = await client.simulateContract({ - address: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + address: BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[chainId], abi: [ - ...balancerCompositeLiquidityRouterAbi, + ...balancerCompositeLiquidityRouterNestedAbi, ...vaultV3Abi, ...vaultExtensionAbi_V3, ...permit2Abi, diff --git a/src/entities/utils/boostedPoolHelpers.ts b/src/entities/utils/boostedPoolHelpers.ts new file mode 100644 index 00000000..02bb8e10 --- /dev/null +++ b/src/entities/utils/boostedPoolHelpers.ts @@ -0,0 +1,65 @@ +import { MinimalToken } from '@/data/types'; +import { PoolStateWithUnderlyings } from '@/entities/types'; +import { Address } from 'viem'; + +export type MinimalTokenWithIsUnderlyingFlag = MinimalToken & { + isUnderlyingToken: boolean; +}; + +/** + * Builds map of pool tokens (including underlying) + * with address as key and useful token details as values. + * + * Example output: + * { + * "0x8a88124522dbbf1e56352ba3de1d9f78c143751e": { + * index: 0, + * decimals: 6, + * address: "0x8a88124522dbbf1e56352ba3de1d9f78c143751e", + * isUnderlyingToken: false + * }, + * "0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8": { + * index: 0, + * decimals: 6, + * address: "0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8", + * isUnderlyingToken: true + * }, + * "0x978206fae13faf5a8d293fb614326b237684b750": { + * index: 1, + * decimals: 6, + * address: "0x978206fae13faf5a8d293fb614326b237684b750", + * isUnderlyingToken: false + * }, + * "0xaa8e23fb1079ea71e0a56f48a2aa51851d8433d0": { + * index: 1, + * decimals: 6, + * address: "0xaa8e23fb1079ea71e0a56f48a2aa51851d8433d0", + * isUnderlyingToken: true + * } + * } + */ +export function buildPoolStateTokenMap( + poolState: PoolStateWithUnderlyings, +): Record { + const map: Record = {}; + + poolState.tokens.forEach((t) => { + const underlyingToken = t.underlyingToken; + map[t.address.toLowerCase()] = { + index: t.index, + decimals: t.decimals, + address: t.address, + isUnderlyingToken: false, + }; + if (underlyingToken) { + map[underlyingToken.address.toLowerCase()] = { + index: underlyingToken.index, + decimals: underlyingToken.decimals, + address: underlyingToken.address, + isUnderlyingToken: true, + }; + } + }); + + return map; +} diff --git a/src/entities/utils/index.ts b/src/entities/utils/index.ts index d0f55b85..97692eab 100644 --- a/src/entities/utils/index.ts +++ b/src/entities/utils/index.ts @@ -10,3 +10,4 @@ export * from './parseInitializeArgs'; export * from './proportionalAmountsHelpers'; export * from './replaceWrapped'; export * from './validateNestedPoolState'; +export * from './boostedPoolHelpers'; diff --git a/src/entities/utils/proportionalAmountsHelpers.ts b/src/entities/utils/proportionalAmountsHelpers.ts index d39b1f3b..16ebb1fd 100644 --- a/src/entities/utils/proportionalAmountsHelpers.ts +++ b/src/entities/utils/proportionalAmountsHelpers.ts @@ -3,7 +3,11 @@ import { InputAmount } from '@/types'; import { HumanAmount } from '@/data'; import { isSameAddress, MathSol } from '@/utils'; import { AddLiquidityProportionalInput } from '../addLiquidity/types'; -import { PoolState, PoolStateWithUnderlyingBalances, PoolStateWithUnderlyings } from '../types'; +import { + PoolState, + PoolStateWithUnderlyingBalances, + PoolStateWithUnderlyings, +} from '../types'; import { getPoolStateWithBalancesV2 } from './getPoolStateWithBalancesV2'; import { getBoostedPoolStateWithBalancesV3, @@ -142,6 +146,7 @@ export const getBptAmountFromReferenceAmount = async ( export const getBptAmountFromReferenceAmountBoosted = async ( input: AddLiquidityBoostedProportionalInput, poolStateWithUnderlyings: PoolStateWithUnderlyings, + wrapUnderlying: boolean[], ): Promise => { let bptAmount: InputAmount; if ( @@ -162,6 +167,7 @@ export const getBptAmountFromReferenceAmountBoosted = async ( ({ bptAmount } = calculateProportionalAmountsBoosted( poolStateWithUnderlyingBalances, input.referenceAmount, + wrapUnderlying, )); } return bptAmount; @@ -170,13 +176,22 @@ export const getBptAmountFromReferenceAmountBoosted = async ( export const calculateProportionalAmountsBoosted = ( poolStateWithUnderlyingBalances: PoolStateWithUnderlyingBalances, referenceAmount: InputAmount, + wrapUnderlying: boolean[], ): { tokenAmounts: InputAmount[]; bptAmount: InputAmount } => { const poolStateWithBalances = { ...poolStateWithUnderlyingBalances, - tokens: poolStateWithUnderlyingBalances.tokens.map( - (t) => t.underlyingToken ?? t, - ), + tokens: poolStateWithUnderlyingBalances.tokens.map((t, i) => { + if (wrapUnderlying[i]) { + if (!t.underlyingToken) { + throw new Error( + 'Underlying token not found for wrapped token', + ); + } + return t.underlyingToken; + } + return t; + }), }; return calculateProportionalAmounts(poolStateWithBalances, referenceAmount); -} \ No newline at end of file +}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index cf85926e..deb0bedd 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -254,7 +254,10 @@ export const BALANCER_BATCH_ROUTER: Record = { [ChainId.BASE]: '0x85a80afee867aDf27B50BdB7b76DA70f1E853062', }; -export const BALANCER_COMPOSITE_LIQUIDITY_ROUTER: Record = { +export const BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED: Record< + number, + Address +> = { [ChainId.SEPOLIA]: '0xc6674C0c7694E9b990eAc939E74F8cc3DD39B4b0', [ChainId.MAINNET]: '0x1CD776897ef4f647bf8241Ec69549e4A9cb1D608', [ChainId.GNOSIS_CHAIN]: '0xC1A64500E035D9159C8826E982dFb802003227f0', @@ -263,6 +266,13 @@ export const BALANCER_COMPOSITE_LIQUIDITY_ROUTER: Record = { [ChainId.BASE]: '0xf23b4DB826DbA14c0e857029dfF076b1c0264843', }; +export const BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED: Record< + number, + Address +> = { + [ChainId.SEPOLIA]: '0x6A20a4b6DcFF78e6D21BF0dbFfD58C96644DB9cb', +}; + export const BALANCER_BUFFER_ROUTER: Record = { [ChainId.SEPOLIA]: '0xb5F3A41515457CC6E2716c62a011D260441CcfC9', [ChainId.MAINNET]: '0x9179C06629ef7f17Cb5759F501D89997FE0E7b45', diff --git a/test/anvil/anvil-global-setup.ts b/test/anvil/anvil-global-setup.ts index ac701821..49e2a95f 100644 --- a/test/anvil/anvil-global-setup.ts +++ b/test/anvil/anvil-global-setup.ts @@ -66,7 +66,7 @@ export const ANVIL_NETWORKS: Record = { rpcEnv: 'SEPOLIA_RPC_URL', fallBackRpc: 'https://sepolia.gateway.tenderly.co', port: ANVIL_PORTS.SEPOLIA, - forkBlockNumber: 7245070n, + forkBlockNumber: 7562540n, }, OPTIMISM: { rpcEnv: 'OPTIMISM_RPC_URL', diff --git a/test/lib/utils/addLiquidityBoostedHelper.ts b/test/lib/utils/addLiquidityBoostedHelper.ts index a03b8338..4f5e0bf6 100644 --- a/test/lib/utils/addLiquidityBoostedHelper.ts +++ b/test/lib/utils/addLiquidityBoostedHelper.ts @@ -11,7 +11,7 @@ import { TokenAmount, } from '@/entities'; import { - BALANCER_COMPOSITE_LIQUIDITY_ROUTER, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED, ChainId, NATIVE_ASSETS, PERMIT2, @@ -69,7 +69,7 @@ export async function GetBoostedBpt( client, testAddress, amount.address, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[chainId], ); } @@ -114,7 +114,7 @@ export async function GetBoostedBpt( client, testAddress, amount.address, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[chainId], 0n, ); } @@ -232,7 +232,7 @@ export const assertAddLiquidityBoostedUnbalanced = ( expect(protocolVersion).toEqual(3); expect(bptOut.amount > 0n).to.be.true; - expect(to).to.eq(BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId]); + expect(to).to.eq(BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[chainId]); expect(transactionReceipt.status).to.eq('success'); // add one extra index for native token balance @@ -279,7 +279,7 @@ export const assertAddLiquidityBoostedProportional = ( expect(protocolVersion).toEqual(3); expect(bptOut.amount > 0n).to.be.true; - expect(to).to.eq(BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId]); + expect(to).to.eq(BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[chainId]); expect(transactionReceipt.status).to.eq('success'); // add one extra index for native token balance diff --git a/test/lib/utils/helper.ts b/test/lib/utils/helper.ts index 48542295..64941c5a 100644 --- a/test/lib/utils/helper.ts +++ b/test/lib/utils/helper.ts @@ -25,7 +25,7 @@ import { PERMIT2, BALANCER_ROUTER, BALANCER_BATCH_ROUTER, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED, PublicWalletClient, BALANCER_BUFFER_ROUTER, } from '@/utils'; @@ -129,7 +129,7 @@ export const approveToken = async ( client, accountAddress, tokenAddress, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[chainId], amount, deadline, ); @@ -331,9 +331,9 @@ export async function sendTransactionGetBalances( // TODO - Leave this in as useful as basis for manual debug // await client.simulateContract({ // address: - // BALANCER_COMPOSITE_LIQUIDITY_ROUTER[client.chain?.id as number], + // BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[client.chain?.id as number], // abi: [ - // ...balancerCompositeLiquidityRouterAbi, + // ...balancerCompositeLiquidityRouterNestedAbi, // ...vaultV3Abi, // ...vaultExtensionAbi_V3, // ...permit2Abi, diff --git a/test/lib/utils/removeLiquidityNestedHelper.ts b/test/lib/utils/removeLiquidityNestedHelper.ts index e4c6149d..d02b94f8 100644 --- a/test/lib/utils/removeLiquidityNestedHelper.ts +++ b/test/lib/utils/removeLiquidityNestedHelper.ts @@ -14,7 +14,7 @@ import { TokenAmount, } from '@/entities'; import { - BALANCER_COMPOSITE_LIQUIDITY_ROUTER, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED, ChainId, NATIVE_ASSETS, PERMIT2, @@ -71,7 +71,7 @@ export async function GetNestedBpt( client, testAddress, amount.address, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[chainId], ); } @@ -116,7 +116,7 @@ export async function GetNestedBpt( client, testAddress, amount.address, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[chainId], 0n, ); } @@ -280,7 +280,7 @@ export const assertRemoveLiquidityNested = ( expect(expectedMinAmountsOut).to.deep.eq( minAmountsOut.map((a) => a.amount), ); - expect(to).to.eq(BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId]); + expect(to).to.eq(BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[chainId]); expect(transactionReceipt.status).to.eq('success'); diff --git a/test/lib/utils/types.ts b/test/lib/utils/types.ts index d924b136..68dd0a68 100644 --- a/test/lib/utils/types.ts +++ b/test/lib/utils/types.ts @@ -13,11 +13,12 @@ import { RemoveLiquidity, Slippage, RemoveLiquidityRecoveryInput, + AddLiquidityBoostedV3, } from '@/.'; export type AddLiquidityTxInput = { client: PublicWalletClient & TestActions; - addLiquidity: AddLiquidity; + addLiquidity: AddLiquidity | AddLiquidityBoostedV3; addLiquidityInput: AddLiquidityInput; slippage: Slippage; poolState: PoolState; diff --git a/test/v3/addLiquidityBoosted/addLiquidityBoosted.integration.test.ts b/test/v3/addLiquidityBoosted/addLiquidityBoosted.integration.test.ts index c57eae2f..aca306a1 100644 --- a/test/v3/addLiquidityBoosted/addLiquidityBoosted.integration.test.ts +++ b/test/v3/addLiquidityBoosted/addLiquidityBoosted.integration.test.ts @@ -6,6 +6,7 @@ config(); import { Address, createTestClient, + erc4626Abi, http, parseUnits, publicActions, @@ -26,24 +27,40 @@ import { PublicWalletClient, AddLiquidityBoostedBuildCallInput, AddLiquidityBoostedInput, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED, + AddLiquidityBoostedUnbalancedInput, + AddLiquidityBoostedProportionalInput, + TokenAmount, } from '../../../src'; import { setTokenBalances, approveSpenderOnTokens, - approveTokens, + approveSpenderOnPermit2, areBigIntsWithinPercent, TOKENS, assertTokenMatch, sendTransactionGetBalances, + doAddLiquidityBoosted, + assertAddLiquidityBoostedUnbalanced, + assertAddLiquidityBoostedProportional, + AddLiquidityBoostedTxInput, } from '../../lib/utils'; import { ANVIL_NETWORKS, startFork } from '../../anvil/anvil-global-setup'; import { boostedPool_USDC_USDT } from 'test/mockData/boostedPool'; -const protocolVersion = 3; - const chainId = ChainId.SEPOLIA; const USDC = TOKENS[chainId].USDC_AAVE; const USDT = TOKENS[chainId].USDT_AAVE; +const stataUSDT = TOKENS[chainId].stataUSDT; + +// These are the underlying tokens +const usdtToken = new Token(chainId, USDT.address, USDT.decimals); +const usdcToken = new Token(chainId, USDC.address, USDC.decimals); +const stataUsdtToken = new Token( + chainId, + stataUSDT.address, + stataUSDT.decimals, +); describe('Boosted AddLiquidity', () => { let client: PublicWalletClient & TestActions; @@ -52,6 +69,28 @@ describe('Boosted AddLiquidity', () => { let testAddress: Address; const addLiquidityBoosted = new AddLiquidityBoostedV3(); + // for unbalanced inputs + const amountsInForSingleWrap = [ + TokenAmount.fromHumanAmount(usdcToken, '1'), + TokenAmount.fromHumanAmount(stataUsdtToken, '2'), + ].map((a) => ({ + address: a.token.address, + rawAmount: a.amount, + decimals: a.token.decimals, + })); + const amountsInForDoubleWrap = [ + TokenAmount.fromHumanAmount(usdcToken, '1'), + TokenAmount.fromHumanAmount(usdtToken, '1'), + ].map((a) => ({ + address: a.token.address, + rawAmount: a.amount, + decimals: a.token.decimals, + })); + + // for proportional inputs + const tokensInForSingleWrap = [USDC.address, stataUSDT.address]; + const tokensInForDoubleWrap = [USDC.address, USDT.address]; + beforeAll(async () => { ({ rpcUrl } = await startFork(ANVIL_NETWORKS[ChainId[chainId]])); @@ -68,20 +107,35 @@ describe('Boosted AddLiquidity', () => { await setTokenBalances( client, testAddress, - [USDT.address, USDC.address] as Address[], + [USDT.address, USDC.address], [USDT.slot, USDC.slot] as number[], [ - parseUnits('100', USDT.decimals), - parseUnits('100', USDC.decimals), + parseUnits('1000', USDT.decimals), + parseUnits('1000', USDC.decimals), ], ); - // approve Permit2 to spend users DAI/USDC - // does not include the sub approvals + // set erc4626 token balance await approveSpenderOnTokens( client, testAddress, - [USDT.address, USDC.address] as Address[], + [USDT.address], + stataUSDT.address, + ); + await client.writeContract({ + account: testAddress, + chain: CHAINS[chainId], + abi: erc4626Abi, + address: stataUSDT.address, + functionName: 'deposit', + args: [parseUnits('500', USDT.decimals), testAddress], + }); + + // approve Permit2 to spend users DAI/USDC, does not include the sub approvals + await approveSpenderOnTokens( + client, + testAddress, + [USDT.address, USDC.address, stataUSDT.address], PERMIT2[chainId], ); @@ -95,251 +149,323 @@ describe('Boosted AddLiquidity', () => { snapshot = await client.snapshot(); }); - describe('permit 2 direct approval', () => { - beforeEach(async () => { - // Here We approve the Vault to spend Tokens on the users behalf via Permit2 - await approveTokens( - client, - testAddress as Address, - [USDT.address, USDC.address] as Address[], - protocolVersion, - ); - }); - describe('add liquidity unbalanced', () => { - test('with tokens', async () => { - const input: AddLiquidityBoostedInput = { + describe('query', () => { + test('unbalanced returns correct tokens', async () => { + const addLiquidityBoostedInput: AddLiquidityBoostedUnbalancedInput = + { chainId, rpcUrl, - amountsIn: [ - { - rawAmount: 1000000n, - decimals: 6, - address: USDC.address as Address, - }, - { - rawAmount: 1000000n, - decimals: 6, - address: USDT.address as Address, - }, - ], + amountsIn: amountsInForSingleWrap, kind: AddLiquidityKind.Unbalanced, - userData: '0x', }; - const addLiquidityQueryOutput = await addLiquidityBoosted.query( - input, - boostedPool_USDC_USDT, - ); + const addLiquidityQueryOutput = await addLiquidityBoosted.query( + addLiquidityBoostedInput, + boostedPool_USDC_USDT, + ); - const addLiquidityBuildInput: AddLiquidityBoostedBuildCallInput = - { - ...addLiquidityQueryOutput, - slippage: Slippage.fromPercentage('1'), - }; + const amountsIn = addLiquidityQueryOutput.amountsIn; - const addLiquidityBuildCallOutput = - await addLiquidityBoosted.buildCall(addLiquidityBuildInput); + expect(amountsIn[0].token.address).to.eq(usdcToken.address); + expect(amountsIn[1].token.address).to.eq(stataUSDT.address); + }); - const { transactionReceipt, balanceDeltas } = - await sendTransactionGetBalances( - [ - addLiquidityQueryOutput.bptOut.token.address, - USDC.address as `0x${string}`, - USDT.address as `0x${string}`, - ], - client, - testAddress, - addLiquidityBuildCallOutput.to, - addLiquidityBuildCallOutput.callData, - ); + test('proportional returns correct tokens', async () => { + const referenceAmount = { + rawAmount: 481201n, + decimals: 6, + address: USDC.address, + }; - expect(transactionReceipt.status).to.eq('success'); + const addLiquidityBoostedInput: AddLiquidityBoostedProportionalInput = + { + chainId, + rpcUrl, + referenceAmount, + tokensIn: tokensInForSingleWrap, + kind: AddLiquidityKind.Proportional, + }; - expect(addLiquidityQueryOutput.bptOut.amount > 0n).to.be.true; + const addLiquidityQueryOutput = await addLiquidityBoosted.query( + addLiquidityBoostedInput, + boostedPool_USDC_USDT, + ); - areBigIntsWithinPercent( - addLiquidityQueryOutput.bptOut.amount, - balanceDeltas[0], - 0.001, - ); + const amountsIn = addLiquidityQueryOutput.amountsIn; - const slippageAdjustedQueryOutput = Slippage.fromPercentage( - '1', - ).applyTo(addLiquidityQueryOutput.bptOut.amount, -1); + expect(amountsIn[0].token.address).to.eq(usdcToken.address); + expect(amountsIn[1].token.address).to.eq(stataUSDT.address); + }); + }); - expect( - slippageAdjustedQueryOutput === - addLiquidityBuildCallOutput.minBptOut.amount, - ).to.be.true; - }); + describe('permit 2 direct approval', () => { + beforeEach(async () => { + // Here we approve the Vault to spend tokens on the users behalf via Permit2 + for (const token of boostedPool_USDC_USDT.tokens) { + await approveSpenderOnPermit2( + client, + testAddress, + token.address, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[chainId], + ); + + if (token.underlyingToken) { + await approveSpenderOnPermit2( + client, + testAddress, + token.underlyingToken.address, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[chainId], + ); + } + } }); - describe('add liquidity proportional', () => { - test('with bpt', async () => { - const addLiquidityProportionalInput: AddLiquidityBoostedInput = + describe('unbalanced', () => { + test('with only one token', async () => { + const wethIsEth = false; + const addLiquidityBoostedInput: AddLiquidityBoostedUnbalancedInput = { chainId, rpcUrl, - referenceAmount: { - rawAmount: 1000000000000000000n, - decimals: 18, - address: boostedPool_USDC_USDT.address, - }, - kind: AddLiquidityKind.Proportional, + amountsIn: [amountsInForDoubleWrap[0]], + kind: AddLiquidityKind.Unbalanced, }; - const addLiquidityQueryOutput = await addLiquidityBoosted.query( - addLiquidityProportionalInput, - boostedPool_USDC_USDT, + + const txInput: AddLiquidityBoostedTxInput = { + client, + addLiquidityBoosted, + addLiquidityBoostedInput, + testAddress, + poolStateWithUnderlyings: boostedPool_USDC_USDT, + slippage: Slippage.fromPercentage('1'), + wethIsEth, + }; + + const { + addLiquidityBoostedQueryOutput, + addLiquidityBuildCallOutput, + tokenAmountsForBalanceCheck, + txOutput, + } = await doAddLiquidityBoosted(txInput); + + assertAddLiquidityBoostedUnbalanced( + { + addLiquidityBoostedQueryOutput, + addLiquidityBuildCallOutput, + tokenAmountsForBalanceCheck, + txOutput, + }, + wethIsEth, ); - const addLiquidityBuildInput: AddLiquidityBoostedBuildCallInput = + }); + + test('with both underlying tokens wrapped', async () => { + const wethIsEth = false; + const addLiquidityBoostedInput: AddLiquidityBoostedUnbalancedInput = { - ...addLiquidityQueryOutput, - slippage: Slippage.fromPercentage('1'), + chainId, + rpcUrl, + amountsIn: amountsInForDoubleWrap, + kind: AddLiquidityKind.Unbalanced, }; - const addLiquidityBuildCallOutput = - addLiquidityBoosted.buildCall(addLiquidityBuildInput); + const txInput: AddLiquidityBoostedTxInput = { + client, + addLiquidityBoosted, + addLiquidityBoostedInput, + testAddress, + poolStateWithUnderlyings: boostedPool_USDC_USDT, + slippage: Slippage.fromPercentage('1'), + wethIsEth, + }; - const { transactionReceipt, balanceDeltas } = - await sendTransactionGetBalances( - [ - addLiquidityQueryOutput.bptOut.token.address, - USDC.address as `0x${string}`, - USDT.address as `0x${string}`, - ], - client, - testAddress, - addLiquidityBuildCallOutput.to, // - addLiquidityBuildCallOutput.callData, - ); + const { + addLiquidityBoostedQueryOutput, + addLiquidityBuildCallOutput, + tokenAmountsForBalanceCheck, + txOutput, + } = await doAddLiquidityBoosted(txInput); - expect(transactionReceipt.status).to.eq('success'); + assertAddLiquidityBoostedUnbalanced( + { + addLiquidityBoostedQueryOutput, + addLiquidityBuildCallOutput, + tokenAmountsForBalanceCheck, + txOutput, + }, + wethIsEth, + ); + }); - addLiquidityQueryOutput.amountsIn.map((a) => { - expect(a.amount > 0n).to.be.true; - }); + test('with only one underlying token wrapped', async () => { + const wethIsEth = false; + const addLiquidityBoostedInput: AddLiquidityBoostedUnbalancedInput = + { + chainId, + rpcUrl, + amountsIn: amountsInForSingleWrap, + kind: AddLiquidityKind.Unbalanced, + }; - const expectedDeltas = [ - addLiquidityProportionalInput.referenceAmount.rawAmount, - ...addLiquidityQueryOutput.amountsIn.map( - (tokenAmount) => tokenAmount.amount, - ), - ]; - expect(balanceDeltas).to.deep.eq(expectedDeltas); + const txInput: AddLiquidityBoostedTxInput = { + client, + addLiquidityBoosted, + addLiquidityBoostedInput, + testAddress, + poolStateWithUnderlyings: boostedPool_USDC_USDT, + slippage: Slippage.fromPercentage('1'), + wethIsEth, + }; - const slippageAdjustedQueryInput = - addLiquidityQueryOutput.amountsIn.map((amountsIn) => { - return Slippage.fromPercentage('1').applyTo( - amountsIn.amount, - 1, - ); - }); - expect( - addLiquidityBuildCallOutput.maxAmountsIn.map( - (a) => a.amount, - ), - ).to.deep.eq(slippageAdjustedQueryInput); + const { + addLiquidityBoostedQueryOutput, + addLiquidityBuildCallOutput, + tokenAmountsForBalanceCheck, + txOutput, + } = await doAddLiquidityBoosted(txInput); - // make sure to pass Tokens in correct order. Same as poolTokens but as underlyings instead - assertTokenMatch( - [ - new Token( - 111555111, - USDC.address as Address, - USDC.decimals, - ), - new Token( - 111555111, - USDT.address as Address, - USDT.decimals, - ), - ], - addLiquidityBuildCallOutput.maxAmountsIn.map( - (a) => a.token, - ), + assertAddLiquidityBoostedUnbalanced( + { + addLiquidityBoostedQueryOutput, + addLiquidityBuildCallOutput, + tokenAmountsForBalanceCheck, + txOutput, + }, + wethIsEth, ); }); - test('with reference token (non bpt)', async () => { - const addLiquidityProportionalInput: AddLiquidityBoostedInput = + }); + + describe('proportional', () => { + test('with bpt as reference token', async () => { + const referenceAmount = { + rawAmount: 1000000000000000000n, + decimals: 18, + address: boostedPool_USDC_USDT.address, + }; + const wethIsEth = false; + + const addLiquidityBoostedInput: AddLiquidityBoostedProportionalInput = { chainId, rpcUrl, - referenceAmount: { - rawAmount: 481201n, - decimals: 6, - address: USDC.address, - }, + referenceAmount, + tokensIn: tokensInForDoubleWrap, kind: AddLiquidityKind.Proportional, }; - const addLiquidityQueryOutput = await addLiquidityBoosted.query( - addLiquidityProportionalInput, - boostedPool_USDC_USDT, + + const txInput: AddLiquidityBoostedTxInput = { + client, + addLiquidityBoosted, + addLiquidityBoostedInput, + testAddress, + poolStateWithUnderlyings: boostedPool_USDC_USDT, + slippage: Slippage.fromPercentage('1'), + wethIsEth, + }; + + const { + addLiquidityBoostedQueryOutput, + addLiquidityBuildCallOutput, + tokenAmountsForBalanceCheck, + txOutput, + } = await doAddLiquidityBoosted(txInput); + + assertAddLiquidityBoostedProportional( + { + addLiquidityBoostedQueryOutput, + addLiquidityBuildCallOutput, + tokenAmountsForBalanceCheck, + txOutput, + }, + wethIsEth, ); - const addLiquidityBuildInput: AddLiquidityBoostedBuildCallInput = + }); + test('with underlying as reference token', async () => { + const referenceAmount = { + rawAmount: 481201n, + decimals: 6, + address: USDC.address, + }; + const wethIsEth = false; + + const addLiquidityBoostedInput: AddLiquidityBoostedProportionalInput = { - ...addLiquidityQueryOutput, - slippage: Slippage.fromPercentage('1'), + chainId, + rpcUrl, + referenceAmount, + tokensIn: tokensInForDoubleWrap, + kind: AddLiquidityKind.Proportional, }; - const addLiquidityBuildCallOutput = - addLiquidityBoosted.buildCall(addLiquidityBuildInput); + const txInput: AddLiquidityBoostedTxInput = { + client, + addLiquidityBoosted, + addLiquidityBoostedInput, + testAddress, + poolStateWithUnderlyings: boostedPool_USDC_USDT, + slippage: Slippage.fromPercentage('1'), + wethIsEth, + }; - const { transactionReceipt, balanceDeltas } = - await sendTransactionGetBalances( - [ - addLiquidityQueryOutput.bptOut.token.address, - USDC.address as `0x${string}`, - USDT.address as `0x${string}`, - ], - client, - testAddress, - addLiquidityBuildCallOutput.to, // - addLiquidityBuildCallOutput.callData, - ); + const { + addLiquidityBoostedQueryOutput, + addLiquidityBuildCallOutput, + tokenAmountsForBalanceCheck, + txOutput, + } = await doAddLiquidityBoosted(txInput); - expect(transactionReceipt.status).to.eq('success'); + assertAddLiquidityBoostedProportional( + { + addLiquidityBoostedQueryOutput, + addLiquidityBuildCallOutput, + tokenAmountsForBalanceCheck, + txOutput, + }, + wethIsEth, + ); + }); - addLiquidityQueryOutput.amountsIn.map((a) => { - expect(a.amount > 0n).to.be.true; - }); + test('with only one underlying token wrapped', async () => { + const referenceAmount = { + rawAmount: 481201n, + decimals: 6, + address: USDC.address, + }; + const wethIsEth = false; - const expectedDeltas = [ - addLiquidityQueryOutput.bptOut.amount, - ...addLiquidityQueryOutput.amountsIn.map( - (tokenAmount) => tokenAmount.amount, - ), - ]; - expect(balanceDeltas).to.deep.eq(expectedDeltas); + const addLiquidityBoostedInput: AddLiquidityBoostedProportionalInput = + { + chainId, + rpcUrl, + referenceAmount, + tokensIn: tokensInForSingleWrap, + kind: AddLiquidityKind.Proportional, + }; + const txInput: AddLiquidityBoostedTxInput = { + client, + addLiquidityBoosted, + addLiquidityBoostedInput, + testAddress, + poolStateWithUnderlyings: boostedPool_USDC_USDT, + slippage: Slippage.fromPercentage('1'), + wethIsEth, + }; - const slippageAdjustedQueryInput = - addLiquidityQueryOutput.amountsIn.map((amountsIn) => { - return Slippage.fromPercentage('1').applyTo( - amountsIn.amount, - 1, - ); - }); - expect( - addLiquidityBuildCallOutput.maxAmountsIn.map( - (a) => a.amount, - ), - ).to.deep.eq(slippageAdjustedQueryInput); + const { + addLiquidityBoostedQueryOutput, + addLiquidityBuildCallOutput, + tokenAmountsForBalanceCheck, + txOutput, + } = await doAddLiquidityBoosted(txInput); - // make sure to pass Tokens in correct order. Same as poolTokens but as underlyings instead - assertTokenMatch( - [ - new Token( - 111555111, - USDC.address as Address, - USDC.decimals, - ), - new Token( - 111555111, - USDT.address as Address, - USDT.decimals, - ), - ], - addLiquidityBuildCallOutput.maxAmountsIn.map( - (a) => a.token, - ), + assertAddLiquidityBoostedProportional( + { + addLiquidityBoostedQueryOutput, + addLiquidityBuildCallOutput, + tokenAmountsForBalanceCheck, + txOutput, + }, + wethIsEth, ); }); }); @@ -351,18 +477,7 @@ describe('Boosted AddLiquidity', () => { const input: AddLiquidityBoostedInput = { chainId, rpcUrl, - amountsIn: [ - { - rawAmount: 1000000n, - decimals: 6, - address: USDC.address as Address, - }, - { - rawAmount: 1000000n, - decimals: 6, - address: USDT.address as Address, - }, - ], + amountsIn: amountsInForDoubleWrap, kind: AddLiquidityKind.Unbalanced, }; @@ -393,8 +508,8 @@ describe('Boosted AddLiquidity', () => { await sendTransactionGetBalances( [ addLiquidityQueryOutput.bptOut.token.address, - USDC.address as `0x${string}`, - USDT.address as `0x${string}`, + USDC.address, + USDT.address, ], client, testAddress, @@ -422,8 +537,8 @@ describe('Boosted AddLiquidity', () => { ).to.be.true; }); }); - describe('add liquidity proportional', () => { - test('token inputs', async () => { + describe('proportional', () => { + test('with tokens', async () => { const addLiquidityProportionalInput: AddLiquidityBoostedInput = { chainId, @@ -433,6 +548,7 @@ describe('Boosted AddLiquidity', () => { decimals: 18, address: boostedPool_USDC_USDT.address, }, + tokensIn: tokensInForDoubleWrap, kind: AddLiquidityKind.Proportional, }; @@ -454,7 +570,7 @@ describe('Boosted AddLiquidity', () => { }); const addLiquidityBuildCallOutput = - await addLiquidityBoosted.buildCallWithPermit2( + addLiquidityBoosted.buildCallWithPermit2( addLiquidityBuildInput, permit2, ); @@ -463,8 +579,8 @@ describe('Boosted AddLiquidity', () => { await sendTransactionGetBalances( [ addLiquidityQueryOutput.bptOut.token.address, - USDC.address as `0x${string}`, - USDT.address as `0x${string}`, + USDC.address, + USDT.address, ], client, testAddress, @@ -502,16 +618,8 @@ describe('Boosted AddLiquidity', () => { // make sure to pass Tokens in correct order. Same as poolTokens but as underlyings instead assertTokenMatch( [ - new Token( - 111555111, - USDC.address as Address, - USDC.decimals, - ), - new Token( - 111555111, - USDT.address as Address, - USDT.decimals, - ), + new Token(111555111, USDC.address, USDC.decimals), + new Token(111555111, USDT.address, USDT.decimals), ], addLiquidityBuildCallOutput.maxAmountsIn.map( (a) => a.token, diff --git a/test/v3/addLiquidityBoosted/addLiquidityPartialBoosted.integration.test.ts b/test/v3/addLiquidityBoosted/addLiquidityPartialBoosted.integration.test.ts index 5b6aa82f..6520b8f2 100644 --- a/test/v3/addLiquidityBoosted/addLiquidityPartialBoosted.integration.test.ts +++ b/test/v3/addLiquidityBoosted/addLiquidityPartialBoosted.integration.test.ts @@ -13,7 +13,7 @@ import { } from 'viem'; import { Address, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED, CHAINS, ChainId, PERMIT2, @@ -60,7 +60,7 @@ describe('V3 add liquidity partial boosted', () => { beforeAll(async () => { // setup chain and test client - ({ rpcUrl } = await startFork(ANVIL_NETWORKS.SEPOLIA)); + ({ rpcUrl } = await startFork(ANVIL_NETWORKS[ChainId[chainId]])); client = createTestClient({ mode: 'anvil', @@ -94,7 +94,7 @@ describe('V3 add liquidity partial boosted', () => { client, testAddress, token.underlyingToken?.address ?? token.address, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[chainId], ); } @@ -124,7 +124,47 @@ describe('V3 add liquidity partial boosted', () => { }; }); - test('with tokens', async () => { + test('with only one token', async () => { + const wethIsEth = false; + + const txInput: AddLiquidityBoostedTxInput = { + client, + addLiquidityBoosted, + addLiquidityBoostedInput: { + ...addLiquidityBoostedInput, + amountsIn: [ + { + address: amountsIn[1].token.address, + rawAmount: amountsIn[1].amount, + decimals: amountsIn[1].token.decimals, + }, + ], + }, + testAddress, + poolStateWithUnderlyings: partialBoostedPool_WETH_stataUSDT, + slippage: Slippage.fromPercentage('1'), + wethIsEth, + }; + + const { + addLiquidityBoostedQueryOutput, + addLiquidityBuildCallOutput, + tokenAmountsForBalanceCheck, + txOutput, + } = await doAddLiquidityBoosted(txInput); + + assertAddLiquidityBoostedUnbalanced( + { + addLiquidityBoostedQueryOutput, + addLiquidityBuildCallOutput, + tokenAmountsForBalanceCheck, + txOutput, + }, + wethIsEth, + ); + }); + + test('with two tokens', async () => { const wethIsEth = false; const txInput: AddLiquidityBoostedTxInput = { @@ -201,12 +241,12 @@ describe('V3 add liquidity partial boosted', () => { chainId, rpcUrl, kind: AddLiquidityKind.Proportional, + tokensIn: [USDT.address, WETH.address], }; }); test('with tokens', async () => { const wethIsEth = false; - const txInput: AddLiquidityBoostedTxInput = { client, addLiquidityBoosted, diff --git a/test/v3/addLiquidityBuffer/addLiquidityBuffer.integration.test.ts b/test/v3/addLiquidityBuffer/addLiquidityBuffer.integration.test.ts index 71e7720d..78e88cd0 100644 --- a/test/v3/addLiquidityBuffer/addLiquidityBuffer.integration.test.ts +++ b/test/v3/addLiquidityBuffer/addLiquidityBuffer.integration.test.ts @@ -140,10 +140,7 @@ describe('Buffer AddLiquidity', () => { const { transactionReceipt, balanceDeltas } = await sendTransactionGetBalances( - [ - stataUSDC.address as `0x${string}`, - USDC.address as `0x${string}`, - ], + [stataUSDC.address, USDC.address], client, testAddress, addLiquidityBufferBuildCallOutput.to, diff --git a/test/v3/addLiquidityNested/addLiquidityNestedV3.integration.test.ts b/test/v3/addLiquidityNested/addLiquidityNestedV3.integration.test.ts index 50dc8ead..861e0aa2 100644 --- a/test/v3/addLiquidityNested/addLiquidityNestedV3.integration.test.ts +++ b/test/v3/addLiquidityNested/addLiquidityNestedV3.integration.test.ts @@ -14,7 +14,7 @@ import { } from 'viem'; import { Address, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED, CHAINS, ChainId, PERMIT2, @@ -158,7 +158,7 @@ describe('V3 add liquidity nested test, with Permit2 direct approval', () => { client, testAddress, amount.address, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[chainId], ); } @@ -177,7 +177,7 @@ describe('V3 add liquidity nested test, with Permit2 direct approval', () => { ); expect(addLiquidityBuildCallOutput.value === 0n).to.be.true; expect(addLiquidityBuildCallOutput.to).to.eq( - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[chainId], ); // send add liquidity transaction and check balance changes @@ -244,7 +244,7 @@ describe('V3 add liquidity nested test, with Permit2 direct approval', () => { client, testAddress, amount.address, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[chainId], ); } @@ -267,7 +267,7 @@ describe('V3 add liquidity nested test, with Permit2 direct approval', () => { addLiquidityInput.amountsIn[0].rawAmount, ).to.be.true; expect(addLiquidityBuildCallOutput.to).to.eq( - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[chainId], ); // send add liquidity transaction and check balance changes diff --git a/test/v3/addLiquidityNested/addLiquidityNestedV3Signature.integration.test.ts b/test/v3/addLiquidityNested/addLiquidityNestedV3Signature.integration.test.ts index b328795c..67cc02e6 100644 --- a/test/v3/addLiquidityNested/addLiquidityNestedV3Signature.integration.test.ts +++ b/test/v3/addLiquidityNested/addLiquidityNestedV3Signature.integration.test.ts @@ -23,7 +23,7 @@ import { AddLiquidityNestedInput, AddLiquidityNestedQueryOutputV3, Permit2Helper, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED, } from '@/index'; import { ANVIL_NETWORKS, startFork } from 'test/anvil/anvil-global-setup'; import { @@ -129,7 +129,7 @@ describe('V3 add liquidity nested test, with Permit2 signature', () => { ); expect(addLiquidityBuildCallOutput.value === 0n).to.be.true; expect(addLiquidityBuildCallOutput.to).to.eq( - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[chainId], ); // send add liquidity transaction and check balance changes diff --git a/test/v3/priceImpact/priceImpact.V3.integration.test.ts b/test/v3/priceImpact/priceImpact.V3.integration.test.ts index 926e811b..60fb1235 100644 --- a/test/v3/priceImpact/priceImpact.V3.integration.test.ts +++ b/test/v3/priceImpact/priceImpact.V3.integration.test.ts @@ -32,8 +32,9 @@ const WETH = TOKENS[chainId].WETH; describe('PriceImpact V3', () => { let rpcUrl: string; beforeAll(async () => { - ({ rpcUrl } = await startFork(ANVIL_NETWORKS.SEPOLIA)); + ({ rpcUrl } = await startFork(ANVIL_NETWORKS[ChainId[chainId]])); }); + describe('Full Boosted Pool Boosted Pool AddLiquidity', () => { test('Close to proportional', async () => { const addLiquidityInput: AddLiquidityBoostedUnbalancedInput = { @@ -59,7 +60,7 @@ describe('PriceImpact V3', () => { addLiquidityInput, boostedPool_USDC_USDT, ); - const priceImpactSpot = PriceImpactAmount.fromDecimal('0.000042'); + const priceImpactSpot = PriceImpactAmount.fromDecimal('0.000605'); expect(priceImpactABA.decimal).eq(priceImpactSpot.decimal); }); @@ -88,7 +89,7 @@ describe('PriceImpact V3', () => { boostedPool_USDC_USDT, ); const priceImpactSpot = - PriceImpactAmount.fromDecimal('0.0005511004'); + PriceImpactAmount.fromDecimal('0.00183097705'); expect(priceImpactABA.decimal).eq(priceImpactSpot.decimal); }); @@ -111,7 +112,7 @@ describe('PriceImpact V3', () => { addLiquidityInput, boostedPool_USDC_USDT, ); - const priceImpactSpot = PriceImpactAmount.fromDecimal('0.000492'); + const priceImpactSpot = PriceImpactAmount.fromDecimal('0.000208'); expect(priceImpactABA.decimal).eq(priceImpactSpot.decimal); }); }); @@ -141,7 +142,7 @@ describe('PriceImpact V3', () => { addLiquidityInput, partialBoostedPool_USDT_stataDAI, ); - const priceImpactSpot = PriceImpactAmount.fromDecimal('0.0000045'); + const priceImpactSpot = PriceImpactAmount.fromDecimal('0.0008225'); expect(priceImpactABA.decimal).eq(priceImpactSpot.decimal); }); @@ -169,8 +170,7 @@ describe('PriceImpact V3', () => { addLiquidityInput, partialBoostedPool_USDT_stataDAI, ); - const priceImpactSpot = - PriceImpactAmount.fromDecimal('0.000396375'); + const priceImpactSpot = PriceImpactAmount.fromDecimal('0.00171675'); expect(priceImpactABA.decimal).eq(priceImpactSpot.decimal); }); }); @@ -198,7 +198,7 @@ describe('PriceImpact V3', () => { nestedWithBoostedPool, ); const priceImpactSpot = PriceImpactAmount.fromDecimal( - '0.005213821423105922', + '0.004206886163133692', ); expect(priceImpactABA.decimal).eq(priceImpactSpot.decimal); }); diff --git a/test/v3/priceImpact/priceImpactErrors.V3.integration.test.ts b/test/v3/priceImpact/priceImpactErrors.V3.integration.test.ts index b96aad82..fe360288 100644 --- a/test/v3/priceImpact/priceImpactErrors.V3.integration.test.ts +++ b/test/v3/priceImpact/priceImpactErrors.V3.integration.test.ts @@ -38,6 +38,7 @@ describe('PriceImpact Errors V3', () => { address: USDC.address as Address, }, ], + wrapUnderlying: [true, true], kind: AddLiquidityKind.Unbalanced, userData: '0x', }; diff --git a/test/v3/removeLiquidityBoosted/removeLiquidityBoosted.integration.test.ts b/test/v3/removeLiquidityBoosted/removeLiquidityBoosted.integration.test.ts index 69780421..c4d7b3d6 100644 --- a/test/v3/removeLiquidityBoosted/removeLiquidityBoosted.integration.test.ts +++ b/test/v3/removeLiquidityBoosted/removeLiquidityBoosted.integration.test.ts @@ -14,29 +14,27 @@ import { } from 'viem'; import { - AddLiquidityProportionalInput, + AddLiquidityBoostedInput, AddLiquidityKind, RemoveLiquidityKind, Slippage, Hex, CHAINS, ChainId, - AddLiquidityInput, PERMIT2, Token, PublicWalletClient, AddLiquidityBoostedV3, RemoveLiquidityBoostedV3, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER, RemoveLiquidityBoostedProportionalInput, PermitHelper, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED, } from 'src'; import { - AddLiquidityTxInput, doAddLiquidity, setTokenBalances, approveSpenderOnTokens, - approveTokens, + approveSpenderOnPermit2, sendTransactionGetBalances, assertTokenMatch, TOKENS, @@ -46,15 +44,13 @@ import { import { ANVIL_NETWORKS, startFork } from '../../anvil/anvil-global-setup'; import { boostedPool_USDC_USDT } from 'test/mockData/boostedPool'; -const protocolVersion = 3; - const chainId = ChainId.SEPOLIA; const USDC = TOKENS[chainId].USDC_AAVE; const USDT = TOKENS[chainId].USDT_AAVE; +const stataUSDT = TOKENS[chainId].stataUSDT; -describe('remove liquidity test', () => { +describe('remove liquidity boosted proportional', () => { let client: PublicWalletClient & TestActions; - let txInput: AddLiquidityTxInput; let rpcUrl: string; let snapshot: Hex; let testAddress: Address; @@ -95,51 +91,72 @@ describe('remove liquidity test', () => { snapshot = await client.snapshot(); }); + // Add liquidity before each test to prepare for remove liquidity beforeEach(async () => { await client.revert({ id: snapshot, }); snapshot = await client.snapshot(); - // subapprovals for permit2 to the vault - // fine to do before each because it does not impact the - // requirement for BPT permits. (which are permits, not permit2) - // Here We approve the Vault to spend Tokens on the users behalf via Permit2 - await approveTokens( - client, - testAddress as Address, - [USDT.address, USDC.address] as Address[], - protocolVersion, - ); + for (const token of boostedPool_USDC_USDT.tokens) { + await approveSpenderOnPermit2( + client, + testAddress, + token.underlyingToken?.address ?? token.address, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[chainId], + ); + } - // join the pool - via direct approval - const slippage: Slippage = Slippage.fromPercentage('1'); + const addLiquidityInput: AddLiquidityBoostedInput = { + chainId: chainId, + rpcUrl: rpcUrl, + referenceAmount: { + rawAmount: 1000000000000000000n, + decimals: 18, + address: boostedPool_USDC_USDT.address, + }, + kind: AddLiquidityKind.Proportional, + tokensIn: [USDC.address, USDT.address], + }; - txInput = { + await doAddLiquidity({ client, addLiquidity: new AddLiquidityBoostedV3(), - slippage: slippage, + addLiquidityInput, + slippage: Slippage.fromPercentage('1'), poolState: boostedPool_USDC_USDT, testAddress, - addLiquidityInput: {} as AddLiquidityInput, - }; + }); + }); - const input: AddLiquidityProportionalInput = { + test('query returns correct token addresses', async () => { + const removeLiquidityBoostedV3 = new RemoveLiquidityBoostedV3(); + + const removeLiquidityInput: RemoveLiquidityBoostedProportionalInput = { chainId: chainId, rpcUrl: rpcUrl, - referenceAmount: { + bptIn: { rawAmount: 1000000000000000000n, decimals: 18, address: boostedPool_USDC_USDT.address, }, - kind: AddLiquidityKind.Proportional, + tokensOut: [USDC.address, stataUSDT.address], + kind: RemoveLiquidityKind.Proportional, }; - const _addLiquidityOutput = await doAddLiquidity({ - ...txInput, - addLiquidityInput: input, - }); + const removeLiquidityQueryOutput = await removeLiquidityBoostedV3.query( + removeLiquidityInput, + boostedPool_USDC_USDT, + ); + + const amountsOut = removeLiquidityQueryOutput.amountsOut; + + expect(amountsOut[0].token.address).to.eq(USDC.address.toLowerCase()); + expect(amountsOut[1].token.address).to.eq( + stataUSDT.address.toLowerCase(), + ); }); + describe('direct approval', () => { beforeEach(async () => { // Approve the Composite liquidity router. @@ -147,10 +164,10 @@ describe('remove liquidity test', () => { client, testAddress, [boostedPool_USDC_USDT.address] as Address[], - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[chainId], ); }); - test('remove liquidity proportional', async () => { + test('unwrap both tokens', async () => { const removeLiquidityBoostedV3 = new RemoveLiquidityBoostedV3(); const removeLiquidityInput: RemoveLiquidityBoostedProportionalInput = @@ -162,6 +179,7 @@ describe('remove liquidity test', () => { decimals: 18, address: boostedPool_USDC_USDT.address, }, + tokensOut: [USDC.address, USDT.address], kind: RemoveLiquidityKind.Proportional, }; @@ -181,11 +199,7 @@ describe('remove liquidity test', () => { const { transactionReceipt, balanceDeltas } = await sendTransactionGetBalances( - [ - boostedPool_USDC_USDT.address, - USDC.address as `0x${string}`, - USDT.address as `0x${string}`, - ], + [boostedPool_USDC_USDT.address, USDC.address, USDT.address], client, testAddress, removeLiquidityBuildCallOutput.to, @@ -240,6 +254,98 @@ describe('remove liquidity test', () => { ), ); }); + + test('unwrap only one token', async () => { + const removeLiquidityBoostedV3 = new RemoveLiquidityBoostedV3(); + + const removeLiquidityInput: RemoveLiquidityBoostedProportionalInput = + { + chainId: chainId, + rpcUrl: rpcUrl, + bptIn: { + rawAmount: 1000000000000000000n, + decimals: 18, + address: boostedPool_USDC_USDT.address, + }, + tokensOut: [USDC.address, stataUSDT.address], + kind: RemoveLiquidityKind.Proportional, + }; + + const removeLiquidityQueryOutput = + await removeLiquidityBoostedV3.query( + removeLiquidityInput, + boostedPool_USDC_USDT, + ); + + const removeLiquidityBuildInput = { + ...removeLiquidityQueryOutput, + slippage: Slippage.fromPercentage('1'), + }; + + const removeLiquidityBuildCallOutput = + removeLiquidityBoostedV3.buildCall(removeLiquidityBuildInput); + + const { transactionReceipt, balanceDeltas } = + await sendTransactionGetBalances( + [ + boostedPool_USDC_USDT.address, + USDC.address, + stataUSDT.address, + ], + client, + testAddress, + removeLiquidityBuildCallOutput.to, + removeLiquidityBuildCallOutput.callData, + ); + expect(transactionReceipt.status).to.eq('success'); + expect( + removeLiquidityQueryOutput.amountsOut.map((amount) => { + expect(amount.amount > 0).to.be.true; + }), + ); + + const expectedDeltas = [ + removeLiquidityQueryOutput.bptIn.amount, + ...removeLiquidityQueryOutput.amountsOut.map( + (amountOut) => amountOut.amount, + ), + ]; + // Here we check that output diff is within an acceptable tolerance as buffers can have difference in queries/result + expectedDeltas.forEach((delta, i) => { + areBigIntsWithinPercent(delta, balanceDeltas[i], 0.001); + }); + const expectedMinAmountsOut = + removeLiquidityQueryOutput.amountsOut.map((amountOut) => + removeLiquidityBuildInput.slippage.applyTo( + amountOut.amount, + -1, + ), + ); + expect(expectedMinAmountsOut).to.deep.eq( + removeLiquidityBuildCallOutput.minAmountsOut.map( + (a) => a.amount, + ), + ); + + // make sure to pass Tokens in correct order + assertTokenMatch( + [ + new Token( + 111555111, + USDC.address as Address, + USDC.decimals, + ), + new Token( + 111555111, + stataUSDT.address as Address, + stataUSDT.decimals, + ), + ], + removeLiquidityBuildCallOutput.minAmountsOut.map( + (a) => a.token, + ), + ); + }); }); describe('permit approval', () => { test('remove liquidity proportional', async () => { @@ -254,6 +360,7 @@ describe('remove liquidity test', () => { decimals: 18, address: boostedPool_USDC_USDT.address, }, + tokensOut: [USDC.address, USDT.address], kind: RemoveLiquidityKind.Proportional, sender: testAddress, userData: '0x123', @@ -286,11 +393,7 @@ describe('remove liquidity test', () => { const { transactionReceipt, balanceDeltas } = await sendTransactionGetBalances( - [ - boostedPool_USDC_USDT.address, - USDC.address as `0x${string}`, - USDT.address as `0x${string}`, - ], + [boostedPool_USDC_USDT.address, USDC.address, USDT.address], client, testAddress, removeLiquidityBuildCallOutput.to, diff --git a/test/v3/removeLiquidityBoosted/removeLiquidityPartialBoosted.integration.test.ts b/test/v3/removeLiquidityBoosted/removeLiquidityPartialBoosted.integration.test.ts index 2856d968..e20d2c11 100644 --- a/test/v3/removeLiquidityBoosted/removeLiquidityPartialBoosted.integration.test.ts +++ b/test/v3/removeLiquidityBoosted/removeLiquidityPartialBoosted.integration.test.ts @@ -19,7 +19,7 @@ import { CHAINS, PublicWalletClient, Token, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED, Slippage, RemoveLiquidityBoostedV3, RemoveLiquidityBoostedProportionalInput, @@ -50,7 +50,7 @@ const parentBptToken = new Token( ); // These are the underlying tokens const usdtToken = new Token(chainId, USDT.address, USDT.decimals); -const daiToken = new Token(chainId, WETH.address, WETH.decimals); +const wethToken = new Token(chainId, WETH.address, WETH.decimals); describe('V3 remove liquidity partial boosted', () => { let rpcUrl: string; @@ -63,7 +63,7 @@ describe('V3 remove liquidity partial boosted', () => { let removeLiquidityInput: RemoveLiquidityBoostedProportionalInput; beforeAll(async () => { - ({ rpcUrl } = await startFork(ANVIL_NETWORKS.SEPOLIA)); + ({ rpcUrl } = await startFork(ANVIL_NETWORKS[ChainId[chainId]])); client = createTestClient({ mode: 'anvil', @@ -108,6 +108,7 @@ describe('V3 remove liquidity partial boosted', () => { decimals: 18, rawAmount: bptAmount, }, + tokensOut: [USDT.address, WETH.address], kind: RemoveLiquidityKind.Proportional, }; @@ -142,7 +143,7 @@ describe('V3 remove liquidity partial boosted', () => { expect(queryOutput.amountsOut.length).to.eq( partialBoostedPool_WETH_stataUSDT.tokens.length, ); - validateTokenAmounts(queryOutput.amountsOut, [usdtToken, daiToken]); + validateTokenAmounts(queryOutput.amountsOut, [usdtToken, wethToken]); }); describe('remove liquidity transaction', async () => { @@ -152,7 +153,7 @@ describe('V3 remove liquidity partial boosted', () => { client, testAddress, parentBptToken.address, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[chainId], ); const queryOutput = await removeLiquidityBoosted.query( @@ -182,7 +183,7 @@ describe('V3 remove liquidity partial boosted', () => { ), ); expect(removeLiquidityBuildCallOutput.to).to.eq( - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[chainId], ); // send remove liquidity transaction and check balance changes @@ -216,7 +217,7 @@ describe('V3 remove liquidity partial boosted', () => { client, testAddress, parentBptToken.address, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[chainId], ); const queryOutput = await removeLiquidityBoosted.query( @@ -247,7 +248,7 @@ describe('V3 remove liquidity partial boosted', () => { ), ); expect(removeLiquidityBuildCallOutput.to).to.eq( - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_BOOSTED[chainId], ); const tokenAmountsForBalanceCheck = [ diff --git a/test/v3/removeLiquidityNested/removeLiquidityNestedV3.integration.test.ts b/test/v3/removeLiquidityNested/removeLiquidityNestedV3.integration.test.ts index 6b449236..c415a084 100644 --- a/test/v3/removeLiquidityNested/removeLiquidityNestedV3.integration.test.ts +++ b/test/v3/removeLiquidityNested/removeLiquidityNestedV3.integration.test.ts @@ -20,7 +20,7 @@ import { Token, RemoveLiquidityNestedInput, RemoveLiquidityNested, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED, Slippage, } from 'src'; @@ -105,7 +105,7 @@ describe('V3 remove liquidity nested test, with Permit direct approval', () => { client, testAddress, parentBptToken.address, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[chainId], ); snapshot = await client.snapshot(); diff --git a/test/v3/removeLiquidityNested/removeLiquidityNestedV3Signature.integration.test.ts b/test/v3/removeLiquidityNested/removeLiquidityNestedV3Signature.integration.test.ts index 16c3f106..debf567e 100644 --- a/test/v3/removeLiquidityNested/removeLiquidityNestedV3Signature.integration.test.ts +++ b/test/v3/removeLiquidityNested/removeLiquidityNestedV3Signature.integration.test.ts @@ -19,7 +19,7 @@ import { Token, RemoveLiquidityNestedInput, RemoveLiquidityNested, - BALANCER_COMPOSITE_LIQUIDITY_ROUTER, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED, Slippage, PermitHelper, RemoveLiquidityNestedCallInputV3, @@ -130,7 +130,7 @@ describe('V3 remove liquidity nested test, with Permit signature', () => { addLiquidityBuildCallOutput.minAmountsOut.map((a) => a.amount), ); expect(addLiquidityBuildCallOutput.to).to.eq( - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + BALANCER_COMPOSITE_LIQUIDITY_ROUTER_NESTED[chainId], ); // send remove liquidity transaction and check balance changes diff --git a/test/v3/utils/getPoolStateWithBalancesV3.integration.test.ts b/test/v3/utils/getPoolStateWithBalancesV3.integration.test.ts index 65dab215..68e557f8 100644 --- a/test/v3/utils/getPoolStateWithBalancesV3.integration.test.ts +++ b/test/v3/utils/getPoolStateWithBalancesV3.integration.test.ts @@ -52,16 +52,16 @@ describe('add liquidity test', () => { address: USDC.address, decimals: USDC.decimals, index: 0, - balance: '4982.377088', + balance: '6916.384366', }, { address: DAI.address, decimals: DAI.decimals, index: 1, - balance: '4412.573626596067233661', + balance: '6240.659067374271172646', }, ], - totalShares: '4685.71985547775593574', + totalShares: '6565.147517543863649467', }; expect(poolStateWithBalances).to.deep.eq(mockData);