diff --git a/.changeset/nice-balloons-train.md b/.changeset/nice-balloons-train.md new file mode 100644 index 00000000..22cb98d9 --- /dev/null +++ b/.changeset/nice-balloons-train.md @@ -0,0 +1,5 @@ +--- +"@balancer/sdk": patch +--- + +Add proportional helper to calculate BPT from a reference amount for boosted pools diff --git a/src/entities/addLiquidityBoosted/index.ts b/src/entities/addLiquidityBoosted/index.ts index 90603530..c0d00790 100644 --- a/src/entities/addLiquidityBoosted/index.ts +++ b/src/entities/addLiquidityBoosted/index.ts @@ -11,7 +11,11 @@ import { getAmountsCall } from '../addLiquidity/helpers'; import { PoolStateWithUnderlyings } from '@/entities/types'; -import { getAmounts, getSortedTokens } from '@/entities/utils'; +import { + getAmounts, + getBptAmountFromReferenceAmountBoosted, + getSortedTokens, +} from '@/entities/utils'; import { AddLiquidityBuildCallOutput, @@ -89,10 +93,10 @@ export class AddLiquidityBoostedV3 { break; } case AddLiquidityKind.Proportional: { - if (input.referenceAmount.address !== poolState.address) { - // TODO: add getBptAmountFromReferenceAmount - throw new Error('Reference token must be the pool token'); - } + const bptAmount = await getBptAmountFromReferenceAmountBoosted( + input, + poolState, + ); const exactAmountsInNumbers = await doAddLiquidityProportionalQuery( @@ -101,7 +105,7 @@ export class AddLiquidityBoostedV3 { input.sender ?? zeroAddress, input.userData ?? '0x', poolState.address, - input.referenceAmount.rawAmount, + bptAmount.rawAmount, ); // Amounts are mapped to child tokens of the pool @@ -114,7 +118,7 @@ export class AddLiquidityBoostedV3 { bptOut = TokenAmount.fromRawAmount( bptToken, - input.referenceAmount.rawAmount, + bptAmount.rawAmount, ); break; } diff --git a/src/entities/types.ts b/src/entities/types.ts index f7bcfb7c..6984bd3b 100644 --- a/src/entities/types.ts +++ b/src/entities/types.ts @@ -23,6 +23,10 @@ export type PoolTokenWithUnderlying = MinimalToken & { underlyingToken: MinimalToken | null; }; +export interface PoolTokenWithUnderlyingBalance extends PoolTokenWithBalance { + underlyingToken: PoolTokenWithBalance | null; +} + export type PoolStateWithUnderlyings = { id: Hex; address: Address; @@ -31,6 +35,15 @@ export type PoolStateWithUnderlyings = { protocolVersion: 1 | 2 | 3; }; +export type PoolStateWithUnderlyingBalances = { + id: Hex; + address: Address; + type: string; + tokens: PoolTokenWithUnderlyingBalance[]; + totalShares: HumanAmount; + protocolVersion: 1 | 2 | 3; +}; + export type AddLiquidityAmounts = { maxAmountsIn: bigint[]; maxAmountsInWithoutBpt: bigint[]; diff --git a/src/entities/utils/getPoolStateWithBalancesV3.ts b/src/entities/utils/getPoolStateWithBalancesV3.ts index a6fe51be..2c277e5a 100644 --- a/src/entities/utils/getPoolStateWithBalancesV3.ts +++ b/src/entities/utils/getPoolStateWithBalancesV3.ts @@ -1,10 +1,23 @@ -import { createPublicClient, formatEther, formatUnits, http } from 'viem'; +import { + createPublicClient, + erc4626Abi, + formatEther, + formatUnits, + http, + PublicClient, +} from 'viem'; import { HumanAmount } from '@/data'; import { CHAINS, VAULT_V3 } from '@/utils'; import { getSortedTokens } from './getSortedTokens'; -import { PoolState, PoolStateWithBalances } from '../types'; +import { + PoolState, + PoolStateWithBalances, + PoolStateWithUnderlyingBalances, + PoolStateWithUnderlyings, + PoolTokenWithUnderlying, +} from '../types'; import { vaultExtensionAbi_V3 } from '@/abi'; import { TokenAmount } from '../tokenAmount'; @@ -13,6 +26,131 @@ export const getPoolStateWithBalancesV3 = async ( chainId: number, rpcUrl: string, ): Promise => { + const publicClient = createPublicClient({ + transport: http(rpcUrl), + chain: CHAINS[chainId], + }); + + // get on-chain pool token balances and total shares + const { tokenAmounts, totalShares } = await getTokenAmountsAndTotalShares( + chainId, + poolState, + publicClient, + ); + + // build PoolStateWithBalances object with queried on-chain balances + const poolStateWithBalances: PoolStateWithBalances = { + ...poolState, + tokens: tokenAmounts.map((tokenAmount, i) => ({ + address: tokenAmount.token.address, + decimals: tokenAmount.token.decimals, + index: i, + balance: formatUnits( + tokenAmount.amount, + tokenAmount.token.decimals, + ) as HumanAmount, + })), + totalShares: formatEther(totalShares) as HumanAmount, + }; + return poolStateWithBalances; +}; + +export const getBoostedPoolStateWithBalancesV3 = async ( + poolState: PoolStateWithUnderlyings, + chainId: number, + rpcUrl: string, +): Promise => { + const publicClient = createPublicClient({ + transport: http(rpcUrl), + chain: CHAINS[chainId], + }); + + // get on-chain pool token balances and total shares + const { tokenAmounts, totalShares } = await getTokenAmountsAndTotalShares( + chainId, + poolState, + publicClient, + ); + + const sortedTokens = [...poolState.tokens].sort( + (a, b) => a.index - b.index, + ); + + // get on-chain balances for each respective underlying pool token (by querying erc4626 previewRedeem function) + const underlyingBalances = await getUnderlyingBalances( + sortedTokens, + tokenAmounts, + publicClient, + ); + + // build PoolStateWithUnderlyingBalances object with queried on-chain balances + const poolStateWithUnderlyingBalances: PoolStateWithUnderlyingBalances = { + ...poolState, + tokens: sortedTokens.map((token, i) => ({ + ...token, + balance: formatUnits( + tokenAmounts[i].amount, + token.decimals, + ) as HumanAmount, + underlyingToken: + token.underlyingToken === null + ? null + : { + ...token.underlyingToken, + balance: formatUnits( + underlyingBalances.shift() as bigint, + token.underlyingToken.decimals, + ) as HumanAmount, + }, + })), + totalShares: formatEther(totalShares) as HumanAmount, + }; + return poolStateWithUnderlyingBalances; +}; + +const getUnderlyingBalances = async ( + sortedTokens: PoolTokenWithUnderlying[], + tokenAmounts: TokenAmount[], + publicClient: PublicClient, +) => { + // create one contract call for each underlying token + const getUnderlyingBalancesContracts = sortedTokens + .filter((token) => token.underlyingToken !== null) + .map((token, i) => ({ + address: token.address, + abi: erc4626Abi, + functionName: 'previewRedeem' as const, + args: [tokenAmounts[i].amount] as const, + })); + + // execute multicall to get on-chain balances for each underlying token + const underlyingBalanceOutputs = await publicClient.multicall({ + contracts: [...getUnderlyingBalancesContracts], + }); + + // throw error if any of the underlying balance calls failed + if ( + underlyingBalanceOutputs.some((output) => output.status === 'failure') + ) { + throw new Error( + 'Error: Unable to get underlying balances for v3 pool.', + ); + } + + // extract underlying balances from multicall outputs + const underlyingBalances = underlyingBalanceOutputs.map( + (output) => output.result as bigint, + ); + + return underlyingBalances; +}; + +const getTokenAmountsAndTotalShares = async ( + chainId: number, + poolState: PoolState, + publicClient: PublicClient, +) => { + // create contract calls to get total supply and balances for each pool token const totalSupplyContract = { address: VAULT_V3[chainId], abi: vaultExtensionAbi_V3, @@ -26,40 +164,25 @@ export const getPoolStateWithBalancesV3 = async ( args: [poolState.address] as const, }; - const publicClient = createPublicClient({ - transport: http(rpcUrl), - chain: CHAINS[chainId], - }); + // execute multicall to get total supply and balances for each pool token const outputs = await publicClient.multicall({ contracts: [totalSupplyContract, getBalanceContracts], }); + // throw error if any of the calls failed if (outputs.some((output) => output.status === 'failure')) { throw new Error( 'Error: Unable to get pool state with balances for v3 pool.', ); } + // extract total supply and balances from multicall outputs const totalShares = outputs[0].result as bigint; const balancesScale18 = outputs[1].result as bigint[]; - - const sortedTokens = getSortedTokens(poolState.tokens, chainId); - const balances = sortedTokens.map((token, i) => + const poolTokens = getSortedTokens(poolState.tokens, chainId); + const tokenAmounts = poolTokens.map((token, i) => TokenAmount.fromScale18Amount(token, balancesScale18[i]), ); - const poolStateWithBalances: PoolStateWithBalances = { - ...poolState, - tokens: sortedTokens.map((token, i) => ({ - address: token.address, - decimals: token.decimals, - index: i, - balance: formatUnits( - balances[i].amount, - token.decimals, - ) as HumanAmount, - })), - totalShares: formatEther(totalShares) as HumanAmount, - }; - return poolStateWithBalances; + return { tokenAmounts, totalShares }; }; diff --git a/src/entities/utils/proportionalAmountsHelpers.ts b/src/entities/utils/proportionalAmountsHelpers.ts index 90f12999..6e8feae1 100644 --- a/src/entities/utils/proportionalAmountsHelpers.ts +++ b/src/entities/utils/proportionalAmountsHelpers.ts @@ -1,11 +1,15 @@ import { Address, parseUnits } from 'viem'; import { InputAmount } from '@/types'; import { HumanAmount } from '@/data'; -import { MathSol } from '@/utils'; +import { isSameAddress, MathSol } from '@/utils'; import { AddLiquidityProportionalInput } from '../addLiquidity/types'; -import { PoolState } from '../types'; +import { PoolState, PoolStateWithUnderlyings } from '../types'; import { getPoolStateWithBalancesV2 } from './getPoolStateWithBalancesV2'; -import { getPoolStateWithBalancesV3 } from './getPoolStateWithBalancesV3'; +import { + getBoostedPoolStateWithBalancesV3, + getPoolStateWithBalancesV3, +} from './getPoolStateWithBalancesV3'; +import { AddLiquidityBoostedProportionalInput } from '../addLiquidityBoosted/types'; /** * For a given pool and reference token amount, calculate all token amounts proportional to their balances within the pool. @@ -127,3 +131,46 @@ export const getBptAmountFromReferenceAmount = async ( } return bptAmount; }; + +/** + * Calculate the BPT amount for a given reference amount in a boosted pool (rounded down). + * + * @param input + * @param poolState + * @returns + */ +export const getBptAmountFromReferenceAmountBoosted = async ( + input: AddLiquidityBoostedProportionalInput, + poolStateWithUnderlyings: PoolStateWithUnderlyings, +): Promise => { + let bptAmount: InputAmount; + if ( + isSameAddress( + input.referenceAmount.address, + poolStateWithUnderlyings.address, + ) + ) { + bptAmount = input.referenceAmount; + } else { + const poolStateWithUnderlyingBalances = + await getBoostedPoolStateWithBalancesV3( + poolStateWithUnderlyings, + input.chainId, + input.rpcUrl, + ); + + // use underlying tokens as tokens if they exist (in case of a partial boosted pool) + const poolStateWithBalances = { + ...poolStateWithUnderlyingBalances, + tokens: poolStateWithUnderlyingBalances.tokens.map( + (t) => t.underlyingToken ?? t, + ), + }; + + ({ bptAmount } = calculateProportionalAmounts( + poolStateWithBalances, + input.referenceAmount, + )); + } + return bptAmount; +}; diff --git a/test/entities/swaps/v2/auraBalSwaps/auraBal.integration.test.ts b/test/entities/swaps/v2/auraBalSwaps/auraBal.integration.test.ts index 0ffb2187..e769c2c4 100644 --- a/test/entities/swaps/v2/auraBalSwaps/auraBal.integration.test.ts +++ b/test/entities/swaps/v2/auraBalSwaps/auraBal.integration.test.ts @@ -1,11 +1,14 @@ // pnpm test -- auraBal.integration.test.ts import { createTestClient, + Hex, http, publicActions, + TestActions, walletActions, zeroAddress, } from 'viem'; + import { Relayer, Slippage, @@ -17,6 +20,7 @@ import { NATIVE_ASSETS, SwapKind, ChainId, + PublicWalletClient, } from '@/index'; import { @@ -32,54 +36,67 @@ const weth = new Token(chainId, NATIVE_ASSETS[chainId].wrapped, 18); describe('auraBalSwaps:Integration tests', () => { let rpcUrl: string; + let snapshot: Hex; + let client: PublicWalletClient & TestActions; + beforeAll(async () => { // setup chain and test client ({ rpcUrl } = await startFork(ANVIL_NETWORKS.MAINNET)); + + client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + snapshot = await client.snapshot(); + }); + + beforeEach(async () => { + await client.revert({ + id: snapshot, + }); + snapshot = await client.snapshot(); }); describe('to auraBal', () => { test('from bal', async () => { - await testAuraBalSwap(bal, auraBalToken, 1, rpcUrl); + await testAuraBalSwap(client, bal, auraBalToken, 1, rpcUrl); }); test('from weth', async () => { - await testAuraBalSwap(weth, auraBalToken, 3, rpcUrl); + await testAuraBalSwap(client, weth, auraBalToken, 3, rpcUrl); }); test('from weth, wethIsEth=true', async () => { - await testAuraBalSwap(weth, auraBalToken, 3, rpcUrl, true); + await testAuraBalSwap(client, weth, auraBalToken, 3, rpcUrl, true); }); }); describe('from auraBal', () => { test('to bal', async () => { - await testAuraBalSwap(auraBalToken, bal, 0, rpcUrl); + await testAuraBalSwap(client, auraBalToken, bal, 0, rpcUrl); }); test('to weth', async () => { - await testAuraBalSwap(auraBalToken, weth, 0, rpcUrl); + await testAuraBalSwap(client, auraBalToken, weth, 0, rpcUrl); }); test('to weth, wethIsEth=true', async () => { - await testAuraBalSwap(auraBalToken, weth, 0, rpcUrl, true); + await testAuraBalSwap(client, auraBalToken, weth, 0, rpcUrl, true); }); }); }); async function testAuraBalSwap( + client: PublicWalletClient & TestActions, tokenIn: Token, tokenOut: Token, tokenInSlot: number, rpcUrl: string, wethIsEth = false, ) { - const client = createTestClient({ - mode: 'anvil', - chain: CHAINS[chainId], - transport: http(rpcUrl), - }) - .extend(publicActions) - .extend(walletActions); - const testAddress = (await client.getAddresses())[0]; const auraBalSwap = new AuraBalSwap(rpcUrl); const swapAmount = TokenAmount.fromHumanAmount(tokenIn, '1'); diff --git a/test/v3/addLiquidity/addLiquidity.integration.test.ts b/test/v3/addLiquidity/addLiquidity.integration.test.ts index ada858d2..a0e9f900 100644 --- a/test/v3/addLiquidity/addLiquidity.integration.test.ts +++ b/test/v3/addLiquidity/addLiquidity.integration.test.ts @@ -438,6 +438,7 @@ describe('add liquidity test', () => { ); }); }); + describe('add liquidity proportional', () => { let addLiquidityInput: AddLiquidityProportionalInput; beforeAll(() => { diff --git a/test/v3/addLiquidityBoosted/addLiquidityBoosted.integration.test.ts b/test/v3/addLiquidityBoosted/addLiquidityBoosted.integration.test.ts index 06bf6046..b6a0750f 100644 --- a/test/v3/addLiquidityBoosted/addLiquidityBoosted.integration.test.ts +++ b/test/v3/addLiquidityBoosted/addLiquidityBoosted.integration.test.ts @@ -177,7 +177,7 @@ describe('Boosted AddLiquidity', () => { }); }); describe('add liquidity proportional', () => { - test('with tokens', async () => { + test('with bpt', async () => { const addLiquidityProportionalInput: AddLiquidityBoostedInput = { chainId, @@ -200,7 +200,7 @@ describe('Boosted AddLiquidity', () => { }; const addLiquidityBuildCallOutput = - await addLiquidityBoosted.buildCall(addLiquidityBuildInput); + addLiquidityBoosted.buildCall(addLiquidityBuildInput); const { transactionReceipt, balanceDeltas } = await sendTransactionGetBalances( @@ -261,6 +261,90 @@ describe('Boosted AddLiquidity', () => { ), ); }); + test('with reference token (non bpt)', async () => { + const addLiquidityProportionalInput: AddLiquidityBoostedInput = + { + chainId, + rpcUrl, + referenceAmount: { + rawAmount: 481201n, + decimals: 6, + address: USDC.address, + }, + kind: AddLiquidityKind.Proportional, + }; + const addLiquidityQueryOutput = await addLiquidityBoosted.query( + addLiquidityProportionalInput, + boostedPool_USDC_USDT, + ); + const addLiquidityBuildInput: AddLiquidityBoostedBuildCallInput = + { + ...addLiquidityQueryOutput, + slippage: Slippage.fromPercentage('1'), + }; + + const addLiquidityBuildCallOutput = + addLiquidityBoosted.buildCall(addLiquidityBuildInput); + + 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, + ); + + expect(transactionReceipt.status).to.eq('success'); + + addLiquidityQueryOutput.amountsIn.map((a) => { + expect(a.amount > 0n).to.be.true; + }); + + const expectedDeltas = [ + addLiquidityQueryOutput.bptOut.amount, + ...addLiquidityQueryOutput.amountsIn.map( + (tokenAmount) => tokenAmount.amount, + ), + ]; + expect(balanceDeltas).to.deep.eq(expectedDeltas); + + 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); + + // 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, + ), + ); + }); test('with native', async () => { // TODO }); diff --git a/test/v3/addLiquidityBoosted/addLiquidityPartialBoosted.integration.test.ts b/test/v3/addLiquidityBoosted/addLiquidityPartialBoosted.integration.test.ts index 4c221f40..caaaa8d2 100644 --- a/test/v3/addLiquidityBoosted/addLiquidityPartialBoosted.integration.test.ts +++ b/test/v3/addLiquidityBoosted/addLiquidityPartialBoosted.integration.test.ts @@ -4,6 +4,7 @@ dotenv.config(); import { createTestClient, + Hex, http, parseUnits, publicActions, @@ -23,6 +24,7 @@ import { AddLiquidityBoostedV3, AddLiquidityKind, AddLiquidityBoostedUnbalancedInput, + AddLiquidityBoostedProportionalInput, } from '@/index'; import { ANVIL_NETWORKS, startFork } from 'test/anvil/anvil-global-setup'; import { @@ -52,8 +54,8 @@ describe('V3 add liquidity partial boosted', () => { let rpcUrl: string; let client: PublicWalletClient & TestActions; let testAddress: Address; + let snapshot: Hex; const addLiquidityBoosted = new AddLiquidityBoostedV3(); - let addLiquidityInput: AddLiquidityBoostedUnbalancedInput; const amountsIn = [ TokenAmount.fromHumanAmount(usdtToken, '1'), TokenAmount.fromHumanAmount(daiToken, '2'), @@ -80,91 +82,192 @@ describe('V3 add liquidity partial boosted', () => { [USDT.slot, DAI.slot] as number[], amountsIn.map((t) => parseUnits('1000', t.token.decimals)), ); - - addLiquidityInput = { - amountsIn: amountsIn.map((a) => ({ - address: a.token.address, - rawAmount: a.amount, - decimals: a.token.decimals, - })), - chainId, - rpcUrl, - kind: AddLiquidityKind.Unbalanced, - }; + // Uses Special RPC methods to revert state back to same snapshot for each test + // https://github.com/trufflesuite/ganache-cli-archive/blob/master/README.md + snapshot = await client.snapshot(); }); - test('query with underlying', async () => { - const queryOutput = await addLiquidityBoosted.query( - addLiquidityInput, - partialBoostedPool_USDT_stataDAI, - ); - expect(queryOutput.protocolVersion).toEqual(3); - expect(queryOutput.bptOut.token).to.deep.eq(parentBptToken); - expect(queryOutput.bptOut.amount > 0n).to.be.true; - expect(queryOutput.amountsIn).to.deep.eq(amountsIn); + beforeEach(async () => { + await client.revert({ + id: snapshot, + }); + snapshot = await client.snapshot(); }); - test('add liquidity transaction', async () => { - for (const amount of addLiquidityInput.amountsIn) { - // Approve Permit2 to spend account tokens - await approveSpenderOnToken( - client, - testAddress, - amount.address, - PERMIT2[chainId], + describe('unbalanced', async () => { + let addLiquidityInput: AddLiquidityBoostedUnbalancedInput; + + beforeAll(async () => { + addLiquidityInput = { + amountsIn: amountsIn.map((a) => ({ + address: a.token.address, + rawAmount: a.amount, + decimals: a.token.decimals, + })), + chainId, + rpcUrl, + kind: AddLiquidityKind.Unbalanced, + }; + }); + + test('add liquidity transaction', async () => { + for (const amount of addLiquidityInput.amountsIn) { + // Approve Permit2 to spend account tokens + await approveSpenderOnToken( + client, + testAddress, + amount.address, + PERMIT2[chainId], + ); + // Approve Router to spend account tokens using Permit2 + await approveSpenderOnPermit2( + client, + testAddress, + amount.address, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + ); + } + + const queryOutput = await addLiquidityBoosted.query( + addLiquidityInput, + partialBoostedPool_USDT_stataDAI, ); - // Approve Router to spend account tokens using Permit2 - await approveSpenderOnPermit2( - client, - testAddress, - amount.address, + + expect(queryOutput.protocolVersion).toEqual(3); + expect(queryOutput.bptOut.token).to.deep.eq(parentBptToken); + expect(queryOutput.bptOut.amount > 0n).to.be.true; + expect(queryOutput.amountsIn).to.deep.eq(amountsIn); + + const addLiquidityBuildInput = { + ...queryOutput, + slippage: Slippage.fromPercentage('1'), // 1%, + }; + + const addLiquidityBuildCallOutput = addLiquidityBoosted.buildCall( + addLiquidityBuildInput, + ); + expect(addLiquidityBuildCallOutput.value === 0n).to.be.true; + expect(addLiquidityBuildCallOutput.to).to.eq( BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], ); - } - const queryOutput = await addLiquidityBoosted.query( - addLiquidityInput, - partialBoostedPool_USDT_stataDAI, - ); + // send add liquidity transaction and check balance changes + const { transactionReceipt, balanceDeltas } = + await sendTransactionGetBalances( + [ + ...amountsIn.map((t) => t.token.address), + queryOutput.bptOut.token.address, + ], + client, + testAddress, + addLiquidityBuildCallOutput.to, + addLiquidityBuildCallOutput.callData, + addLiquidityBuildCallOutput.value, + ); - const addLiquidityBuildInput = { - ...queryOutput, - slippage: Slippage.fromPercentage('1'), // 1%, - }; + expect(transactionReceipt.status).to.eq('success'); - const addLiquidityBuildCallOutput = addLiquidityBoosted.buildCall( - addLiquidityBuildInput, - ); - expect(addLiquidityBuildCallOutput.value === 0n).to.be.true; - expect(addLiquidityBuildCallOutput.to).to.eq( - BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], - ); + expect(amountsIn.map((a) => a.amount)).to.deep.eq( + balanceDeltas.slice(0, -1), + ); + // Here we check that output diff is within an acceptable tolerance. + // !!! This should only be used in the case of buffers as all other cases can be equal + areBigIntsWithinPercent( + balanceDeltas[balanceDeltas.length - 1], + queryOutput.bptOut.amount, + 0.001, + ); + }); + }); - // send add liquidity transaction and check balance changes - const { transactionReceipt, balanceDeltas } = - await sendTransactionGetBalances( - [ - ...amountsIn.map((t) => t.token.address), - queryOutput.bptOut.token.address, - ], - client, - testAddress, - addLiquidityBuildCallOutput.to, - addLiquidityBuildCallOutput.callData, - addLiquidityBuildCallOutput.value, + describe('proportional', async () => { + let addLiquidityInput: AddLiquidityBoostedProportionalInput; + let referenceTokenAmount: TokenAmount; + beforeAll(async () => { + referenceTokenAmount = amountsIn[0]; + addLiquidityInput = { + referenceAmount: { + address: referenceTokenAmount.token.address, + rawAmount: referenceTokenAmount.amount, + decimals: referenceTokenAmount.token.decimals, + }, + chainId, + rpcUrl, + kind: AddLiquidityKind.Proportional, + }; + }); + + test('add liquidity transaction', async () => { + for (const token of partialBoostedPool_USDT_stataDAI.tokens) { + // Approve Permit2 to spend account tokens + await approveSpenderOnToken( + client, + testAddress, + token.underlyingToken?.address ?? token.address, + PERMIT2[chainId], + ); + // Approve Router to spend account tokens using Permit2 + await approveSpenderOnPermit2( + client, + testAddress, + token.underlyingToken?.address ?? token.address, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + ); + } + + const queryOutput = await addLiquidityBoosted.query( + addLiquidityInput, + partialBoostedPool_USDT_stataDAI, ); - expect(transactionReceipt.status).to.eq('success'); + expect(queryOutput.protocolVersion).toEqual(3); + expect(queryOutput.bptOut.token).to.deep.eq(parentBptToken); + expect(queryOutput.bptOut.amount > 0n).to.be.true; - expect(amountsIn.map((a) => a.amount)).to.deep.eq( - balanceDeltas.slice(0, -1), - ); - // Here we check that output diff is within an acceptable tolerance. - // !!! This should only be used in the case of buffers as all other cases can be equal - areBigIntsWithinPercent( - balanceDeltas[balanceDeltas.length - 1], - queryOutput.bptOut.amount, - 0.001, - ); + const addLiquidityBuildInput = { + ...queryOutput, + slippage: Slippage.fromPercentage('1'), // 1%, + }; + + const addLiquidityBuildCallOutput = addLiquidityBoosted.buildCall( + addLiquidityBuildInput, + ); + expect(addLiquidityBuildCallOutput.value === 0n).to.be.true; + expect(addLiquidityBuildCallOutput.to).to.eq( + BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + ); + + // send add liquidity transaction and check balance changes + + const tokensForBalanceCheck = [ + ...queryOutput.amountsIn, + queryOutput.bptOut, + ]; + + const { transactionReceipt, balanceDeltas } = + await sendTransactionGetBalances( + tokensForBalanceCheck.map((t) => t.token.address), + client, + testAddress, + addLiquidityBuildCallOutput.to, + addLiquidityBuildCallOutput.callData, + addLiquidityBuildCallOutput.value, + ); + + expect(transactionReceipt.status).to.eq('success'); + + // Here we check that output diff is within an acceptable tolerance. + // !!! This should only be used in the case of buffers as all other cases can be equal + tokensForBalanceCheck.forEach( + (token, i) => + expect( + areBigIntsWithinPercent( + balanceDeltas[i], + token.amount, + 0.001, + ), + ).to.be.true, + ); + }); }); });