From 6cf4cc6c5fde5a39ac43ad75be7dbd191ee1ed86 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 23 Oct 2024 09:55:30 +0100 Subject: [PATCH 01/10] chore: Re-org so addLiquidityNested can switch on protocol version. --- .../doAddLiquidityNestedQuery.ts | 11 +- .../{ => addLiquidityNestedV2}/encodeCalls.ts | 16 +- .../getQueryCallsAttributes.ts | 16 +- .../addLiquidityNestedV2/index.ts | 126 ++++++++ .../{ => addLiquidityNestedV2}/types.ts | 13 +- .../validateInputs.ts | 8 +- src/entities/addLiquidityNested/index.ts | 138 +++----- src/entities/index.ts | 2 - src/entities/priceImpact/index.ts | 4 +- src/entities/types.ts | 1 + test/lib/utils/addLiquidityNestedHelper.ts | 2 +- .../addLiquidityNested.integration.test.ts | 1 + ...LiquidityNestedPolygon.integration.test.ts | 1 + .../priceImpact.integration.test.ts | 1 + .../addLiquidity.integration.test.ts | 6 +- .../addLiquidityNestedV3.integration.test.ts | 299 ++++++++++++++++++ .../removeLiquidity.integration.test.ts | 0 17 files changed, 507 insertions(+), 138 deletions(-) rename src/entities/addLiquidityNested/{ => addLiquidityNestedV2}/doAddLiquidityNestedQuery.ts (86%) rename src/entities/addLiquidityNested/{ => addLiquidityNestedV2}/encodeCalls.ts (86%) rename src/entities/addLiquidityNested/{ => addLiquidityNestedV2}/getQueryCallsAttributes.ts (95%) create mode 100644 src/entities/addLiquidityNested/addLiquidityNestedV2/index.ts rename src/entities/addLiquidityNested/{ => addLiquidityNestedV2}/types.ts (74%) rename src/entities/addLiquidityNested/{ => addLiquidityNestedV2}/validateInputs.ts (87%) rename test/v3/{ => addLiquidity}/addLiquidity.integration.test.ts (99%) create mode 100644 test/v3/addLiquidityNested/addLiquidityNestedV3.integration.test.ts rename test/v3/{ => removeLiquidity}/removeLiquidity.integration.test.ts (100%) diff --git a/src/entities/addLiquidityNested/doAddLiquidityNestedQuery.ts b/src/entities/addLiquidityNested/addLiquidityNestedV2/doAddLiquidityNestedQuery.ts similarity index 86% rename from src/entities/addLiquidityNested/doAddLiquidityNestedQuery.ts rename to src/entities/addLiquidityNested/addLiquidityNestedV2/doAddLiquidityNestedQuery.ts index 79df6a1f..670a3e18 100644 --- a/src/entities/addLiquidityNested/doAddLiquidityNestedQuery.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV2/doAddLiquidityNestedQuery.ts @@ -4,14 +4,19 @@ import { decodeFunctionResult, http, } from 'viem'; -import { Hex } from '../../types'; -import { BALANCER_RELAYER, CHAINS, ChainId, EMPTY_SENDER } from '../../utils'; +import { Hex } from '../../../types'; +import { + BALANCER_RELAYER, + CHAINS, + ChainId, + EMPTY_SENDER, +} from '../../../utils'; import { balancerRelayerAbi, permit2Abi, vaultExtensionAbi_V3, vaultV3Abi, -} from '../../abi'; +} from '../../../abi'; export const doAddLiquidityNestedQuery = async ( chainId: ChainId, diff --git a/src/entities/addLiquidityNested/encodeCalls.ts b/src/entities/addLiquidityNested/addLiquidityNestedV2/encodeCalls.ts similarity index 86% rename from src/entities/addLiquidityNested/encodeCalls.ts rename to src/entities/addLiquidityNested/addLiquidityNestedV2/encodeCalls.ts index f20af1de..96193ca8 100644 --- a/src/entities/addLiquidityNested/encodeCalls.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV2/encodeCalls.ts @@ -1,12 +1,12 @@ -import { Hex, PoolType } from '../../types'; -import { WeightedEncoder } from '../encoders'; -import { ComposableStableEncoder } from '../encoders/composableStable'; +import { ComposableStableEncoder } from '../../encoders/composableStable'; +import { batchRelayerLibraryAbi } from '../../../abi'; +import { encodeFunctionData, Hex } from 'viem'; +import { TokenAmount } from '../../tokenAmount'; +import { getValue } from '../../utils/getValue'; +import { replaceWrapped } from '@/entities/utils'; import { AddLiquidityNestedCallAttributes } from './types'; -import { replaceWrapped } from '../utils/replaceWrapped'; -import { batchRelayerLibraryAbi } from '../../abi'; -import { encodeFunctionData } from 'viem'; -import { TokenAmount } from '../tokenAmount'; -import { getValue } from '../utils/getValue'; +import { WeightedEncoder } from '@/entities/encoders'; +import { PoolType } from '@/types'; export const encodeCalls = ( callsAttributes: AddLiquidityNestedCallAttributes[], diff --git a/src/entities/addLiquidityNested/getQueryCallsAttributes.ts b/src/entities/addLiquidityNested/addLiquidityNestedV2/getQueryCallsAttributes.ts similarity index 95% rename from src/entities/addLiquidityNested/getQueryCallsAttributes.ts rename to src/entities/addLiquidityNested/addLiquidityNestedV2/getQueryCallsAttributes.ts index 08949d4c..95ec2d91 100644 --- a/src/entities/addLiquidityNested/getQueryCallsAttributes.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV2/getQueryCallsAttributes.ts @@ -1,17 +1,17 @@ -import { Token } from '../token'; +import { Address, PoolType } from '@/types'; +import { Token } from '@/entities/token'; import { - BALANCER_RELAYER, - ChainId, ZERO_ADDRESS, + BALANCER_RELAYER, getPoolAddress, -} from '../../utils'; + ChainId, +} from '@/utils'; import { - AddLiquidityNestedInput, AddLiquidityNestedCallAttributes, + AddLiquidityNestedInput, } from './types'; -import { NestedPool, PoolKind } from '../types'; -import { Address, PoolType } from '../../types'; -import { Relayer } from '../relayer'; +import { NestedPool, PoolKind } from '@/entities/types'; +import { Relayer } from '@/entities/relayer'; export const getQueryCallsAttributes = ( { amountsIn, chainId, fromInternalBalance }: AddLiquidityNestedInput, diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV2/index.ts b/src/entities/addLiquidityNested/addLiquidityNestedV2/index.ts new file mode 100644 index 00000000..c24e90ba --- /dev/null +++ b/src/entities/addLiquidityNested/addLiquidityNestedV2/index.ts @@ -0,0 +1,126 @@ +import { encodeFunctionData } from 'viem'; +import { Address, Hex } from '../../../types'; +import { Token } from '../../token'; +import { BALANCER_RELAYER, ZERO_ADDRESS } from '../../../utils'; +import { Relayer } from '../../relayer'; +import { encodeCalls } from './encodeCalls'; +import { TokenAmount } from '../../tokenAmount'; +import { balancerRelayerAbi } from '../../../abi'; +import { + AddLiquidityNestedInput, + AddLiquidityNestedQueryOutput, + AddLiquidityNestedCallInput, +} from './types'; +import { doAddLiquidityNestedQuery } from './doAddLiquidityNestedQuery'; +import { getQueryCallsAttributes } from './getQueryCallsAttributes'; +import { validateBuildCallInput, validateQueryInput } from './validateInputs'; +import { NestedPoolState } from '../../types'; +import { validateNestedPoolState } from '../../utils'; + +export class AddLiquidityNestedV2 { + async query( + input: AddLiquidityNestedInput, + nestedPoolState: NestedPoolState, + ): Promise { + const amountsIn = validateQueryInput(input, nestedPoolState); + validateNestedPoolState(nestedPoolState); + + const callsAttributes = getQueryCallsAttributes( + input, + nestedPoolState.pools, + ); + + const { encodedCalls } = encodeCalls(callsAttributes); + + // append peek call to get bptOut + const peekCall = Relayer.encodePeekChainedReferenceValue( + callsAttributes[callsAttributes.length - 1].outputReference, + ); + encodedCalls.push(peekCall); + + const encodedMulticall = encodeFunctionData({ + abi: balancerRelayerAbi, + functionName: 'vaultActionsQueryMulticall', + args: [encodedCalls], + }); + + const peekedValue = await doAddLiquidityNestedQuery( + input.chainId, + input.rpcUrl, + encodedMulticall, + ); + + const tokenOut = new Token( + input.chainId, + callsAttributes[callsAttributes.length - 1].poolAddress, + 18, + ); + const bptOut = TokenAmount.fromRawAmount(tokenOut, peekedValue); + + return { callsAttributes, amountsIn, bptOut, protocolVersion: 2 }; + } + + buildCall(input: AddLiquidityNestedCallInput): { + callData: Hex; + to: Address; + value: bigint | undefined; + minBptOut: bigint; + } { + validateBuildCallInput(input); + // apply slippage to bptOut + const minBptOut = input.slippage.applyTo(input.bptOut.amount, -1); + + // update last call with minBptOut limit in place + input.callsAttributes[input.callsAttributes.length - 1] = { + ...input.callsAttributes[input.callsAttributes.length - 1], + minBptOut, + }; + + // update wethIsEth flag + sender and recipient placeholders + input.callsAttributes = input.callsAttributes.map((call) => { + return { + ...call, + sender: + call.sender === ZERO_ADDRESS + ? input.accountAddress + : call.sender, + recipient: + call.recipient === ZERO_ADDRESS + ? input.accountAddress + : call.recipient, + wethIsEth: input.wethIsEth, + }; + }); + + const { encodedCalls, values } = encodeCalls(input.callsAttributes); + + // prepend relayer approval if provided + if (input.relayerApprovalSignature !== undefined) { + encodedCalls.unshift( + Relayer.encodeSetRelayerApproval( + BALANCER_RELAYER[input.callsAttributes[0].chainId], + true, + input.relayerApprovalSignature, + ), + ); + } + + const callData = encodeFunctionData({ + abi: balancerRelayerAbi, + functionName: 'multicall', + args: [encodedCalls], + }); + + // aggregate values from all calls + const accumulatedValue = values.reduce((acc, value) => { + return acc + value; + }, 0n); + + return { + callData, + to: BALANCER_RELAYER[input.callsAttributes[0].chainId], + value: accumulatedValue, + minBptOut, + }; + } +} diff --git a/src/entities/addLiquidityNested/types.ts b/src/entities/addLiquidityNested/addLiquidityNestedV2/types.ts similarity index 74% rename from src/entities/addLiquidityNested/types.ts rename to src/entities/addLiquidityNested/addLiquidityNestedV2/types.ts index c7ece58d..ba5af10b 100644 --- a/src/entities/addLiquidityNested/types.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV2/types.ts @@ -1,9 +1,9 @@ -import { Address, Hex, InputAmount, PoolType } from '../../types'; -import { ChainId } from '../../utils'; -import { Slippage } from '../slippage'; -import { Token } from '../token'; -import { TokenAmount } from '../tokenAmount'; -import { PoolKind } from '../types'; +import { Address, Hex, InputAmount, PoolType } from '../../../types'; +import { ChainId } from '../../../utils'; +import { Slippage } from '../../slippage'; +import { Token } from '../../token'; +import { TokenAmount } from '../../tokenAmount'; +import { PoolKind } from '../../types'; export type AddLiquidityNestedInput = { amountsIn: InputAmount[]; @@ -35,6 +35,7 @@ export type AddLiquidityNestedQueryOutput = { callsAttributes: AddLiquidityNestedCallAttributes[]; amountsIn: TokenAmount[]; bptOut: TokenAmount; + protocolVersion: 1 | 2 | 3; }; export type AddLiquidityNestedCallInput = AddLiquidityNestedQueryOutput & { diff --git a/src/entities/addLiquidityNested/validateInputs.ts b/src/entities/addLiquidityNested/addLiquidityNestedV2/validateInputs.ts similarity index 87% rename from src/entities/addLiquidityNested/validateInputs.ts rename to src/entities/addLiquidityNested/addLiquidityNestedV2/validateInputs.ts index acb31a18..bd81af47 100644 --- a/src/entities/addLiquidityNested/validateInputs.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV2/validateInputs.ts @@ -1,7 +1,7 @@ -import { NATIVE_ASSETS } from '../../utils'; -import { Token } from '../token'; -import { TokenAmount } from '../tokenAmount'; -import { NestedPoolState } from '../types'; +import { NATIVE_ASSETS } from '../../../utils'; +import { Token } from '../../token'; +import { TokenAmount } from '../../tokenAmount'; +import { NestedPoolState } from '../../types'; import { AddLiquidityNestedCallInput, AddLiquidityNestedInput } from './types'; export const validateQueryInput = ( diff --git a/src/entities/addLiquidityNested/index.ts b/src/entities/addLiquidityNested/index.ts index 802b623d..1d090cda 100644 --- a/src/entities/addLiquidityNested/index.ts +++ b/src/entities/addLiquidityNested/index.ts @@ -1,63 +1,39 @@ -import { encodeFunctionData } from 'viem'; import { Address, Hex } from '../../types'; -import { Token } from '../token'; -import { BALANCER_RELAYER, ZERO_ADDRESS } from '../../utils'; -import { Relayer } from '../relayer'; -import { encodeCalls } from './encodeCalls'; -import { TokenAmount } from '../tokenAmount'; -import { balancerRelayerAbi } from '../../abi'; import { AddLiquidityNestedInput, AddLiquidityNestedQueryOutput, AddLiquidityNestedCallInput, -} from './types'; -import { doAddLiquidityNestedQuery } from './doAddLiquidityNestedQuery'; -import { getQueryCallsAttributes } from './getQueryCallsAttributes'; -import { validateBuildCallInput, validateQueryInput } from './validateInputs'; +} from './addLiquidityNestedV2/types'; +// import { validateQueryInput } from './addLiquidityNestedV2/validateInputs'; import { NestedPoolState } from '../types'; import { validateNestedPoolState } from '../utils'; +import { AddLiquidityNestedV2 } from './addLiquidityNestedV2'; export class AddLiquidityNested { + constructor(public config?: AddLiquidityConfig) {} + async query( input: AddLiquidityNestedInput, nestedPoolState: NestedPoolState, ): Promise { - const amountsIn = validateQueryInput(input, nestedPoolState); + // const amountsIn = validateQueryInput(input, nestedPoolState); validateNestedPoolState(nestedPoolState); - - const callsAttributes = getQueryCallsAttributes( - input, - nestedPoolState.pools, - ); - - const { encodedCalls } = encodeCalls(callsAttributes); - - // append peek call to get bptOut - const peekCall = Relayer.encodePeekChainedReferenceValue( - callsAttributes[callsAttributes.length - 1].outputReference, - ); - encodedCalls.push(peekCall); - - const encodedMulticall = encodeFunctionData({ - abi: balancerRelayerAbi, - functionName: 'vaultActionsQueryMulticall', - args: [encodedCalls], - }); - - const peekedValue = await doAddLiquidityNestedQuery( - input.chainId, - input.rpcUrl, - encodedMulticall, - ); - - const tokenOut = new Token( - input.chainId, - callsAttributes[callsAttributes.length - 1].poolAddress, - 18, - ); - const bptOut = TokenAmount.fromRawAmount(tokenOut, peekedValue); - - return { callsAttributes, amountsIn, bptOut }; + switch (nestedPoolState.protocolVersion) { + case 1: { + throw new Error( + 'AddLiquidityNested not supported for ProtocolVersion 1.', + ); + } + case 2: { + const addLiquidity = new AddLiquidityNestedV2(); + return addLiquidity.query(input, nestedPoolState); + } + case 3: { + throw new Error( + 'AddLiquidityNested not supported for ProtocolVersion 3.', + ); + } + } } buildCall(input: AddLiquidityNestedCallInput): { @@ -66,61 +42,21 @@ export class AddLiquidityNested { value: bigint | undefined; minBptOut: bigint; } { - validateBuildCallInput(input); - // apply slippage to bptOut - const minBptOut = input.slippage.applyTo(input.bptOut.amount, -1); - - // update last call with minBptOut limit in place - input.callsAttributes[input.callsAttributes.length - 1] = { - ...input.callsAttributes[input.callsAttributes.length - 1], - minBptOut, - }; - - // update wethIsEth flag + sender and recipient placeholders - input.callsAttributes = input.callsAttributes.map((call) => { - return { - ...call, - sender: - call.sender === ZERO_ADDRESS - ? input.accountAddress - : call.sender, - recipient: - call.recipient === ZERO_ADDRESS - ? input.accountAddress - : call.recipient, - wethIsEth: input.wethIsEth, - }; - }); - - const { encodedCalls, values } = encodeCalls(input.callsAttributes); - - // prepend relayer approval if provided - if (input.relayerApprovalSignature !== undefined) { - encodedCalls.unshift( - Relayer.encodeSetRelayerApproval( - BALANCER_RELAYER[input.callsAttributes[0].chainId], - true, - input.relayerApprovalSignature, - ), - ); + switch (input.protocolVersion) { + case 1: { + throw new Error( + 'AddLiquidityNested not supported for ProtocolVersion 1.', + ); + } + case 2: { + const addLiquidity = new AddLiquidityNestedV2(); + return addLiquidity.buildCall(input); + } + case 3: { + throw new Error( + 'AddLiquidityNested not supported for ProtocolVersion 3.', + ); + } } - - const callData = encodeFunctionData({ - abi: balancerRelayerAbi, - functionName: 'multicall', - args: [encodedCalls], - }); - - // aggregate values from all calls - const accumulatedValue = values.reduce((acc, value) => { - return acc + value; - }, 0n); - - return { - callData, - to: BALANCER_RELAYER[input.callsAttributes[0].chainId], - value: accumulatedValue, - minBptOut, - }; } } diff --git a/src/entities/index.ts b/src/entities/index.ts index 9c602e88..a7d8c1cd 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -1,7 +1,5 @@ export * from './addLiquidity'; export * from './addLiquidity/types'; -export * from './addLiquidityNested'; -export * from './addLiquidityNested/types'; export * from './createPool'; export * from './encoders'; export * from './initPool'; diff --git a/src/entities/priceImpact/index.ts b/src/entities/priceImpact/index.ts index 724300c9..26dd500e 100644 --- a/src/entities/priceImpact/index.ts +++ b/src/entities/priceImpact/index.ts @@ -7,8 +7,6 @@ import { AddLiquiditySingleTokenInput, AddLiquidityUnbalancedInput, } from '../addLiquidity/types'; -import { AddLiquidityNested } from '../addLiquidityNested'; -import { AddLiquidityNestedInput } from '../addLiquidityNested/types'; import { PriceImpactAmount } from '../priceImpactAmount'; import { RemoveLiquidity } from '../removeLiquidity'; import { @@ -24,6 +22,8 @@ import { Token } from '../token'; import { TokenAmount } from '../tokenAmount'; import { NestedPoolState, PoolState } from '../types'; import { getSortedTokens } from '../utils'; +import { AddLiquidityNestedInput } from '../addLiquidityNested/addLiquidityNestedV2/types'; +import { AddLiquidityNested } from '../addLiquidityNested'; export class PriceImpact { /** diff --git a/src/entities/types.ts b/src/entities/types.ts index 954f046b..79e5deb0 100644 --- a/src/entities/types.ts +++ b/src/entities/types.ts @@ -41,6 +41,7 @@ export type NestedPool = { }; export type NestedPoolState = { + protocolVersion: 1 | 2 | 3; pools: NestedPool[]; mainTokens: { address: Address; diff --git a/test/lib/utils/addLiquidityNestedHelper.ts b/test/lib/utils/addLiquidityNestedHelper.ts index dbb4b302..843b13ed 100644 --- a/test/lib/utils/addLiquidityNestedHelper.ts +++ b/test/lib/utils/addLiquidityNestedHelper.ts @@ -1,6 +1,5 @@ import { Address, TransactionReceipt } from 'viem'; import { - AddLiquidityNested, AddLiquidityNestedInput, Relayer, Slippage, @@ -10,6 +9,7 @@ import { import { BALANCER_RELAYER, NATIVE_ASSETS } from '@/utils'; import { AddLiquidityNestedTxInput } from './types'; import { sendTransactionGetBalances } from './helper'; +import { AddLiquidityNested } from '@/entities/addLiquidityNested'; export const assertResults = ( transactionReceipt: TransactionReceipt, diff --git a/test/v2/addLiquidityNested/addLiquidityNested.integration.test.ts b/test/v2/addLiquidityNested/addLiquidityNested.integration.test.ts index 834b0e2c..fb08b1b9 100644 --- a/test/v2/addLiquidityNested/addLiquidityNested.integration.test.ts +++ b/test/v2/addLiquidityNested/addLiquidityNested.integration.test.ts @@ -227,6 +227,7 @@ class MockApi { public async getNestedPool(poolId: Hex): Promise { if (poolId !== BPT_WETH_3POOL.id) throw Error(); return { + protocolVersion: 2, pools: [ { id: BPT_WETH_3POOL.id, diff --git a/test/v2/addLiquidityNested/addLiquidityNestedPolygon.integration.test.ts b/test/v2/addLiquidityNested/addLiquidityNestedPolygon.integration.test.ts index de6ac85a..7f64e20b 100644 --- a/test/v2/addLiquidityNested/addLiquidityNestedPolygon.integration.test.ts +++ b/test/v2/addLiquidityNested/addLiquidityNestedPolygon.integration.test.ts @@ -134,6 +134,7 @@ export class MockApi { public async getNestedPool(poolId: Hex): Promise { if (poolId !== DAO_st_WMATIC.id) throw Error(); return { + protocolVersion: 2, pools: [ { id: DAO_st_WMATIC.id, diff --git a/test/v2/priceImpact/priceImpact.integration.test.ts b/test/v2/priceImpact/priceImpact.integration.test.ts index bb09cbf9..6160ec4b 100644 --- a/test/v2/priceImpact/priceImpact.integration.test.ts +++ b/test/v2/priceImpact/priceImpact.integration.test.ts @@ -518,6 +518,7 @@ class MockApi { public async getNestedPool(poolId: Hex): Promise { if (poolId !== BPT_WETH_3POOL.id) throw Error(); return { + protocolVersion: 2, pools: [ { id: BPT_WETH_3POOL.id, diff --git a/test/v3/addLiquidity.integration.test.ts b/test/v3/addLiquidity/addLiquidity.integration.test.ts similarity index 99% rename from test/v3/addLiquidity.integration.test.ts rename to test/v3/addLiquidity/addLiquidity.integration.test.ts index 419d3df0..04a04d22 100644 --- a/test/v3/addLiquidity.integration.test.ts +++ b/test/v3/addLiquidity/addLiquidity.integration.test.ts @@ -29,7 +29,7 @@ import { PoolType, PERMIT2, PublicWalletClient, -} from '../../src'; +} from '@/index'; import { AddLiquidityTxInput, assertAddLiquidityUnbalanced, @@ -41,8 +41,8 @@ import { setTokenBalances, approveSpenderOnTokens, approveTokens, -} from '../lib/utils'; -import { ANVIL_NETWORKS, startFork } from '../anvil/anvil-global-setup'; +} from '../../lib/utils'; +import { ANVIL_NETWORKS, startFork } from '../../anvil/anvil-global-setup'; const protocolVersion = 3; diff --git a/test/v3/addLiquidityNested/addLiquidityNestedV3.integration.test.ts b/test/v3/addLiquidityNested/addLiquidityNestedV3.integration.test.ts new file mode 100644 index 00000000..3a47d440 --- /dev/null +++ b/test/v3/addLiquidityNested/addLiquidityNestedV3.integration.test.ts @@ -0,0 +1,299 @@ +// pnpm test -- addLiquidityNestedV3.integration.test.ts +import dotenv from 'dotenv'; +dotenv.config(); + +import { + createTestClient, + http, + parseUnits, + publicActions, + TestActions, + walletActions, +} from 'viem'; + +import { + Address, + CHAINS, + ChainId, + Hex, + NestedPoolState, + PublicWalletClient, +} from '@/index'; + +import { ANVIL_NETWORKS, startFork } from 'test/anvil/anvil-global-setup'; +import { + AddLiquidityNestedTxInput, + assertResults, + doAddLiquidityNested, + forkSetup, + POOLS, + TestToken, + TOKENS, +} from 'test/lib/utils'; + +const chainId = ChainId.SEPOLIA; +const DAI = TOKENS[chainId].DAI; +const WETH = TOKENS[chainId].WETH; +const USDC = TOKENS[chainId].USDC; +const USDT = TOKENS[chainId].USDT; +const BPT_3POOL = POOLS[chainId].BPT_3POOL; +const BPT_WETH_3POOL = POOLS[chainId].BPT_WETH_3POOL; + +describe('V3 add liquidity nested test', () => { + let rpcUrl: string; + let client: PublicWalletClient & TestActions; + let poolId: Hex; + let testAddress: Address; + let mainTokens: TestToken[]; + let initialBalances: bigint[]; + let txInput: AddLiquidityNestedTxInput; + + beforeAll(async () => { + // setup chain and test client + ({ rpcUrl } = await startFork(ANVIL_NETWORKS.SEPOLIA)); + + client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + testAddress = (await client.getAddresses())[0]; + + poolId = BPT_WETH_3POOL.id; + + // setup mock api + const api = new MockApi(); + // get pool state from api + const nestedPoolState = await api.getNestedPool(poolId); + + mainTokens = [WETH, DAI, USDC, USDT]; + initialBalances = mainTokens.map((t) => parseUnits('1000', t.decimals)); + + txInput = { + nestedPoolState, + chainId, + rpcUrl, + testAddress, + client, + amountsIn: [], + }; + }); + + beforeEach(async () => { + // User approve vault to spend their tokens and update user balance + await forkSetup( + client, + testAddress, + mainTokens.map((t) => t.address), + mainTokens.map((t) => t.slot) as number[], + initialBalances, + ); + }); + + test('single token', async () => { + const amountsIn = [ + { + address: WETH.address, + rawAmount: parseUnits('1', WETH.decimals), + decimals: WETH.decimals, + }, + ]; + + txInput = { + ...txInput, + amountsIn, + }; + + const { + transactionReceipt, + balanceDeltas, + bptOut, + minBptOut, + slippage, + value, + } = await doAddLiquidityNested(txInput); + + assertResults( + transactionReceipt, + bptOut, + amountsIn, + balanceDeltas, + slippage, + minBptOut, + chainId, + value, + ); + }); + + // test('all tokens', async () => { + // const amountsIn = mainTokens.map((t) => ({ + // address: t.address, + // rawAmount: parseUnits('1', t.decimals), + // decimals: t.decimals, + // })); + + // txInput = { + // ...txInput, + // amountsIn, + // }; + + // const { + // transactionReceipt, + // balanceDeltas, + // bptOut, + // minBptOut, + // slippage, + // value, + // } = await doAddLiquidityNested(txInput); + + // assertResults( + // transactionReceipt, + // bptOut, + // amountsIn, + // balanceDeltas, + // slippage, + // minBptOut, + // chainId, + // value, + // ); + // }); + + // test('native asset', async () => { + // const amountsIn = mainTokens.map((t) => ({ + // address: t.address, + // rawAmount: parseUnits('1', t.decimals), + // decimals: t.decimals, + // })); + + // const wethIsEth = true; + + // txInput = { + // ...txInput, + // amountsIn, + // wethIsEth, + // }; + + // const { + // transactionReceipt, + // balanceDeltas, + // bptOut, + // minBptOut, + // slippage, + // value, + // } = await doAddLiquidityNested(txInput); + + // assertResults( + // transactionReceipt, + // bptOut, + // amountsIn, + // balanceDeltas, + // slippage, + // minBptOut, + // chainId, + // value, + // wethIsEth, + // ); + // }); + + // test('native asset - invalid input', async () => { + // const amountsIn = [ + // { + // address: USDC.address, + // rawAmount: parseUnits('1', USDC.decimals), + // decimals: USDC.decimals, + // }, + // ]; + + // const wethIsEth = true; + + // txInput = { + // ...txInput, + // amountsIn, + // wethIsEth, + // }; + + // await expect(() => doAddLiquidityNested(txInput)).rejects.toThrowError( + // 'Adding liquidity with native asset requires wrapped native asset to exist within amountsIn', + // ); + // }); +}); + +/*********************** Mock To Represent API Requirements **********************/ + +class MockApi { + public async getNestedPool(poolId: Hex): Promise { + if (poolId !== BPT_WETH_3POOL.id) throw Error(); + return { + protocolVersion: 3, + pools: [ + { + id: BPT_WETH_3POOL.id, + address: BPT_WETH_3POOL.address, + type: BPT_WETH_3POOL.type, + level: 1, + tokens: [ + { + address: BPT_3POOL.address, + decimals: BPT_3POOL.decimals, + index: 0, + }, + { + address: WETH.address, + decimals: WETH.decimals, + index: 1, + }, + ], + }, + { + id: BPT_3POOL.id, + address: BPT_3POOL.address, + type: BPT_3POOL.type, + level: 0, + tokens: [ + { + address: DAI.address, + decimals: DAI.decimals, + index: 0, + }, + { + address: BPT_3POOL.address, + decimals: BPT_3POOL.decimals, + index: 1, + }, + { + address: USDC.address, + decimals: USDC.decimals, + index: 2, + }, + { + address: USDT.address, + decimals: USDT.decimals, + index: 3, + }, + ], + }, + ], + mainTokens: [ + { + address: WETH.address, + decimals: WETH.decimals, + }, + { + address: DAI.address, + decimals: DAI.decimals, + }, + { + address: USDC.address, + decimals: USDC.decimals, + }, + { + address: USDT.address, + decimals: USDT.decimals, + }, + ], + }; + } +} diff --git a/test/v3/removeLiquidity.integration.test.ts b/test/v3/removeLiquidity/removeLiquidity.integration.test.ts similarity index 100% rename from test/v3/removeLiquidity.integration.test.ts rename to test/v3/removeLiquidity/removeLiquidity.integration.test.ts From 78c313d71176b18a0231f03a426590df0fe0649f Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 23 Oct 2024 10:24:22 +0100 Subject: [PATCH 02/10] chore: Re-org so removeLiquidityNested can switch on protocol version. --- examples/addLiquidity/addLiquidityNested.ts | 4 +- src/entities/addLiquidityNested/index.ts | 2 - src/entities/index.ts | 4 +- src/entities/priceImpact/index.ts | 2 +- src/entities/removeLiquidityNested/index.ts | 166 ++----- .../doRemoveLiquidityNestedQuery.ts | 6 +- .../encodeCalls.ts | 13 +- .../getPeekCalls.ts | 4 +- .../getQueryCallsAttributes.ts | 16 +- .../removeLiquidityNestedV2/index.ts | 155 +++++++ .../{ => removeLiquidityNestedV2}/types.ts | 14 +- .../removeLiquidityNestedV2/validateInputs.ts | 57 +++ .../removeLiquidityNested/validateInputs.ts | 4 +- test/lib/utils/addLiquidityNestedHelper.ts | 9 +- .../removeLiquidityNested.integration.test.ts | 3 +- ...emoveLiquidityNestedV3.integration.test.ts | 414 ++++++++++++++++++ 16 files changed, 701 insertions(+), 172 deletions(-) rename src/entities/removeLiquidityNested/{ => removeLiquidityNestedV2}/doRemoveLiquidityNestedQuery.ts (85%) rename src/entities/removeLiquidityNested/{ => removeLiquidityNestedV2}/encodeCalls.ts (91%) rename src/entities/removeLiquidityNested/{ => removeLiquidityNestedV2}/getPeekCalls.ts (96%) rename src/entities/removeLiquidityNested/{ => removeLiquidityNestedV2}/getQueryCallsAttributes.ts (96%) create mode 100644 src/entities/removeLiquidityNested/removeLiquidityNestedV2/index.ts rename src/entities/removeLiquidityNested/{ => removeLiquidityNestedV2}/types.ts (80%) create mode 100644 src/entities/removeLiquidityNested/removeLiquidityNestedV2/validateInputs.ts create mode 100644 test/v3/removeLiquidityNestedV3.integration.test.ts/removeLiquidityNestedV3.integration.test.ts diff --git a/examples/addLiquidity/addLiquidityNested.ts b/examples/addLiquidity/addLiquidityNested.ts index 8eaf2588..5a08cc4c 100644 --- a/examples/addLiquidity/addLiquidityNested.ts +++ b/examples/addLiquidity/addLiquidityNested.ts @@ -14,8 +14,6 @@ import { } from 'viem'; import { Address, - AddLiquidityNested, - AddLiquidityNestedInput, BALANCER_RELAYER, BalancerApi, API_ENDPOINT, @@ -29,6 +27,8 @@ import { import { ANVIL_NETWORKS, startFork } from '../../test/anvil/anvil-global-setup'; import { makeForkTx } from 'examples/lib/makeForkTx'; import { getSlot } from 'examples/lib/getSlot'; +import { AddLiquidityNestedInput } from '@/entities/addLiquidityNested/addLiquidityNestedV2/types'; +import { AddLiquidityNested } from '@/entities/addLiquidityNested'; async function runAgainstFork() { // User defined inputs diff --git a/src/entities/addLiquidityNested/index.ts b/src/entities/addLiquidityNested/index.ts index 1d090cda..69802c3c 100644 --- a/src/entities/addLiquidityNested/index.ts +++ b/src/entities/addLiquidityNested/index.ts @@ -10,8 +10,6 @@ import { validateNestedPoolState } from '../utils'; import { AddLiquidityNestedV2 } from './addLiquidityNestedV2'; export class AddLiquidityNested { - constructor(public config?: AddLiquidityConfig) {} - async query( input: AddLiquidityNestedInput, nestedPoolState: NestedPoolState, diff --git a/src/entities/index.ts b/src/entities/index.ts index a7d8c1cd..a478a11d 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -10,8 +10,8 @@ export * from './priceImpactAmount'; export * from './relayer'; export * from './removeLiquidity'; export * from './removeLiquidity/types'; -export * from './removeLiquidityNested'; -export * from './removeLiquidityNested/types'; +export * from './removeLiquidityNested/removeLiquidityNestedV2'; +export * from './removeLiquidityNested/removeLiquidityNestedV2/types'; export * from './slippage'; export * from './token'; export * from './tokenAmount'; diff --git a/src/entities/priceImpact/index.ts b/src/entities/priceImpact/index.ts index 26dd500e..0c70751b 100644 --- a/src/entities/priceImpact/index.ts +++ b/src/entities/priceImpact/index.ts @@ -16,7 +16,7 @@ import { RemoveLiquidityUnbalancedInput, } from '../removeLiquidity/types'; import { RemoveLiquidityNested } from '../removeLiquidityNested'; -import { RemoveLiquidityNestedSingleTokenInput } from '../removeLiquidityNested/types'; +import { RemoveLiquidityNestedSingleTokenInput } from '../removeLiquidityNested/removeLiquidityNestedV2/types'; import { Swap, SwapInput } from '../swap'; import { Token } from '../token'; import { TokenAmount } from '../tokenAmount'; diff --git a/src/entities/removeLiquidityNested/index.ts b/src/entities/removeLiquidityNested/index.ts index 6ca5db0c..079e0d02 100644 --- a/src/entities/removeLiquidityNested/index.ts +++ b/src/entities/removeLiquidityNested/index.ts @@ -1,87 +1,36 @@ -import { encodeFunctionData } from 'viem'; - -import { balancerRelayerAbi } from '../../abi'; -import { Address, Hex } from '../../types'; -import { BALANCER_RELAYER, ZERO_ADDRESS } from '../../utils'; - -import { Relayer } from '../relayer'; -import { TokenAmount } from '../tokenAmount'; -import { NestedPoolState } from '../types'; -import { validateNestedPoolState } from '../utils'; - -import { encodeCalls } from './encodeCalls'; -import { doRemoveLiquidityNestedQuery } from './doRemoveLiquidityNestedQuery'; -import { getPeekCalls } from './getPeekCalls'; -import { getQueryCallsAttributes } from './getQueryCallsAttributes'; +import { Address, Hex } from 'viem'; +import { TokenAmount, NestedPoolState } from '@/entities'; +import { validateNestedPoolState } from '@/entities/utils'; import { RemoveLiquidityNestedQueryOutput, RemoveLiquidityNestedCallInput, RemoveLiquidityNestedInput, -} from './types'; -import { validateQueryInput, validateBuildCallInput } from './validateInputs'; +} from './removeLiquidityNestedV2/types'; +import { validateBuildCallInput } from './validateInputs'; +import { RemoveLiquidityNestedV2 } from './removeLiquidityNestedV2'; export class RemoveLiquidityNested { async query( input: RemoveLiquidityNestedInput, nestedPoolState: NestedPoolState, ): Promise { - const isProportional = validateQueryInput(input, nestedPoolState); validateNestedPoolState(nestedPoolState); - - const { callsAttributes, bptAmountIn } = getQueryCallsAttributes( - input, - nestedPoolState.pools, - isProportional, - ); - - const encodedCalls = encodeCalls(callsAttributes, isProportional); - - const { peekCalls, tokensOut } = getPeekCalls( - callsAttributes, - isProportional, - ); - - // insert peek calls to get amountsOut - let tokensOutCount = 0; - const tokensOutIndexes: number[] = []; - callsAttributes.forEach((call, i) => { - tokensOut.forEach((tokenOut, j) => { - if ( - call.sortedTokens.some((t) => - t.isSameAddress(tokenOut.address), - ) - ) { - tokensOutCount++; - encodedCalls.splice(i + tokensOutCount, 0, peekCalls[j]); - tokensOutIndexes.push(i + tokensOutCount); - } - }); - }); - - const encodedMulticall = encodeFunctionData({ - abi: balancerRelayerAbi, - functionName: 'vaultActionsQueryMulticall', - args: [encodedCalls], - }); - - const peekedValues = await doRemoveLiquidityNestedQuery( - input.chainId, - input.rpcUrl, - encodedMulticall, - tokensOutIndexes, - ); - - const amountsOut = tokensOut.map((tokenOut, i) => - TokenAmount.fromRawAmount(tokenOut, peekedValues[i]), - ); - - return { - callsAttributes, - bptAmountIn, - amountsOut, - isProportional, - chainId: input.chainId, - }; + switch (nestedPoolState.protocolVersion) { + case 1: { + throw new Error( + 'RemoveLiquidityNested not supported for ProtocolVersion 1.', + ); + } + case 2: { + const addLiquidity = new RemoveLiquidityNestedV2(); + return addLiquidity.query(input, nestedPoolState); + } + case 3: { + throw new Error( + 'RemoveLiquidityNested not supported for ProtocolVersion 3.', + ); + } + } } buildCall(input: RemoveLiquidityNestedCallInput): { @@ -91,64 +40,21 @@ export class RemoveLiquidityNested { } { validateBuildCallInput(input); - // apply slippage to amountsOut - const minAmountsOut = input.amountsOut.map((amountOut) => - TokenAmount.fromRawAmount( - amountOut.token, - input.slippage.applyTo(amountOut.amount, -1), - ), - ); - - input.callsAttributes.forEach((call) => { - // update relevant calls with minAmountOut limits in place - minAmountsOut.forEach((minAmountOut, j) => { - const minAmountOutIndex = call.sortedTokens.findIndex((t) => - t.isSameAddress(minAmountOut.token.address), + switch (input.protocolVersion) { + case 1: { + throw new Error( + 'AddLiquidityNested not supported for ProtocolVersion 1.', ); - if (minAmountOutIndex !== -1) { - call.minAmountsOut[minAmountOutIndex] = - minAmountsOut[j].amount; - } - }); - // update wethIsEth flag - call.wethIsEth = !!input.wethIsEth; - // update sender and recipient placeholders - call.sender = - call.sender === ZERO_ADDRESS - ? input.accountAddress - : call.sender; - call.recipient = - call.recipient === ZERO_ADDRESS - ? input.accountAddress - : call.recipient; - }); - - const encodedCalls = encodeCalls( - input.callsAttributes, - input.isProportional, - ); - - // prepend relayer approval if provided - if (input.relayerApprovalSignature !== undefined) { - encodedCalls.unshift( - Relayer.encodeSetRelayerApproval( - BALANCER_RELAYER[input.callsAttributes[0].chainId], - true, - input.relayerApprovalSignature, - ), - ); + } + case 2: { + const removeLiquidity = new RemoveLiquidityNestedV2(); + return removeLiquidity.buildCall(input); + } + case 3: { + throw new Error( + 'AddLiquidityNested not supported for ProtocolVersion 3.', + ); + } } - - const callData = encodeFunctionData({ - abi: balancerRelayerAbi, - functionName: 'multicall', - args: [encodedCalls], - }); - - return { - callData, - to: BALANCER_RELAYER[input.callsAttributes[0].chainId], - minAmountsOut, - }; } } diff --git a/src/entities/removeLiquidityNested/doRemoveLiquidityNestedQuery.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/doRemoveLiquidityNestedQuery.ts similarity index 85% rename from src/entities/removeLiquidityNested/doRemoveLiquidityNestedQuery.ts rename to src/entities/removeLiquidityNested/removeLiquidityNestedV2/doRemoveLiquidityNestedQuery.ts index 5f0668ff..53e5da02 100644 --- a/src/entities/removeLiquidityNested/doRemoveLiquidityNestedQuery.ts +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/doRemoveLiquidityNestedQuery.ts @@ -2,11 +2,11 @@ import { createPublicClient, decodeAbiParameters, decodeFunctionResult, + Hex, http, } from 'viem'; -import { Hex } from '../../types'; -import { BALANCER_RELAYER, CHAINS, ChainId, EMPTY_SENDER } from '../../utils'; -import { balancerRelayerAbi } from '../../abi'; +import { balancerRelayerAbi } from '@/abi'; +import { BALANCER_RELAYER, ChainId, CHAINS, EMPTY_SENDER } from '@/utils'; export const doRemoveLiquidityNestedQuery = async ( chainId: ChainId, diff --git a/src/entities/removeLiquidityNested/encodeCalls.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/encodeCalls.ts similarity index 91% rename from src/entities/removeLiquidityNested/encodeCalls.ts rename to src/entities/removeLiquidityNested/removeLiquidityNestedV2/encodeCalls.ts index 40cdba89..322cf172 100644 --- a/src/entities/removeLiquidityNested/encodeCalls.ts +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/encodeCalls.ts @@ -1,11 +1,10 @@ -import { Hex, PoolType } from '../../types'; -import { WeightedEncoder } from '../encoders'; -import { ComposableStableEncoder } from '../encoders/composableStable'; -import { RemoveLiquidityNestedCallAttributes } from './types'; -import { replaceWrapped } from '../utils/replaceWrapped'; -import { batchRelayerLibraryAbi } from '../../abi'; -import { encodeFunctionData } from 'viem'; +import { encodeFunctionData, Hex } from 'viem'; import { removeLiquiditySingleTokenExactInShouldHaveTokenOutIndexError } from '@/utils'; +import { RemoveLiquidityNestedCallAttributes } from './types'; +import { replaceWrapped } from '@/entities/utils'; +import { batchRelayerLibraryAbi } from '@/abi'; +import { PoolType } from '@/types'; +import { ComposableStableEncoder, WeightedEncoder } from '@/entities/encoders'; export const encodeCalls = ( callsAttributes: RemoveLiquidityNestedCallAttributes[], diff --git a/src/entities/removeLiquidityNested/getPeekCalls.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/getPeekCalls.ts similarity index 96% rename from src/entities/removeLiquidityNested/getPeekCalls.ts rename to src/entities/removeLiquidityNested/removeLiquidityNestedV2/getPeekCalls.ts index 5cf8239c..30cd9ede 100644 --- a/src/entities/removeLiquidityNested/getPeekCalls.ts +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/getPeekCalls.ts @@ -1,7 +1,7 @@ import { Hex } from 'viem'; -import { Token } from '../token'; import { RemoveLiquidityNestedCallAttributes } from './types'; -import { Relayer } from '../relayer'; +import { Token } from '@/entities/token'; +import { Relayer } from '@/entities/relayer'; export const getPeekCalls = ( calls: RemoveLiquidityNestedCallAttributes[], diff --git a/src/entities/removeLiquidityNested/getQueryCallsAttributes.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/getQueryCallsAttributes.ts similarity index 96% rename from src/entities/removeLiquidityNested/getQueryCallsAttributes.ts rename to src/entities/removeLiquidityNested/removeLiquidityNestedV2/getQueryCallsAttributes.ts index 4aaa2e4b..b35dc23c 100644 --- a/src/entities/removeLiquidityNested/getQueryCallsAttributes.ts +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/getQueryCallsAttributes.ts @@ -1,14 +1,16 @@ -import { Token } from '../token'; +import { Address } from 'viem'; +import { TokenAmount } from '@/entities/tokenAmount'; + +import { BALANCER_RELAYER, ChainId, ZERO_ADDRESS } from '@/utils'; +import { Token } from '@/entities/token'; +import { NestedPool, PoolKind } from '@/entities/types'; import { + RemoveLiquidityNestedCallAttributes, RemoveLiquidityNestedProportionalInput, RemoveLiquidityNestedSingleTokenInput, - RemoveLiquidityNestedCallAttributes, } from './types'; -import { NestedPool, PoolKind } from '../types'; -import { TokenAmount } from '../tokenAmount'; -import { Address, PoolType } from '../../types'; -import { BALANCER_RELAYER, ChainId, ZERO_ADDRESS } from '../../utils'; -import { Relayer } from '../relayer'; +import { PoolType } from '@/types'; +import { Relayer } from '@/entities/relayer'; export const getQueryCallsAttributes = ( input: diff --git a/src/entities/removeLiquidityNested/removeLiquidityNestedV2/index.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/index.ts new file mode 100644 index 00000000..98e075f5 --- /dev/null +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/index.ts @@ -0,0 +1,155 @@ +import { encodeFunctionData } from 'viem'; + +import { balancerRelayerAbi } from '../../../abi'; +import { Address, Hex } from '../../../types'; +import { BALANCER_RELAYER, ZERO_ADDRESS } from '../../../utils'; + +import { Relayer } from '../../relayer'; +import { TokenAmount } from '../../tokenAmount'; +import { NestedPoolState } from '../../types'; +import { validateNestedPoolState } from '../../utils'; + +import { encodeCalls } from './encodeCalls'; +import { doRemoveLiquidityNestedQuery } from './doRemoveLiquidityNestedQuery'; +import { getPeekCalls } from './getPeekCalls'; +import { getQueryCallsAttributes } from './getQueryCallsAttributes'; +import { + RemoveLiquidityNestedQueryOutput, + RemoveLiquidityNestedCallInput, + RemoveLiquidityNestedInput, +} from './types'; +import { validateQueryInput, validateBuildCallInput } from './validateInputs'; + +export class RemoveLiquidityNestedV2 { + async query( + input: RemoveLiquidityNestedInput, + nestedPoolState: NestedPoolState, + ): Promise { + const isProportional = validateQueryInput(input, nestedPoolState); + validateNestedPoolState(nestedPoolState); + + const { callsAttributes, bptAmountIn } = getQueryCallsAttributes( + input, + nestedPoolState.pools, + isProportional, + ); + + const encodedCalls = encodeCalls(callsAttributes, isProportional); + + const { peekCalls, tokensOut } = getPeekCalls( + callsAttributes, + isProportional, + ); + + // insert peek calls to get amountsOut + let tokensOutCount = 0; + const tokensOutIndexes: number[] = []; + callsAttributes.forEach((call, i) => { + tokensOut.forEach((tokenOut, j) => { + if ( + call.sortedTokens.some((t) => + t.isSameAddress(tokenOut.address), + ) + ) { + tokensOutCount++; + encodedCalls.splice(i + tokensOutCount, 0, peekCalls[j]); + tokensOutIndexes.push(i + tokensOutCount); + } + }); + }); + + const encodedMulticall = encodeFunctionData({ + abi: balancerRelayerAbi, + functionName: 'vaultActionsQueryMulticall', + args: [encodedCalls], + }); + + const peekedValues = await doRemoveLiquidityNestedQuery( + input.chainId, + input.rpcUrl, + encodedMulticall, + tokensOutIndexes, + ); + + const amountsOut = tokensOut.map((tokenOut, i) => + TokenAmount.fromRawAmount(tokenOut, peekedValues[i]), + ); + + return { + protocolVersion: 2, + callsAttributes, + bptAmountIn, + amountsOut, + isProportional, + chainId: input.chainId, + }; + } + + buildCall(input: RemoveLiquidityNestedCallInput): { + callData: Hex; + to: Address; + minAmountsOut: TokenAmount[]; + } { + validateBuildCallInput(input); + + // apply slippage to amountsOut + const minAmountsOut = input.amountsOut.map((amountOut) => + TokenAmount.fromRawAmount( + amountOut.token, + input.slippage.applyTo(amountOut.amount, -1), + ), + ); + + input.callsAttributes.forEach((call) => { + // update relevant calls with minAmountOut limits in place + minAmountsOut.forEach((minAmountOut, j) => { + const minAmountOutIndex = call.sortedTokens.findIndex((t) => + t.isSameAddress(minAmountOut.token.address), + ); + if (minAmountOutIndex !== -1) { + call.minAmountsOut[minAmountOutIndex] = + minAmountsOut[j].amount; + } + }); + // update wethIsEth flag + call.wethIsEth = !!input.wethIsEth; + // update sender and recipient placeholders + call.sender = + call.sender === ZERO_ADDRESS + ? input.accountAddress + : call.sender; + call.recipient = + call.recipient === ZERO_ADDRESS + ? input.accountAddress + : call.recipient; + }); + + const encodedCalls = encodeCalls( + input.callsAttributes, + input.isProportional, + ); + + // prepend relayer approval if provided + if (input.relayerApprovalSignature !== undefined) { + encodedCalls.unshift( + Relayer.encodeSetRelayerApproval( + BALANCER_RELAYER[input.callsAttributes[0].chainId], + true, + input.relayerApprovalSignature, + ), + ); + } + + const callData = encodeFunctionData({ + abi: balancerRelayerAbi, + functionName: 'multicall', + args: [encodedCalls], + }); + + return { + callData, + to: BALANCER_RELAYER[input.callsAttributes[0].chainId], + minAmountsOut, + }; + } +} diff --git a/src/entities/removeLiquidityNested/types.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/types.ts similarity index 80% rename from src/entities/removeLiquidityNested/types.ts rename to src/entities/removeLiquidityNested/removeLiquidityNestedV2/types.ts index e9e27012..20f81cd3 100644 --- a/src/entities/removeLiquidityNested/types.ts +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/types.ts @@ -1,9 +1,10 @@ -import { Address, Hex, PoolType } from '../../types'; -import { ChainId } from '../../utils'; -import { Slippage } from '../slippage'; -import { Token } from '../token'; -import { TokenAmount } from '../tokenAmount'; -import { PoolKind } from '../types'; +import { Address, Hex } from 'viem'; +import { Slippage } from '@/entities/slippage'; +import { Token } from '@/entities/token'; +import { TokenAmount } from '@/entities/tokenAmount'; +import { PoolKind } from '@/entities/types'; +import { PoolType } from '@/types'; +import { ChainId } from '@/utils'; export type RemoveLiquidityNestedProportionalInput = { bptAmountIn: bigint; @@ -45,6 +46,7 @@ export type RemoveLiquidityNestedCallAttributes = { }; export type RemoveLiquidityNestedQueryOutput = { + protocolVersion: 1 | 2 | 3; callsAttributes: RemoveLiquidityNestedCallAttributes[]; bptAmountIn: TokenAmount; amountsOut: TokenAmount[]; diff --git a/src/entities/removeLiquidityNested/removeLiquidityNestedV2/validateInputs.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/validateInputs.ts new file mode 100644 index 00000000..7bb0cfb9 --- /dev/null +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/validateInputs.ts @@ -0,0 +1,57 @@ +import { NATIVE_ASSETS } from '../../../utils'; +import { Token } from '../../token'; +import { NestedPoolState } from '../../types'; +import { + RemoveLiquidityNestedCallInput, + RemoveLiquidityNestedProportionalInput, + RemoveLiquidityNestedSingleTokenInput, +} from './types'; + +export const validateQueryInput = ( + input: + | RemoveLiquidityNestedProportionalInput + | RemoveLiquidityNestedSingleTokenInput, + nestedPoolState: NestedPoolState, +) => { + const tokenOut = 'tokenOut' in input ? input.tokenOut : undefined; + const isProportional = tokenOut === undefined; + const mainTokens = nestedPoolState.mainTokens.map( + (token) => new Token(input.chainId, token.address, token.decimals), + ); + if (!isProportional) { + validateInputsSingleToken( + input as RemoveLiquidityNestedSingleTokenInput, + mainTokens, + ); + } + + return isProportional; +}; + +const validateInputsSingleToken = ( + input: RemoveLiquidityNestedSingleTokenInput, + mainTokens: Token[], +) => { + const tokenOut = mainTokens.find((t) => t.isSameAddress(input.tokenOut)); + + if (tokenOut === undefined) { + throw new Error( + `Removing liquidity to ${input.tokenOut} requires it to exist within main tokens`, + ); + } +}; + +export const validateBuildCallInput = ( + input: RemoveLiquidityNestedCallInput, +) => { + if ( + input.wethIsEth && + !input.amountsOut.some((a) => + a.token.isSameAddress(NATIVE_ASSETS[input.chainId].wrapped), + ) + ) { + throw new Error( + 'Removing liquidity to native asset requires wrapped native asset to exist within amounts out', + ); + } +}; diff --git a/src/entities/removeLiquidityNested/validateInputs.ts b/src/entities/removeLiquidityNested/validateInputs.ts index 71d1c8ab..5e905bb2 100644 --- a/src/entities/removeLiquidityNested/validateInputs.ts +++ b/src/entities/removeLiquidityNested/validateInputs.ts @@ -1,11 +1,11 @@ -import { NATIVE_ASSETS } from '../../utils'; +import { NATIVE_ASSETS } from '@/utils'; import { Token } from '../token'; import { NestedPoolState } from '../types'; import { RemoveLiquidityNestedCallInput, RemoveLiquidityNestedProportionalInput, RemoveLiquidityNestedSingleTokenInput, -} from './types'; +} from './removeLiquidityNestedV2/types'; export const validateQueryInput = ( input: diff --git a/test/lib/utils/addLiquidityNestedHelper.ts b/test/lib/utils/addLiquidityNestedHelper.ts index 843b13ed..1d5947b4 100644 --- a/test/lib/utils/addLiquidityNestedHelper.ts +++ b/test/lib/utils/addLiquidityNestedHelper.ts @@ -1,15 +1,10 @@ import { Address, TransactionReceipt } from 'viem'; -import { - AddLiquidityNestedInput, - Relayer, - Slippage, - TokenAmount, - replaceWrapped, -} from '@/entities'; +import { Relayer, Slippage, TokenAmount, replaceWrapped } from '@/entities'; import { BALANCER_RELAYER, NATIVE_ASSETS } from '@/utils'; import { AddLiquidityNestedTxInput } from './types'; import { sendTransactionGetBalances } from './helper'; import { AddLiquidityNested } from '@/entities/addLiquidityNested'; +import { AddLiquidityNestedInput } from '@/entities/addLiquidityNested/addLiquidityNestedV2/types'; export const assertResults = ( transactionReceipt: TransactionReceipt, diff --git a/test/v2/removeLiquidityNested/removeLiquidityNested.integration.test.ts b/test/v2/removeLiquidityNested/removeLiquidityNested.integration.test.ts index d4105119..68734904 100644 --- a/test/v2/removeLiquidityNested/removeLiquidityNested.integration.test.ts +++ b/test/v2/removeLiquidityNested/removeLiquidityNested.integration.test.ts @@ -21,7 +21,6 @@ import { NestedPoolState, PoolType, Relayer, - RemoveLiquidityNested, RemoveLiquidityNestedProportionalInput, RemoveLiquidityNestedSingleTokenInput, replaceWrapped, @@ -32,6 +31,7 @@ import { import { ANVIL_NETWORKS, startFork } from 'test/anvil/anvil-global-setup'; import { forkSetup, sendTransactionGetBalances } from 'test/lib/utils'; +import { RemoveLiquidityNested } from '@/entities/removeLiquidityNested'; type TxInput = { poolId: Hex; @@ -316,6 +316,7 @@ class MockApi { ) throw Error(); return { + protocolVersion: 2, pools: [ { id: '0x08775ccb6674d6bdceb0797c364c2653ed84f3840002000000000000000004f0', diff --git a/test/v3/removeLiquidityNestedV3.integration.test.ts/removeLiquidityNestedV3.integration.test.ts b/test/v3/removeLiquidityNestedV3.integration.test.ts/removeLiquidityNestedV3.integration.test.ts new file mode 100644 index 00000000..b70de744 --- /dev/null +++ b/test/v3/removeLiquidityNestedV3.integration.test.ts/removeLiquidityNestedV3.integration.test.ts @@ -0,0 +1,414 @@ +// pnpm test -- removeLiquidityNestedV3.integration.test.ts +import dotenv from 'dotenv'; +dotenv.config(); + +import { + createTestClient, + http, + parseUnits, + publicActions, + TestActions, + TransactionReceipt, + walletActions, +} from 'viem'; + +import { + Address, + BALANCER_RELAYER, + ChainId, + CHAINS, + Hex, + NestedPoolState, + PoolType, + Relayer, + RemoveLiquidityNestedProportionalInput, + RemoveLiquidityNestedSingleTokenInput, + replaceWrapped, + Slippage, + TokenAmount, + PublicWalletClient, +} from 'src'; + +import { ANVIL_NETWORKS, startFork } from 'test/anvil/anvil-global-setup'; +import { forkSetup, sendTransactionGetBalances } from 'test/lib/utils'; +import { RemoveLiquidityNested } from '@/entities/removeLiquidityNested'; + +type TxInput = { + poolId: Hex; + amountIn: bigint; + chainId: ChainId; + rpcUrl: string; + testAddress: Address; + client: PublicWalletClient & TestActions; + tokenOut?: Address; + wethIsEth?: boolean; +}; + +describe.skip('remove liquidity nested test', () => { + let chainId: ChainId; + let rpcUrl: string; + let client: PublicWalletClient & TestActions; + let poolId: Hex; + let testAddress: Address; + + beforeAll(async () => { + // setup chain and test client + chainId = ChainId.MAINNET; + ({ rpcUrl } = await startFork(ANVIL_NETWORKS.MAINNET)); + + client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + testAddress = (await client.getAddresses())[0]; + + poolId = + '0x08775ccb6674d6bdceb0797c364c2653ed84f3840002000000000000000004f0'; // WETH-3POOL-BPT + }); + + beforeEach(async () => { + await forkSetup( + client, + testAddress, + ['0x08775ccb6674d6bdceb0797c364c2653ed84f384'], + [0], + [parseUnits('1000', 18)], + ); + }); + + test('proportional', async () => { + const amountIn = parseUnits('1', 18); + + const { + transactionReceipt, + expectedDeltas, + balanceDeltas, + amountsOut, + slippage, + minAmountsOut, + } = await doTransaction({ + poolId, + amountIn, + chainId, + rpcUrl, + testAddress, + client, + }); + + assertResults( + transactionReceipt, + expectedDeltas, + balanceDeltas, + amountsOut, + slippage, + minAmountsOut, + ); + }); + + test('proportional - native asset', async () => { + const amountIn = parseUnits('1', 18); + const wethIsEth = true; + + const { + transactionReceipt, + expectedDeltas, + balanceDeltas, + amountsOut, + slippage, + minAmountsOut, + } = await doTransaction({ + poolId, + amountIn, + chainId, + rpcUrl, + testAddress, + client, + wethIsEth, + }); + + assertResults( + transactionReceipt, + expectedDeltas, + balanceDeltas, + amountsOut, + slippage, + minAmountsOut, + ); + }); + + test('single token - token index > bptIndex', async () => { + const amountIn = parseUnits('1', 18); + const tokenOut = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; // USDC + + const { + transactionReceipt, + expectedDeltas, + balanceDeltas, + amountsOut, + slippage, + minAmountsOut, + } = await doTransaction({ + poolId, + amountIn, + chainId, + rpcUrl, + testAddress, + client, + tokenOut, + }); + + assertResults( + transactionReceipt, + expectedDeltas, + balanceDeltas, + amountsOut, + slippage, + minAmountsOut, + ); + }); + + test('single token - native asset', async () => { + const amountIn = parseUnits('1', 18); + const tokenOut = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; // WETH + const wethIsEth = true; + + const { + transactionReceipt, + expectedDeltas, + balanceDeltas, + amountsOut, + slippage, + minAmountsOut, + } = await doTransaction({ + poolId, + amountIn, + chainId, + rpcUrl, + testAddress, + client, + tokenOut, + wethIsEth, + }); + + assertResults( + transactionReceipt, + expectedDeltas, + balanceDeltas, + amountsOut, + slippage, + minAmountsOut, + ); + }); + + test('single token - native asset - invalid input', async () => { + const amountIn = parseUnits('1', 18); + const tokenOut = '0x6b175474e89094c44da98b954eedeac495271d0f'; // DAI + const wethIsEth = true; + + await expect( + doTransaction({ + poolId, + amountIn, + chainId, + rpcUrl, + testAddress, + client, + tokenOut, + wethIsEth, + }), + ).rejects.toThrow( + 'Removing liquidity to native asset requires wrapped native asset to exist within amounts out', + ); + }); +}); + +export const doTransaction = async ({ + poolId, + amountIn, + chainId, + rpcUrl, + testAddress, + client, + tokenOut, + wethIsEth = false, +}: TxInput) => { + // setup mock api + const api = new MockApi(); + // get pool state from api + const nestedPoolFromApi = await api.getNestedPool(poolId); + + // setup remove liquidity helper + const removeLiquidityNested = new RemoveLiquidityNested(); + const removeLiquidityInput: + | RemoveLiquidityNestedProportionalInput + | RemoveLiquidityNestedSingleTokenInput = { + bptAmountIn: amountIn, + chainId, + rpcUrl, + tokenOut, + }; + const queryOutput = await removeLiquidityNested.query( + removeLiquidityInput, + nestedPoolFromApi, + ); + + // build remove liquidity call with expected minBpOut based on slippage + const slippage = Slippage.fromPercentage('1'); // 1% + + const signature = await Relayer.signRelayerApproval( + BALANCER_RELAYER[chainId], + testAddress, + client, + ); + + const { callData, to, minAmountsOut } = removeLiquidityNested.buildCall({ + ...queryOutput, + slippage, + accountAddress: testAddress, + relayerApprovalSignature: signature, + wethIsEth, + }); + + let tokensOut = minAmountsOut.map((a) => a.token); + if (wethIsEth) { + tokensOut = replaceWrapped(tokensOut, chainId); + } + + // send remove liquidity transaction and check balance changes + const { transactionReceipt, balanceDeltas } = + await sendTransactionGetBalances( + [ + queryOutput.bptAmountIn.token.address, + ...tokensOut.map((t) => t.address), + ], + client, + testAddress, + to, + callData, + ); + + const expectedDeltas = [ + queryOutput.bptAmountIn.amount, + ...queryOutput.amountsOut.map((amountOut) => amountOut.amount), + ]; + + return { + transactionReceipt, + expectedDeltas, + balanceDeltas, + amountsOut: queryOutput.amountsOut, + slippage, + minAmountsOut, + }; +}; + +/*********************** Mock To Represent API Requirements **********************/ + +class MockApi { + public async getNestedPool(poolId: Hex): Promise { + if ( + poolId !== + '0x08775ccb6674d6bdceb0797c364c2653ed84f3840002000000000000000004f0' + ) + throw Error(); + return { + protocolVersion: 3, + pools: [ + { + id: '0x08775ccb6674d6bdceb0797c364c2653ed84f3840002000000000000000004f0', + address: '0x08775ccb6674d6bdceb0797c364c2653ed84f384', + type: PoolType.Weighted, + level: 1, + tokens: [ + { + address: + '0x79c58f70905f734641735bc61e45c19dd9ad60bc', // 3POOL-BPT + decimals: 18, + index: 0, + }, + { + address: + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // WETH + decimals: 18, + index: 1, + }, + ], + }, + { + id: '0x79c58f70905f734641735bc61e45c19dd9ad60bc0000000000000000000004e7', + address: '0x79c58f70905f734641735bc61e45c19dd9ad60bc', + type: PoolType.ComposableStable, + level: 0, + tokens: [ + { + address: + '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI + decimals: 18, + index: 0, + }, + { + address: + '0x79c58f70905f734641735bc61e45c19dd9ad60bc', // 3POOL-BPT + decimals: 18, + index: 1, + }, + { + address: + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC + decimals: 6, + index: 2, + }, + { + address: + '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT + decimals: 6, + index: 3, + }, + ], + }, + ], + mainTokens: [ + { + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // WETH + decimals: 18, + }, + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI + decimals: 18, + }, + { + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC + decimals: 6, + }, + { + address: '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT + decimals: 6, + }, + ], + }; + } +} + +function assertResults( + transactionReceipt: TransactionReceipt, + expectedDeltas: bigint[], + balanceDeltas: bigint[], + amountsOut: TokenAmount[], + slippage: Slippage, + minAmountsOut: TokenAmount[], +) { + expect(transactionReceipt.status).to.eq('success'); + amountsOut.map((amountOut) => expect(amountOut.amount > 0n).to.be.true); + expect(expectedDeltas).to.deep.eq(balanceDeltas); + const expectedMinAmountsOut = amountsOut.map((amountOut) => + slippage.applyTo(amountOut.amount, -1), + ); + expect(expectedMinAmountsOut).to.deep.eq( + minAmountsOut.map((a) => a.amount), + ); +} +/******************************************************************************/ From 5060deef9edd01ce20bdb2aa36fdf457ba05b165 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 23 Oct 2024 10:45:25 +0100 Subject: [PATCH 03/10] chore: AddLiquidityNested type tidy. --- .../addLiquidityNestedV2/encodeCalls.ts | 2 +- .../getQueryCallsAttributes.ts | 2 +- .../addLiquidityNestedV2/index.ts | 23 ++++++++----------- .../addLiquidityNestedV3/index.ts | 22 ++++++++++++++++++ src/entities/addLiquidityNested/index.ts | 13 ++++------- .../{addLiquidityNestedV2 => }/types.ts | 20 +++++++++++----- src/entities/priceImpact/index.ts | 2 +- 7 files changed, 52 insertions(+), 32 deletions(-) create mode 100644 src/entities/addLiquidityNested/addLiquidityNestedV3/index.ts rename src/entities/addLiquidityNested/{addLiquidityNestedV2 => }/types.ts (69%) diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV2/encodeCalls.ts b/src/entities/addLiquidityNested/addLiquidityNestedV2/encodeCalls.ts index 96193ca8..a4a07ebd 100644 --- a/src/entities/addLiquidityNested/addLiquidityNestedV2/encodeCalls.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV2/encodeCalls.ts @@ -4,7 +4,7 @@ import { encodeFunctionData, Hex } from 'viem'; import { TokenAmount } from '../../tokenAmount'; import { getValue } from '../../utils/getValue'; import { replaceWrapped } from '@/entities/utils'; -import { AddLiquidityNestedCallAttributes } from './types'; +import { AddLiquidityNestedCallAttributes } from '../types'; import { WeightedEncoder } from '@/entities/encoders'; import { PoolType } from '@/types'; diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV2/getQueryCallsAttributes.ts b/src/entities/addLiquidityNested/addLiquidityNestedV2/getQueryCallsAttributes.ts index 95ec2d91..d3bc42c2 100644 --- a/src/entities/addLiquidityNested/addLiquidityNestedV2/getQueryCallsAttributes.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV2/getQueryCallsAttributes.ts @@ -9,7 +9,7 @@ import { import { AddLiquidityNestedCallAttributes, AddLiquidityNestedInput, -} from './types'; +} from '../types'; import { NestedPool, PoolKind } from '@/entities/types'; import { Relayer } from '@/entities/relayer'; diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV2/index.ts b/src/entities/addLiquidityNested/addLiquidityNestedV2/index.ts index c24e90ba..28617e93 100644 --- a/src/entities/addLiquidityNested/addLiquidityNestedV2/index.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV2/index.ts @@ -1,21 +1,20 @@ import { encodeFunctionData } from 'viem'; -import { Address, Hex } from '../../../types'; import { Token } from '../../token'; import { BALANCER_RELAYER, ZERO_ADDRESS } from '../../../utils'; import { Relayer } from '../../relayer'; import { encodeCalls } from './encodeCalls'; import { TokenAmount } from '../../tokenAmount'; import { balancerRelayerAbi } from '../../../abi'; -import { - AddLiquidityNestedInput, - AddLiquidityNestedQueryOutput, - AddLiquidityNestedCallInput, -} from './types'; import { doAddLiquidityNestedQuery } from './doAddLiquidityNestedQuery'; import { getQueryCallsAttributes } from './getQueryCallsAttributes'; import { validateBuildCallInput, validateQueryInput } from './validateInputs'; import { NestedPoolState } from '../../types'; -import { validateNestedPoolState } from '../../utils'; +import { + AddLiquidityNestedBuildCallOutput, + AddLiquidityNestedInput, + AddLiquidityNestedQueryOutput, + AddLiquidityNestedCallInput, +} from '../types'; export class AddLiquidityNestedV2 { async query( @@ -23,7 +22,6 @@ export class AddLiquidityNestedV2 { nestedPoolState: NestedPoolState, ): Promise { const amountsIn = validateQueryInput(input, nestedPoolState); - validateNestedPoolState(nestedPoolState); const callsAttributes = getQueryCallsAttributes( input, @@ -60,12 +58,9 @@ export class AddLiquidityNestedV2 { return { callsAttributes, amountsIn, bptOut, protocolVersion: 2 }; } - buildCall(input: AddLiquidityNestedCallInput): { - callData: Hex; - to: Address; - value: bigint | undefined; - minBptOut: bigint; - } { + buildCall( + input: AddLiquidityNestedCallInput, + ): AddLiquidityNestedBuildCallOutput { validateBuildCallInput(input); // apply slippage to bptOut const minBptOut = input.slippage.applyTo(input.bptOut.amount, -1); diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV3/index.ts b/src/entities/addLiquidityNested/addLiquidityNestedV3/index.ts new file mode 100644 index 00000000..896c7de3 --- /dev/null +++ b/src/entities/addLiquidityNested/addLiquidityNestedV3/index.ts @@ -0,0 +1,22 @@ +import { NestedPoolState } from '@/entities/types'; +import { + AddLiquidityNestedBuildCallOutput, + AddLiquidityNestedCallInput, + AddLiquidityNestedInput, + AddLiquidityNestedQueryOutput, +} from '../types'; + +export class AddLiquidityNestedV3 { + async query( + _input: AddLiquidityNestedInput, + _nestedPoolState: NestedPoolState, + ): Promise { + return {} as AddLiquidityNestedQueryOutput; + } + + buildCall( + _input: AddLiquidityNestedCallInput, + ): AddLiquidityNestedBuildCallOutput { + return {} as AddLiquidityNestedBuildCallOutput; + } +} diff --git a/src/entities/addLiquidityNested/index.ts b/src/entities/addLiquidityNested/index.ts index 69802c3c..943033c3 100644 --- a/src/entities/addLiquidityNested/index.ts +++ b/src/entities/addLiquidityNested/index.ts @@ -1,20 +1,18 @@ -import { Address, Hex } from '../../types'; import { AddLiquidityNestedInput, AddLiquidityNestedQueryOutput, AddLiquidityNestedCallInput, } from './addLiquidityNestedV2/types'; -// import { validateQueryInput } from './addLiquidityNestedV2/validateInputs'; import { NestedPoolState } from '../types'; import { validateNestedPoolState } from '../utils'; import { AddLiquidityNestedV2 } from './addLiquidityNestedV2'; +import { AddLiquidityNestedBuildCallOutput } from './types'; export class AddLiquidityNested { async query( input: AddLiquidityNestedInput, nestedPoolState: NestedPoolState, ): Promise { - // const amountsIn = validateQueryInput(input, nestedPoolState); validateNestedPoolState(nestedPoolState); switch (nestedPoolState.protocolVersion) { case 1: { @@ -34,12 +32,9 @@ export class AddLiquidityNested { } } - buildCall(input: AddLiquidityNestedCallInput): { - callData: Hex; - to: Address; - value: bigint | undefined; - minBptOut: bigint; - } { + buildCall( + input: AddLiquidityNestedCallInput, + ): AddLiquidityNestedBuildCallOutput { switch (input.protocolVersion) { case 1: { throw new Error( diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV2/types.ts b/src/entities/addLiquidityNested/types.ts similarity index 69% rename from src/entities/addLiquidityNested/addLiquidityNestedV2/types.ts rename to src/entities/addLiquidityNested/types.ts index ba5af10b..8fb04048 100644 --- a/src/entities/addLiquidityNested/addLiquidityNestedV2/types.ts +++ b/src/entities/addLiquidityNested/types.ts @@ -1,9 +1,10 @@ -import { Address, Hex, InputAmount, PoolType } from '../../../types'; -import { ChainId } from '../../../utils'; -import { Slippage } from '../../slippage'; -import { Token } from '../../token'; -import { TokenAmount } from '../../tokenAmount'; -import { PoolKind } from '../../types'; +import { Address, Hex } from 'viem'; +import { InputAmount, PoolType } from '../../types'; +import { ChainId } from '../../utils'; +import { Slippage } from '../slippage'; +import { Token } from '../token'; +import { TokenAmount } from '../tokenAmount'; +import { PoolKind } from '../types'; export type AddLiquidityNestedInput = { amountsIn: InputAmount[]; @@ -44,3 +45,10 @@ export type AddLiquidityNestedCallInput = AddLiquidityNestedQueryOutput & { relayerApprovalSignature?: Hex; wethIsEth?: boolean; }; + +export type AddLiquidityNestedBuildCallOutput = { + callData: Hex; + to: Address; + value: bigint; + minBptOut: bigint; +}; diff --git a/src/entities/priceImpact/index.ts b/src/entities/priceImpact/index.ts index 0c70751b..c1fa2079 100644 --- a/src/entities/priceImpact/index.ts +++ b/src/entities/priceImpact/index.ts @@ -22,7 +22,7 @@ import { Token } from '../token'; import { TokenAmount } from '../tokenAmount'; import { NestedPoolState, PoolState } from '../types'; import { getSortedTokens } from '../utils'; -import { AddLiquidityNestedInput } from '../addLiquidityNested/addLiquidityNestedV2/types'; +import { AddLiquidityNestedInput } from '../addLiquidityNested/types'; import { AddLiquidityNested } from '../addLiquidityNested'; export class PriceImpact { From 2f353681fa0c3aba39bc565d27eec5ad8797028c Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 23 Oct 2024 16:38:53 +0100 Subject: [PATCH 04/10] feat: Add support for V3 nested add, direct permit2 approval only. --- .../addLiquidityNestedV2/encodeCalls.ts | 2 +- .../getQueryCallsAttributes.ts | 6 +- .../addLiquidityNestedV2/index.ts | 10 +- .../addLiquidityNestedV2/types.ts | 47 +++ .../addLiquidityNestedV2/validateInputs.ts | 5 +- .../addLiquidityNestedV3/index.ts | 120 +++++- .../addLiquidityNestedV3/types.ts | 26 ++ src/entities/addLiquidityNested/index.ts | 28 +- src/entities/addLiquidityNested/types.ts | 62 +-- src/entities/index.ts | 3 + test/lib/utils/addresses.ts | 7 + .../addLiquidityNestedV3.integration.test.ts | 395 ++++++++---------- 12 files changed, 410 insertions(+), 301 deletions(-) create mode 100644 src/entities/addLiquidityNested/addLiquidityNestedV2/types.ts create mode 100644 src/entities/addLiquidityNested/addLiquidityNestedV3/types.ts diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV2/encodeCalls.ts b/src/entities/addLiquidityNested/addLiquidityNestedV2/encodeCalls.ts index a4a07ebd..96193ca8 100644 --- a/src/entities/addLiquidityNested/addLiquidityNestedV2/encodeCalls.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV2/encodeCalls.ts @@ -4,7 +4,7 @@ import { encodeFunctionData, Hex } from 'viem'; import { TokenAmount } from '../../tokenAmount'; import { getValue } from '../../utils/getValue'; import { replaceWrapped } from '@/entities/utils'; -import { AddLiquidityNestedCallAttributes } from '../types'; +import { AddLiquidityNestedCallAttributes } from './types'; import { WeightedEncoder } from '@/entities/encoders'; import { PoolType } from '@/types'; diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV2/getQueryCallsAttributes.ts b/src/entities/addLiquidityNested/addLiquidityNestedV2/getQueryCallsAttributes.ts index d3bc42c2..0af5086b 100644 --- a/src/entities/addLiquidityNested/addLiquidityNestedV2/getQueryCallsAttributes.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV2/getQueryCallsAttributes.ts @@ -8,13 +8,13 @@ import { } from '@/utils'; import { AddLiquidityNestedCallAttributes, - AddLiquidityNestedInput, -} from '../types'; + AddLiquidityNestedInputV2, +} from './types'; import { NestedPool, PoolKind } from '@/entities/types'; import { Relayer } from '@/entities/relayer'; export const getQueryCallsAttributes = ( - { amountsIn, chainId, fromInternalBalance }: AddLiquidityNestedInput, + { amountsIn, chainId, fromInternalBalance }: AddLiquidityNestedInputV2, pools: NestedPool[], ): AddLiquidityNestedCallAttributes[] => { /** diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV2/index.ts b/src/entities/addLiquidityNested/addLiquidityNestedV2/index.ts index 28617e93..a8c87472 100644 --- a/src/entities/addLiquidityNested/addLiquidityNestedV2/index.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV2/index.ts @@ -12,15 +12,17 @@ import { NestedPoolState } from '../../types'; import { AddLiquidityNestedBuildCallOutput, AddLiquidityNestedInput, - AddLiquidityNestedQueryOutput, - AddLiquidityNestedCallInput, } from '../types'; +import { + AddLiquidityNestedCallInputV2, + AddLiquidityNestedQueryOutputV2, +} from './types'; export class AddLiquidityNestedV2 { async query( input: AddLiquidityNestedInput, nestedPoolState: NestedPoolState, - ): Promise { + ): Promise { const amountsIn = validateQueryInput(input, nestedPoolState); const callsAttributes = getQueryCallsAttributes( @@ -59,7 +61,7 @@ export class AddLiquidityNestedV2 { } buildCall( - input: AddLiquidityNestedCallInput, + input: AddLiquidityNestedCallInputV2, ): AddLiquidityNestedBuildCallOutput { validateBuildCallInput(input); // apply slippage to bptOut diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV2/types.ts b/src/entities/addLiquidityNested/addLiquidityNestedV2/types.ts new file mode 100644 index 00000000..3a2f8d9a --- /dev/null +++ b/src/entities/addLiquidityNested/addLiquidityNestedV2/types.ts @@ -0,0 +1,47 @@ +import { Address, Hex } from 'viem'; +import { InputAmount, PoolType } from '../../../types'; +import { ChainId } from '../../../utils'; +import { Token } from '../../token'; +import { TokenAmount } from '../../tokenAmount'; +import { PoolKind } from '../../types'; +import { Slippage } from '@/entities/slippage'; + +export type AddLiquidityNestedInputV2 = { + amountsIn: InputAmount[]; + chainId: ChainId; + rpcUrl: string; + fromInternalBalance?: boolean; +}; + +export type AddLiquidityNestedCallAttributes = { + chainId: ChainId; + wethIsEth?: boolean; + sortedTokens: Token[]; + poolId: Hex; + poolAddress: Address; + poolType: PoolType; + kind: PoolKind; + sender: Address; + recipient: Address; + maxAmountsIn: { + amount: bigint; + isRef: boolean; + }[]; + minBptOut: bigint; + fromInternalBalance: boolean; + outputReference: bigint; +}; + +export type AddLiquidityNestedQueryOutputV2 = { + callsAttributes: AddLiquidityNestedCallAttributes[]; + amountsIn: TokenAmount[]; + bptOut: TokenAmount; + protocolVersion: 2; +}; + +export type AddLiquidityNestedCallInputV2 = AddLiquidityNestedQueryOutputV2 & { + slippage: Slippage; + accountAddress: Address; + relayerApprovalSignature?: Hex; + wethIsEth?: boolean; +}; diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV2/validateInputs.ts b/src/entities/addLiquidityNested/addLiquidityNestedV2/validateInputs.ts index bd81af47..235f520d 100644 --- a/src/entities/addLiquidityNested/addLiquidityNestedV2/validateInputs.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV2/validateInputs.ts @@ -2,7 +2,8 @@ import { NATIVE_ASSETS } from '../../../utils'; import { Token } from '../../token'; import { TokenAmount } from '../../tokenAmount'; import { NestedPoolState } from '../../types'; -import { AddLiquidityNestedCallInput, AddLiquidityNestedInput } from './types'; +import { AddLiquidityNestedInput } from '../types'; +import { AddLiquidityNestedCallInputV2 } from './types'; export const validateQueryInput = ( input: AddLiquidityNestedInput, @@ -26,7 +27,7 @@ export const validateQueryInput = ( }; export const validateBuildCallInput = ( - input: AddLiquidityNestedCallInput, + input: AddLiquidityNestedCallInputV2, ): void => { const chainId = input.callsAttributes[0].chainId; if (input.wethIsEth) { diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV3/index.ts b/src/entities/addLiquidityNested/addLiquidityNestedV3/index.ts index 896c7de3..27cba989 100644 --- a/src/entities/addLiquidityNested/addLiquidityNestedV3/index.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV3/index.ts @@ -1,22 +1,126 @@ +import { + Address, + createPublicClient, + encodeFunctionData, + Hex, + http, + zeroAddress, +} from 'viem'; import { NestedPoolState } from '@/entities/types'; import { AddLiquidityNestedBuildCallOutput, - AddLiquidityNestedCallInput, AddLiquidityNestedInput, - AddLiquidityNestedQueryOutput, } from '../types'; +import { Token } from '@/entities/token'; +import { getAmounts } from '@/entities/utils'; +import { TokenAmount } from '@/entities/tokenAmount'; +import { BALANCER_COMPOSITE_LIQUIDITY_ROUTER, CHAINS } from '@/utils'; +import { + balancerCompositeLiquidityRouterAbi, + permit2Abi, + vaultExtensionAbi_V3, + vaultV3Abi, +} from '@/abi'; +import { + AddLiquidityNestedCallInputV3, + AddLiquidityNestedInputV3, + AddLiquidityNestedQueryOutputV3, +} from './types'; +import { validateQueryInput } from '../addLiquidityNestedV2/validateInputs'; export class AddLiquidityNestedV3 { async query( - _input: AddLiquidityNestedInput, - _nestedPoolState: NestedPoolState, - ): Promise { - return {} as AddLiquidityNestedQueryOutput; + input: AddLiquidityNestedInputV3, + nestedPoolState: NestedPoolState, + ): Promise { + validateQueryInput(input, nestedPoolState); + + // Address of the highest level pool (which contains BPTs of other pools), i.e. the pool we wish to join + const parentPool = nestedPoolState.pools.reduce((max, curr) => + curr.level > max.level ? curr : max, + ); + // query function input, `tokensIn` array, must have all tokens from child pools + // and all tokens that are not BPTs from the nested pool (parent pool). + const mainTokens = nestedPoolState.mainTokens.map( + (t) => new Token(input.chainId, t.address, t.decimals), + ); + // This will add 0 amount for any tokensIn the user hasn't included + const maxAmountsIn = getAmounts(mainTokens, input.amountsIn, 0n); + + // Query the router to get the onchain amount + // Note - tokens do not have to be sorted, user preference is fine + const bptAmountOut = await this.doQueryAddLiquidityUnbalancedNestedPool( + input, + parentPool.address, + nestedPoolState.mainTokens.map((t) => t.address), + maxAmountsIn, + input.sender ?? zeroAddress, + input.userData ?? '0x', + ); + + const bptToken = new Token(input.chainId, parentPool.address, 18); + + return { + parentPool: parentPool.address, + chainId: input.chainId, + amountsIn: mainTokens.map((t, i) => + TokenAmount.fromRawAmount(t, maxAmountsIn[i]), + ), + bptOut: TokenAmount.fromRawAmount(bptToken, bptAmountOut), + protocolVersion: 3, + userData: input.userData ?? '0x', + }; } buildCall( - _input: AddLiquidityNestedCallInput, + input: AddLiquidityNestedCallInputV3, ): AddLiquidityNestedBuildCallOutput { - return {} as AddLiquidityNestedBuildCallOutput; + // validateBuildCallInput(input); TODO - Add this like V2 once weth/native is allowed + // apply slippage to bptOut + const minBptOut = input.slippage.applyTo(input.bptOut.amount, -1); + const callData = encodeFunctionData({ + abi: balancerCompositeLiquidityRouterAbi, + functionName: 'addLiquidityUnbalancedNestedPool', + args: [ + input.parentPool, + input.amountsIn.map((a) => a.token.address), + input.amountsIn.map((a) => a.amount), + minBptOut, + input.userData, + ], + }); + return { + callData, + to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[input.chainId], + value: 0n, // TODO defaulting to 0 until router has payable + minBptOut, + }; } + + private doQueryAddLiquidityUnbalancedNestedPool = async ( + { rpcUrl, chainId }: AddLiquidityNestedInput, + parentPool: Address, + tokensIn: Address[], + maxAmountsIn: bigint[], + _sender: Address, + userData: Hex, + ) => { + const client = createPublicClient({ + transport: http(rpcUrl), + chain: CHAINS[chainId], + }); + + const { result: bptAmountOut } = await client.simulateContract({ + address: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + abi: [ + ...balancerCompositeLiquidityRouterAbi, + ...vaultV3Abi, + ...vaultExtensionAbi_V3, + ...permit2Abi, + ], + functionName: 'queryAddLiquidityUnbalancedNestedPool', + args: [parentPool, tokensIn, maxAmountsIn, /*sender,*/ userData], + }); + return bptAmountOut; + }; } diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV3/types.ts b/src/entities/addLiquidityNested/addLiquidityNestedV3/types.ts new file mode 100644 index 00000000..54d5d28c --- /dev/null +++ b/src/entities/addLiquidityNested/addLiquidityNestedV3/types.ts @@ -0,0 +1,26 @@ +import { Slippage } from '@/entities/slippage'; +import { TokenAmount } from '@/entities/tokenAmount'; +import { InputAmount } from '@/types'; +import { ChainId } from '@/utils'; +import { Address, Hex } from 'viem'; + +export type AddLiquidityNestedInputV3 = { + amountsIn: InputAmount[]; + chainId: ChainId; + rpcUrl: string; + sender?: Address; + userData?: Hex; +}; + +export type AddLiquidityNestedQueryOutputV3 = { + amountsIn: TokenAmount[]; + bptOut: TokenAmount; + protocolVersion: 3; + parentPool: Address; + userData: Hex; + chainId: ChainId; +}; + +export type AddLiquidityNestedCallInputV3 = AddLiquidityNestedQueryOutputV3 & { + slippage: Slippage; +}; diff --git a/src/entities/addLiquidityNested/index.ts b/src/entities/addLiquidityNested/index.ts index 943033c3..49ea767c 100644 --- a/src/entities/addLiquidityNested/index.ts +++ b/src/entities/addLiquidityNested/index.ts @@ -1,12 +1,13 @@ -import { - AddLiquidityNestedInput, - AddLiquidityNestedQueryOutput, - AddLiquidityNestedCallInput, -} from './addLiquidityNestedV2/types'; import { NestedPoolState } from '../types'; import { validateNestedPoolState } from '../utils'; import { AddLiquidityNestedV2 } from './addLiquidityNestedV2'; -import { AddLiquidityNestedBuildCallOutput } from './types'; +import { + AddLiquidityNestedBuildCallOutput, + AddLiquidityNestedCallInput, + AddLiquidityNestedInput, + AddLiquidityNestedQueryOutput, +} from './types'; +import { AddLiquidityNestedV3 } from './addLiquidityNestedV3'; export class AddLiquidityNested { async query( @@ -25,9 +26,8 @@ export class AddLiquidityNested { return addLiquidity.query(input, nestedPoolState); } case 3: { - throw new Error( - 'AddLiquidityNested not supported for ProtocolVersion 3.', - ); + const addLiquidity = new AddLiquidityNestedV3(); + return addLiquidity.query(input, nestedPoolState); } } } @@ -36,19 +36,13 @@ export class AddLiquidityNested { input: AddLiquidityNestedCallInput, ): AddLiquidityNestedBuildCallOutput { switch (input.protocolVersion) { - case 1: { - throw new Error( - 'AddLiquidityNested not supported for ProtocolVersion 1.', - ); - } case 2: { const addLiquidity = new AddLiquidityNestedV2(); return addLiquidity.buildCall(input); } case 3: { - throw new Error( - 'AddLiquidityNested not supported for ProtocolVersion 3.', - ); + const addLiquidity = new AddLiquidityNestedV3(); + return addLiquidity.buildCall(input); } } } diff --git a/src/entities/addLiquidityNested/types.ts b/src/entities/addLiquidityNested/types.ts index 8fb04048..e72baba8 100644 --- a/src/entities/addLiquidityNested/types.ts +++ b/src/entities/addLiquidityNested/types.ts @@ -1,50 +1,26 @@ import { Address, Hex } from 'viem'; -import { InputAmount, PoolType } from '../../types'; -import { ChainId } from '../../utils'; -import { Slippage } from '../slippage'; -import { Token } from '../token'; -import { TokenAmount } from '../tokenAmount'; -import { PoolKind } from '../types'; +import { + AddLiquidityNestedCallInputV2, + AddLiquidityNestedQueryOutputV2, + AddLiquidityNestedInputV2, +} from './addLiquidityNestedV2/types'; +import { + AddLiquidityNestedCallInputV3, + AddLiquidityNestedInputV3, + AddLiquidityNestedQueryOutputV3, +} from './addLiquidityNestedV3/types'; -export type AddLiquidityNestedInput = { - amountsIn: InputAmount[]; - chainId: ChainId; - rpcUrl: string; - fromInternalBalance?: boolean; -}; - -export type AddLiquidityNestedCallAttributes = { - chainId: ChainId; - wethIsEth?: boolean; - sortedTokens: Token[]; - poolId: Hex; - poolAddress: Address; - poolType: PoolType; - kind: PoolKind; - sender: Address; - recipient: Address; - maxAmountsIn: { - amount: bigint; - isRef: boolean; - }[]; - minBptOut: bigint; - fromInternalBalance: boolean; - outputReference: bigint; -}; +export type AddLiquidityNestedInput = + | AddLiquidityNestedInputV2 + | AddLiquidityNestedInputV3; -export type AddLiquidityNestedQueryOutput = { - callsAttributes: AddLiquidityNestedCallAttributes[]; - amountsIn: TokenAmount[]; - bptOut: TokenAmount; - protocolVersion: 1 | 2 | 3; -}; +export type AddLiquidityNestedQueryOutput = + | AddLiquidityNestedQueryOutputV2 + | AddLiquidityNestedQueryOutputV3; -export type AddLiquidityNestedCallInput = AddLiquidityNestedQueryOutput & { - slippage: Slippage; - accountAddress: Address; - relayerApprovalSignature?: Hex; - wethIsEth?: boolean; -}; +export type AddLiquidityNestedCallInput = + | AddLiquidityNestedCallInputV2 + | AddLiquidityNestedCallInputV3; export type AddLiquidityNestedBuildCallOutput = { callData: Hex; diff --git a/src/entities/index.ts b/src/entities/index.ts index a478a11d..5d66fae3 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -1,5 +1,8 @@ export * from './addLiquidity'; export * from './addLiquidity/types'; +export * from './addLiquidityNested'; +export * from './addLiquidityNested/types'; +export * from './addLiquidityNested/addLiquidityNestedV3/types'; export * from './createPool'; export * from './encoders'; export * from './initPool'; diff --git a/test/lib/utils/addresses.ts b/test/lib/utils/addresses.ts index 8bc45ac9..04a835f4 100644 --- a/test/lib/utils/addresses.ts +++ b/test/lib/utils/addresses.ts @@ -308,5 +308,12 @@ export const POOLS: Record> = { decimals: 18, slot: 0, }, + NESTED_WITH_BOOSTED_POOL: { + address: '0xee76b8f75e20d4bb9eb483cdec176dfc8d02bb3a', + id: '0xee76b8f75e20d4bb9eb483cdec176dfc8d02bb3a', + type: PoolType.Weighted, + decimals: 18, + slot: 0, + }, }, }; diff --git a/test/v3/addLiquidityNested/addLiquidityNestedV3.integration.test.ts b/test/v3/addLiquidityNested/addLiquidityNestedV3.integration.test.ts index 3a47d440..4f4e6503 100644 --- a/test/v3/addLiquidityNested/addLiquidityNestedV3.integration.test.ts +++ b/test/v3/addLiquidityNested/addLiquidityNestedV3.integration.test.ts @@ -10,43 +10,54 @@ import { TestActions, walletActions, } from 'viem'; - import { Address, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER, CHAINS, ChainId, - Hex, NestedPoolState, + PERMIT2, PublicWalletClient, + Slippage, + Token, + TokenAmount, + AddLiquidityNested, + AddLiquidityNestedInput, + AddLiquidityNestedQueryOutputV3, } from '@/index'; - import { ANVIL_NETWORKS, startFork } from 'test/anvil/anvil-global-setup'; import { - AddLiquidityNestedTxInput, - assertResults, - doAddLiquidityNested, - forkSetup, + approveSpenderOnPermit2, + approveSpenderOnToken, POOLS, - TestToken, + sendTransactionGetBalances, + setTokenBalances, TOKENS, } from 'test/lib/utils'; const chainId = ChainId.SEPOLIA; -const DAI = TOKENS[chainId].DAI; +const NESTED_WITH_BOOSTED_POOL = POOLS[chainId].NESTED_WITH_BOOSTED_POOL; +const BOOSTED_POOL = POOLS[chainId].MOCK_BOOSTED_POOL; +const DAI = TOKENS[chainId].DAI_AAVE; +const USDC = TOKENS[chainId].USDC_AAVE; const WETH = TOKENS[chainId].WETH; -const USDC = TOKENS[chainId].USDC; -const USDT = TOKENS[chainId].USDT; -const BPT_3POOL = POOLS[chainId].BPT_3POOL; -const BPT_WETH_3POOL = POOLS[chainId].BPT_WETH_3POOL; + +const parentBptToken = new Token( + chainId, + NESTED_WITH_BOOSTED_POOL.address, + NESTED_WITH_BOOSTED_POOL.decimals, +); +// These are the underlying tokens +const daiToken = new Token(chainId, DAI.address, DAI.decimals); +const usdcToken = new Token(chainId, USDC.address, USDC.decimals); +const wethToken = new Token(chainId, WETH.address, WETH.decimals); +const mainTokens = [wethToken, daiToken, usdcToken]; describe('V3 add liquidity nested test', () => { let rpcUrl: string; let client: PublicWalletClient & TestActions; - let poolId: Hex; let testAddress: Address; - let mainTokens: TestToken[]; - let initialBalances: bigint[]; - let txInput: AddLiquidityNestedTxInput; + const addLiquidityNested = new AddLiquidityNested(); beforeAll(async () => { // setup chain and test client @@ -62,238 +73,176 @@ describe('V3 add liquidity nested test', () => { testAddress = (await client.getAddresses())[0]; - poolId = BPT_WETH_3POOL.id; - - // setup mock api - const api = new MockApi(); - // get pool state from api - const nestedPoolState = await api.getNestedPool(poolId); - - mainTokens = [WETH, DAI, USDC, USDT]; - initialBalances = mainTokens.map((t) => parseUnits('1000', t.decimals)); - - txInput = { - nestedPoolState, - chainId, - rpcUrl, - testAddress, - client, - amountsIn: [], - }; - }); - - beforeEach(async () => { - // User approve vault to spend their tokens and update user balance - await forkSetup( + await setTokenBalances( client, testAddress, mainTokens.map((t) => t.address), - mainTokens.map((t) => t.slot) as number[], - initialBalances, + [WETH.slot, DAI.slot, USDC.slot] as number[], + mainTokens.map((t) => parseUnits('1000', t.decimals)), ); }); - test('single token', async () => { - const amountsIn = [ - { - address: WETH.address, - rawAmount: parseUnits('1', WETH.decimals), - decimals: WETH.decimals, - }, - ]; - - txInput = { - ...txInput, - amountsIn, - }; - - const { - transactionReceipt, - balanceDeltas, - bptOut, - minBptOut, - slippage, - value, - } = await doAddLiquidityNested(txInput); - - assertResults( - transactionReceipt, - bptOut, - amountsIn, - balanceDeltas, - slippage, - minBptOut, + test('query with underlying', async () => { + const addLiquidityInput: AddLiquidityNestedInput = { + amountsIn: [ + { + address: WETH.address, + rawAmount: parseUnits('0.1', WETH.decimals), + decimals: WETH.decimals, + }, + { + address: USDC.address, + rawAmount: parseUnits('20', USDC.decimals), + decimals: USDC.decimals, + }, + ], chainId, - value, + rpcUrl, + }; + const queryOutput = await addLiquidityNested.query( + addLiquidityInput, + nestedPoolState, ); + const expectedAmountsIn = [ + TokenAmount.fromHumanAmount(wethToken, '0.1'), + TokenAmount.fromHumanAmount(daiToken, '0'), + TokenAmount.fromHumanAmount(usdcToken, '20'), + ]; + 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(expectedAmountsIn); }); - // test('all tokens', async () => { - // const amountsIn = mainTokens.map((t) => ({ - // address: t.address, - // rawAmount: parseUnits('1', t.decimals), - // decimals: t.decimals, - // })); - - // txInput = { - // ...txInput, - // amountsIn, - // }; - - // const { - // transactionReceipt, - // balanceDeltas, - // bptOut, - // minBptOut, - // slippage, - // value, - // } = await doAddLiquidityNested(txInput); - - // assertResults( - // transactionReceipt, - // bptOut, - // amountsIn, - // balanceDeltas, - // slippage, - // minBptOut, - // chainId, - // value, - // ); - // }); - - // test('native asset', async () => { - // const amountsIn = mainTokens.map((t) => ({ - // address: t.address, - // rawAmount: parseUnits('1', t.decimals), - // decimals: t.decimals, - // })); - - // const wethIsEth = true; - - // txInput = { - // ...txInput, - // amountsIn, - // wethIsEth, - // }; - - // const { - // transactionReceipt, - // balanceDeltas, - // bptOut, - // minBptOut, - // slippage, - // value, - // } = await doAddLiquidityNested(txInput); - - // assertResults( - // transactionReceipt, - // bptOut, - // amountsIn, - // balanceDeltas, - // slippage, - // minBptOut, - // chainId, - // value, - // wethIsEth, - // ); - // }); + test('add liquidity transaction', async () => { + const addLiquidityInput: AddLiquidityNestedInput = { + amountsIn: [ + { + address: WETH.address, + rawAmount: parseUnits('0.1', WETH.decimals), + decimals: WETH.decimals, + }, + { + address: USDC.address, + rawAmount: parseUnits('20', USDC.decimals), + decimals: USDC.decimals, + }, + ], + chainId, + rpcUrl, + }; - // test('native asset - invalid input', async () => { - // const amountsIn = [ - // { - // address: USDC.address, - // rawAmount: parseUnits('1', USDC.decimals), - // decimals: USDC.decimals, - // }, - // ]; + 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 addLiquidityNested.query( + addLiquidityInput, + nestedPoolState, + )) as AddLiquidityNestedQueryOutputV3; - // const wethIsEth = true; + const addLiquidityBuildInput = { + ...queryOutput, + slippage: Slippage.fromPercentage('1'), // 1%, + }; - // txInput = { - // ...txInput, - // amountsIn, - // wethIsEth, - // }; + const addLiquidityBuildCallOutput = addLiquidityNested.buildCall( + addLiquidityBuildInput, + ); - // await expect(() => doAddLiquidityNested(txInput)).rejects.toThrowError( - // 'Adding liquidity with native asset requires wrapped native asset to exist within amountsIn', - // ); - // }); + // send add liquidity transaction and check balance changes + const { transactionReceipt, balanceDeltas } = + await sendTransactionGetBalances( + [ + ...mainTokens.map((t) => t.address), + queryOutput.bptOut.token.address, + ], + client, + testAddress, + addLiquidityBuildCallOutput.to, + addLiquidityBuildCallOutput.callData, + addLiquidityBuildCallOutput.value, + ); + // think about using assertResults helper here + expect(transactionReceipt.status).to.eq('success'); + const expectedAmountsIn = [ + TokenAmount.fromHumanAmount(wethToken, '0.1'), + TokenAmount.fromHumanAmount(daiToken, '0'), + TokenAmount.fromHumanAmount(usdcToken, '20'), + ]; + const expectedDeltas = [ + ...expectedAmountsIn.map((a) => a.amount), + queryOutput.bptOut.amount, + ]; + expect(expectedDeltas).to.deep.eq(balanceDeltas); + }); }); -/*********************** Mock To Represent API Requirements **********************/ - -class MockApi { - public async getNestedPool(poolId: Hex): Promise { - if (poolId !== BPT_WETH_3POOL.id) throw Error(); - return { - protocolVersion: 3, - pools: [ - { - id: BPT_WETH_3POOL.id, - address: BPT_WETH_3POOL.address, - type: BPT_WETH_3POOL.type, - level: 1, - tokens: [ - { - address: BPT_3POOL.address, - decimals: BPT_3POOL.decimals, - index: 0, - }, - { - address: WETH.address, - decimals: WETH.decimals, - index: 1, - }, - ], - }, +const nestedPoolState: NestedPoolState = { + protocolVersion: 3, + pools: [ + { + id: NESTED_WITH_BOOSTED_POOL.id, + address: NESTED_WITH_BOOSTED_POOL.address, + type: NESTED_WITH_BOOSTED_POOL.type, + level: 1, + tokens: [ { - id: BPT_3POOL.id, - address: BPT_3POOL.address, - type: BPT_3POOL.type, - level: 0, - tokens: [ - { - address: DAI.address, - decimals: DAI.decimals, - index: 0, - }, - { - address: BPT_3POOL.address, - decimals: BPT_3POOL.decimals, - index: 1, - }, - { - address: USDC.address, - decimals: USDC.decimals, - index: 2, - }, - { - address: USDT.address, - decimals: USDT.decimals, - index: 3, - }, - ], + address: BOOSTED_POOL.address, + decimals: BOOSTED_POOL.decimals, + index: 0, }, - ], - mainTokens: [ { address: WETH.address, decimals: WETH.decimals, + index: 1, }, - { - address: DAI.address, - decimals: DAI.decimals, - }, + ], + }, + { + id: BOOSTED_POOL.id, + address: BOOSTED_POOL.address, + type: BOOSTED_POOL.type, + level: 0, + tokens: [ { address: USDC.address, decimals: USDC.decimals, + index: 0, }, { - address: USDT.address, - decimals: USDT.decimals, + address: DAI.address, + decimals: DAI.decimals, + index: 1, }, ], - }; - } -} + }, + ], + mainTokens: [ + { + address: WETH.address, + decimals: WETH.decimals, + }, + { + address: DAI.address, + decimals: DAI.decimals, + }, + { + address: USDC.address, + decimals: USDC.decimals, + }, + ], +}; From f63ddc4af5f66e3800c2db86f519f945e85e2e42 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 23 Oct 2024 20:13:22 +0100 Subject: [PATCH 05/10] feat: Add permit signature support for addLiquidityNested V3. --- .../addLiquidityNestedV3/index.ts | 27 +++ src/entities/addLiquidityNested/index.ts | 10 + src/entities/permit2Helper/index.ts | 40 ++++ .../addLiquidityNestedV3.integration.test.ts | 2 +- ...idityNestedV3Signature.integration.test.ts | 213 ++++++++++++++++++ 5 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 test/v3/addLiquidityNested/addLiquidityNestedV3Signature.integration.test.ts diff --git a/src/entities/addLiquidityNested/addLiquidityNestedV3/index.ts b/src/entities/addLiquidityNested/addLiquidityNestedV3/index.ts index 27cba989..44428829 100644 --- a/src/entities/addLiquidityNested/addLiquidityNestedV3/index.ts +++ b/src/entities/addLiquidityNested/addLiquidityNestedV3/index.ts @@ -27,6 +27,7 @@ import { AddLiquidityNestedQueryOutputV3, } from './types'; import { validateQueryInput } from '../addLiquidityNestedV2/validateInputs'; +import { Permit2 } from '@/entities/permit2Helper'; export class AddLiquidityNestedV3 { async query( @@ -97,6 +98,32 @@ export class AddLiquidityNestedV3 { }; } + public buildCallWithPermit2( + input: AddLiquidityNestedCallInputV3, + permit2: Permit2, + ): AddLiquidityNestedBuildCallOutput { + const buildCallOutput = this.buildCall(input); + + const args = [ + [], + [], + permit2.batch, + permit2.signature, + [buildCallOutput.callData], + ] as const; + + const callData = encodeFunctionData({ + abi: balancerCompositeLiquidityRouterAbi, + functionName: 'permitBatchAndCall', + args, + }); + + return { + ...buildCallOutput, + callData, + }; + } + private doQueryAddLiquidityUnbalancedNestedPool = async ( { rpcUrl, chainId }: AddLiquidityNestedInput, parentPool: Address, diff --git a/src/entities/addLiquidityNested/index.ts b/src/entities/addLiquidityNested/index.ts index 49ea767c..ba2eb5de 100644 --- a/src/entities/addLiquidityNested/index.ts +++ b/src/entities/addLiquidityNested/index.ts @@ -8,6 +8,8 @@ import { AddLiquidityNestedQueryOutput, } from './types'; import { AddLiquidityNestedV3 } from './addLiquidityNestedV3'; +import { Permit2 } from '../permit2Helper'; +import { AddLiquidityNestedCallInputV3 } from './addLiquidityNestedV3/types'; export class AddLiquidityNested { async query( @@ -46,4 +48,12 @@ export class AddLiquidityNested { } } } + + public buildCallWithPermit2( + input: AddLiquidityNestedCallInputV3, + permit2: Permit2, + ): AddLiquidityNestedBuildCallOutput { + const addLiquidity = new AddLiquidityNestedV3(); + return addLiquidity.buildCallWithPermit2(input, permit2); + } } diff --git a/src/entities/permit2Helper/index.ts b/src/entities/permit2Helper/index.ts index 601fb580..a39af5e6 100644 --- a/src/entities/permit2Helper/index.ts +++ b/src/entities/permit2Helper/index.ts @@ -7,7 +7,9 @@ import { } from './allowanceTransfer'; import { BALANCER_BATCH_ROUTER, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER, BALANCER_ROUTER, + ChainId, PERMIT2, PublicWalletClient, } from '@/utils'; @@ -74,6 +76,44 @@ export class Permit2Helper { return signPermit2(input.client, input.owner, spender, details); } + static async signAddLiquidityNestedApproval(input: { + amountsIn: TokenAmount[]; + chainId: ChainId; + client: PublicWalletClient; + owner: Address; + nonces?: number[]; + expirations?: number[]; + }): Promise { + if (input.nonces && input.nonces.length !== input.amountsIn.length) { + throw new Error("Nonces length doesn't match amountsIn length"); + } + if ( + input.expirations && + input.expirations.length !== input.amountsIn.length + ) { + throw new Error( + "Expirations length doesn't match amountsIn length", + ); + } + const maxAmountsIn = input.amountsIn.map((a) => a.amount); + const spender = BALANCER_COMPOSITE_LIQUIDITY_ROUTER[input.chainId]; + const details: PermitDetails[] = []; + for (let i = 0; i < input.amountsIn.length; i++) { + details.push( + await getDetails( + input.client, + input.amountsIn[i].token.address, + input.owner, + spender, + maxAmountsIn[i], + input.expirations ? input.expirations[i] : undefined, + input.nonces ? input.nonces[i] : undefined, + ), + ); + } + return signPermit2(input.client, input.owner, spender, details); + } + static async signSwapApproval( input: SwapBuildCallInputBase & { client: PublicWalletClient; diff --git a/test/v3/addLiquidityNested/addLiquidityNestedV3.integration.test.ts b/test/v3/addLiquidityNested/addLiquidityNestedV3.integration.test.ts index 4f4e6503..4b9742b1 100644 --- a/test/v3/addLiquidityNested/addLiquidityNestedV3.integration.test.ts +++ b/test/v3/addLiquidityNested/addLiquidityNestedV3.integration.test.ts @@ -53,7 +53,7 @@ const usdcToken = new Token(chainId, USDC.address, USDC.decimals); const wethToken = new Token(chainId, WETH.address, WETH.decimals); const mainTokens = [wethToken, daiToken, usdcToken]; -describe('V3 add liquidity nested test', () => { +describe('V3 add liquidity nested test, with Permit2 direct approval', () => { let rpcUrl: string; let client: PublicWalletClient & TestActions; let testAddress: Address; diff --git a/test/v3/addLiquidityNested/addLiquidityNestedV3Signature.integration.test.ts b/test/v3/addLiquidityNested/addLiquidityNestedV3Signature.integration.test.ts new file mode 100644 index 00000000..87010b8b --- /dev/null +++ b/test/v3/addLiquidityNested/addLiquidityNestedV3Signature.integration.test.ts @@ -0,0 +1,213 @@ +// pnpm test -- addLiquidityNestedV3Signature.integration.test.ts +import dotenv from 'dotenv'; +dotenv.config(); + +import { + createTestClient, + http, + parseUnits, + publicActions, + TestActions, + walletActions, +} from 'viem'; +import { + Address, + CHAINS, + ChainId, + NestedPoolState, + PERMIT2, + PublicWalletClient, + Slippage, + Token, + TokenAmount, + AddLiquidityNested, + AddLiquidityNestedInput, + AddLiquidityNestedQueryOutputV3, + Permit2Helper, +} from '@/index'; +import { ANVIL_NETWORKS, startFork } from 'test/anvil/anvil-global-setup'; +import { + approveSpenderOnToken, + POOLS, + sendTransactionGetBalances, + setTokenBalances, + TOKENS, +} from 'test/lib/utils'; + +const chainId = ChainId.SEPOLIA; +const NESTED_WITH_BOOSTED_POOL = POOLS[chainId].NESTED_WITH_BOOSTED_POOL; +const BOOSTED_POOL = POOLS[chainId].MOCK_BOOSTED_POOL; +const DAI = TOKENS[chainId].DAI_AAVE; +const USDC = TOKENS[chainId].USDC_AAVE; +const WETH = TOKENS[chainId].WETH; + +// These are the underlying tokens +const daiToken = new Token(chainId, DAI.address, DAI.decimals); +const usdcToken = new Token(chainId, USDC.address, USDC.decimals); +const wethToken = new Token(chainId, WETH.address, WETH.decimals); +const mainTokens = [wethToken, daiToken, usdcToken]; + +describe('V3 add liquidity nested test, with Permit2 signature', () => { + let rpcUrl: string; + let client: PublicWalletClient & TestActions; + let testAddress: Address; + const addLiquidityNested = new AddLiquidityNested(); + + beforeAll(async () => { + // setup chain and test client + ({ rpcUrl } = await startFork(ANVIL_NETWORKS.SEPOLIA)); + + client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + testAddress = (await client.getAddresses())[0]; + + await setTokenBalances( + client, + testAddress, + mainTokens.map((t) => t.address), + [WETH.slot, DAI.slot, USDC.slot] as number[], + mainTokens.map((t) => parseUnits('1000', t.decimals)), + ); + }); + + test('add liquidity transaction', async () => { + const addLiquidityInput: AddLiquidityNestedInput = { + amountsIn: [ + { + address: WETH.address, + rawAmount: parseUnits('0.1', WETH.decimals), + decimals: WETH.decimals, + }, + { + address: USDC.address, + rawAmount: parseUnits('20', USDC.decimals), + decimals: USDC.decimals, + }, + ], + chainId, + rpcUrl, + }; + + const queryOutput = (await addLiquidityNested.query( + addLiquidityInput, + nestedPoolState, + )) as AddLiquidityNestedQueryOutputV3; + + const addLiquidityBuildInput = { + ...queryOutput, + slippage: Slippage.fromPercentage('1'), // 1%, + }; + + // Even when using signatures there must be an initial approve by the user to allow Permit2 to spend their tokens + for (const amount of addLiquidityInput.amountsIn) { + // Approve Permit2 to spend account tokens + await approveSpenderOnToken( + client, + testAddress, + amount.address, + PERMIT2[chainId], + ); + } + + // Create signature for each token being used to add + const permit2 = await Permit2Helper.signAddLiquidityNestedApproval({ + ...addLiquidityBuildInput, + client, + owner: testAddress, + }); + + const addLiquidityBuildCallOutput = + addLiquidityNested.buildCallWithPermit2( + addLiquidityBuildInput, + permit2, + ); + + // send add liquidity transaction and check balance changes + const { transactionReceipt, balanceDeltas } = + await sendTransactionGetBalances( + [ + ...mainTokens.map((t) => t.address), + queryOutput.bptOut.token.address, + ], + client, + testAddress, + addLiquidityBuildCallOutput.to, + addLiquidityBuildCallOutput.callData, + addLiquidityBuildCallOutput.value, + ); + // think about using assertResults helper here + expect(transactionReceipt.status).to.eq('success'); + const expectedAmountsIn = [ + TokenAmount.fromHumanAmount(wethToken, '0.1'), + TokenAmount.fromHumanAmount(daiToken, '0'), + TokenAmount.fromHumanAmount(usdcToken, '20'), + ]; + const expectedDeltas = [ + ...expectedAmountsIn.map((a) => a.amount), + queryOutput.bptOut.amount, + ]; + expect(expectedDeltas).to.deep.eq(balanceDeltas); + }); +}); + +const nestedPoolState: NestedPoolState = { + protocolVersion: 3, + pools: [ + { + id: NESTED_WITH_BOOSTED_POOL.id, + address: NESTED_WITH_BOOSTED_POOL.address, + type: NESTED_WITH_BOOSTED_POOL.type, + level: 1, + tokens: [ + { + address: BOOSTED_POOL.address, + decimals: BOOSTED_POOL.decimals, + index: 0, + }, + { + address: WETH.address, + decimals: WETH.decimals, + index: 1, + }, + ], + }, + { + id: BOOSTED_POOL.id, + address: BOOSTED_POOL.address, + type: BOOSTED_POOL.type, + level: 0, + tokens: [ + { + address: USDC.address, + decimals: USDC.decimals, + index: 0, + }, + { + address: DAI.address, + decimals: DAI.decimals, + index: 1, + }, + ], + }, + ], + mainTokens: [ + { + address: WETH.address, + decimals: WETH.decimals, + }, + { + address: DAI.address, + decimals: DAI.decimals, + }, + { + address: USDC.address, + decimals: USDC.decimals, + }, + ], +}; From 33fb7852232aaa15352603cf87b8f5427bceb3d6 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 23 Oct 2024 21:24:37 +0100 Subject: [PATCH 06/10] chore: Initial tidy and test setup. --- .../removeLiquidity/removeLiquidityNested.ts | 4 +- src/entities/index.ts | 2 + src/entities/priceImpact/index.ts | 4 +- src/entities/removeLiquidityNested/index.ts | 43 +- .../removeLiquidityNestedV2/encodeCalls.ts | 4 +- .../removeLiquidityNestedV2/getPeekCalls.ts | 4 +- .../getQueryCallsAttributes.ts | 28 +- .../removeLiquidityNestedV2/index.ts | 17 +- .../removeLiquidityNestedV2/types.ts | 22 +- .../removeLiquidityNestedV2/validateInputs.ts | 16 +- .../removeLiquidityNestedV3/index.ts | 22 + .../removeLiquidityNestedV3/types.ts | 24 + src/entities/removeLiquidityNested/types.ts | 30 ++ .../removeLiquidityNested/validateInputs.ts | 57 --- .../priceImpact.integration.test.ts | 4 +- .../removeLiquidityNested.integration.test.ts | 8 +- ...emoveLiquidityNestedV3.integration.test.ts | 145 ++++++ ...emoveLiquidityNestedV3.integration.test.ts | 414 ------------------ 18 files changed, 295 insertions(+), 553 deletions(-) create mode 100644 src/entities/removeLiquidityNested/removeLiquidityNestedV3/index.ts create mode 100644 src/entities/removeLiquidityNested/removeLiquidityNestedV3/types.ts create mode 100644 src/entities/removeLiquidityNested/types.ts delete mode 100644 src/entities/removeLiquidityNested/validateInputs.ts create mode 100644 test/v3/removeLiquidityNested/removeLiquidityNestedV3.integration.test.ts delete mode 100644 test/v3/removeLiquidityNestedV3.integration.test.ts/removeLiquidityNestedV3.integration.test.ts diff --git a/examples/removeLiquidity/removeLiquidityNested.ts b/examples/removeLiquidity/removeLiquidityNested.ts index 43a75b80..0a9955ec 100644 --- a/examples/removeLiquidity/removeLiquidityNested.ts +++ b/examples/removeLiquidity/removeLiquidityNested.ts @@ -24,7 +24,7 @@ import { replaceWrapped, Slippage, RemoveLiquidityNested, - RemoveLiquidityNestedSingleTokenInput, + RemoveLiquidityNestedSingleTokenInputV2, } from '../../src'; import { ANVIL_NETWORKS, startFork } from '../../test/anvil/anvil-global-setup'; import { makeForkTx } from 'examples/lib/makeForkTx'; @@ -103,7 +103,7 @@ const removeLiquidityNested = async ({ // setup remove liquidity helper const removeLiquidityNested = new RemoveLiquidityNested(); - const removeLiquidityInput: RemoveLiquidityNestedSingleTokenInput = { + const removeLiquidityInput: RemoveLiquidityNestedSingleTokenInputV2 = { bptAmountIn, chainId, rpcUrl, diff --git a/src/entities/index.ts b/src/entities/index.ts index 5d66fae3..a01238ec 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -13,6 +13,8 @@ export * from './priceImpactAmount'; export * from './relayer'; export * from './removeLiquidity'; export * from './removeLiquidity/types'; +export * from './removeLiquidityNested/index'; +export * from './removeLiquidityNested/types'; export * from './removeLiquidityNested/removeLiquidityNestedV2'; export * from './removeLiquidityNested/removeLiquidityNestedV2/types'; export * from './slippage'; diff --git a/src/entities/priceImpact/index.ts b/src/entities/priceImpact/index.ts index c1fa2079..a6ad8719 100644 --- a/src/entities/priceImpact/index.ts +++ b/src/entities/priceImpact/index.ts @@ -16,7 +16,7 @@ import { RemoveLiquidityUnbalancedInput, } from '../removeLiquidity/types'; import { RemoveLiquidityNested } from '../removeLiquidityNested'; -import { RemoveLiquidityNestedSingleTokenInput } from '../removeLiquidityNested/removeLiquidityNestedV2/types'; +import { RemoveLiquidityNestedSingleTokenInputV2 } from '../removeLiquidityNested/removeLiquidityNestedV2/types'; import { Swap, SwapInput } from '../swap'; import { Token } from '../token'; import { TokenAmount } from '../tokenAmount'; @@ -370,7 +370,7 @@ export class PriceImpact { * @returns price impact amount */ static removeLiquidityNested = async ( - input: RemoveLiquidityNestedSingleTokenInput, + input: RemoveLiquidityNestedSingleTokenInputV2, nestedPoolState: NestedPoolState, ): Promise => { // inputs are being validated within RemoveLiquidity diff --git a/src/entities/removeLiquidityNested/index.ts b/src/entities/removeLiquidityNested/index.ts index 079e0d02..048a40e3 100644 --- a/src/entities/removeLiquidityNested/index.ts +++ b/src/entities/removeLiquidityNested/index.ts @@ -1,13 +1,14 @@ -import { Address, Hex } from 'viem'; -import { TokenAmount, NestedPoolState } from '@/entities'; +import { NestedPoolState } from '@/entities'; import { validateNestedPoolState } from '@/entities/utils'; +import { RemoveLiquidityNestedV2 } from './removeLiquidityNestedV2'; import { + RemoveLiquidityNestedInput, RemoveLiquidityNestedQueryOutput, RemoveLiquidityNestedCallInput, - RemoveLiquidityNestedInput, -} from './removeLiquidityNestedV2/types'; -import { validateBuildCallInput } from './validateInputs'; -import { RemoveLiquidityNestedV2 } from './removeLiquidityNestedV2'; + RemoveLiquidityNestedBuildCallOutput, +} from './types'; +import { RemoveLiquidityNestedV3 } from './removeLiquidityNestedV3'; +import { validateBuildCallInput } from './removeLiquidityNestedV2/validateInputs'; export class RemoveLiquidityNested { async query( @@ -22,38 +23,28 @@ export class RemoveLiquidityNested { ); } case 2: { - const addLiquidity = new RemoveLiquidityNestedV2(); - return addLiquidity.query(input, nestedPoolState); + const removeLiquidity = new RemoveLiquidityNestedV2(); + return removeLiquidity.query(input, nestedPoolState); } case 3: { - throw new Error( - 'RemoveLiquidityNested not supported for ProtocolVersion 3.', - ); + const removeLiquidity = new RemoveLiquidityNestedV3(); + return removeLiquidity.query(input, nestedPoolState); } } } - buildCall(input: RemoveLiquidityNestedCallInput): { - callData: Hex; - to: Address; - minAmountsOut: TokenAmount[]; - } { - validateBuildCallInput(input); - + buildCall( + input: RemoveLiquidityNestedCallInput, + ): RemoveLiquidityNestedBuildCallOutput { switch (input.protocolVersion) { - case 1: { - throw new Error( - 'AddLiquidityNested not supported for ProtocolVersion 1.', - ); - } case 2: { + validateBuildCallInput(input); const removeLiquidity = new RemoveLiquidityNestedV2(); return removeLiquidity.buildCall(input); } case 3: { - throw new Error( - 'AddLiquidityNested not supported for ProtocolVersion 3.', - ); + const removeLiquidity = new RemoveLiquidityNestedV3(); + return removeLiquidity.buildCall(input); } } } diff --git a/src/entities/removeLiquidityNested/removeLiquidityNestedV2/encodeCalls.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/encodeCalls.ts index 322cf172..163aa615 100644 --- a/src/entities/removeLiquidityNested/removeLiquidityNestedV2/encodeCalls.ts +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/encodeCalls.ts @@ -1,13 +1,13 @@ import { encodeFunctionData, Hex } from 'viem'; import { removeLiquiditySingleTokenExactInShouldHaveTokenOutIndexError } from '@/utils'; -import { RemoveLiquidityNestedCallAttributes } from './types'; +import { RemoveLiquidityNestedCallAttributesV2 } from './types'; import { replaceWrapped } from '@/entities/utils'; import { batchRelayerLibraryAbi } from '@/abi'; import { PoolType } from '@/types'; import { ComposableStableEncoder, WeightedEncoder } from '@/entities/encoders'; export const encodeCalls = ( - callsAttributes: RemoveLiquidityNestedCallAttributes[], + callsAttributes: RemoveLiquidityNestedCallAttributesV2[], isProportional: boolean, ) => { const encodedCalls: Hex[] = []; diff --git a/src/entities/removeLiquidityNested/removeLiquidityNestedV2/getPeekCalls.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/getPeekCalls.ts index 30cd9ede..8b8cd1fc 100644 --- a/src/entities/removeLiquidityNested/removeLiquidityNestedV2/getPeekCalls.ts +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/getPeekCalls.ts @@ -1,10 +1,10 @@ import { Hex } from 'viem'; -import { RemoveLiquidityNestedCallAttributes } from './types'; +import { RemoveLiquidityNestedCallAttributesV2 } from './types'; import { Token } from '@/entities/token'; import { Relayer } from '@/entities/relayer'; export const getPeekCalls = ( - calls: RemoveLiquidityNestedCallAttributes[], + calls: RemoveLiquidityNestedCallAttributesV2[], isProportional: boolean, ) => { const tokensOut: Token[] = []; diff --git a/src/entities/removeLiquidityNested/removeLiquidityNestedV2/getQueryCallsAttributes.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/getQueryCallsAttributes.ts index b35dc23c..d21dec7b 100644 --- a/src/entities/removeLiquidityNested/removeLiquidityNestedV2/getQueryCallsAttributes.ts +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/getQueryCallsAttributes.ts @@ -5,25 +5,25 @@ import { BALANCER_RELAYER, ChainId, ZERO_ADDRESS } from '@/utils'; import { Token } from '@/entities/token'; import { NestedPool, PoolKind } from '@/entities/types'; import { - RemoveLiquidityNestedCallAttributes, - RemoveLiquidityNestedProportionalInput, - RemoveLiquidityNestedSingleTokenInput, + RemoveLiquidityNestedCallAttributesV2, + RemoveLiquidityNestedProportionalInputV2, + RemoveLiquidityNestedSingleTokenInputV2, } from './types'; import { PoolType } from '@/types'; import { Relayer } from '@/entities/relayer'; export const getQueryCallsAttributes = ( input: - | RemoveLiquidityNestedProportionalInput - | RemoveLiquidityNestedSingleTokenInput, + | RemoveLiquidityNestedProportionalInputV2 + | RemoveLiquidityNestedSingleTokenInputV2, pools: NestedPool[], isProportional: boolean, ): { bptAmountIn: TokenAmount; - callsAttributes: RemoveLiquidityNestedCallAttributes[]; + callsAttributes: RemoveLiquidityNestedCallAttributesV2[]; } => { const { bptAmountIn, chainId, toInternalBalance = false } = input; - let callsAttributes: RemoveLiquidityNestedCallAttributes[]; + let callsAttributes: RemoveLiquidityNestedCallAttributesV2[]; // sort pools by descending level const poolsTopDown = pools.sort((a, b) => b.level - a.level); @@ -39,7 +39,7 @@ export const getQueryCallsAttributes = ( toInternalBalance, ); } else { - const { tokenOut } = input as RemoveLiquidityNestedSingleTokenInput; + const { tokenOut } = input as RemoveLiquidityNestedSingleTokenInputV2; callsAttributes = getSingleTokenCallsAttributes( poolsTopDown, @@ -70,7 +70,7 @@ const getProportionalCallsAttributes = ( * 3. Output at bottom level is the amountsOut */ - const calls: RemoveLiquidityNestedCallAttributes[] = []; + const calls: RemoveLiquidityNestedCallAttributesV2[] = []; for (const pool of poolsSortedByLevel) { const sortedTokens = pool.tokens .sort((a, b) => a.index - b.index) @@ -133,7 +133,7 @@ const getSingleTokenCallsAttributes = ( tokenOut, poolsTopDown, ); - const calls: RemoveLiquidityNestedCallAttributes[] = []; + const calls: RemoveLiquidityNestedCallAttributesV2[] = []; for (let i = 0; i < removeLiquidityPath.length; i++) { const pool = removeLiquidityPath[i]; @@ -209,7 +209,7 @@ const getRemoveLiquidityPath = ( const getBptAmountIn = ( pool: NestedPool, bptAmountIn: bigint, - calls: RemoveLiquidityNestedCallAttributes[], + calls: RemoveLiquidityNestedCallAttributesV2[], isProportional: boolean, ) => { // first call has bptAmountIn provided as it's input @@ -221,14 +221,14 @@ const getBptAmountIn = ( } // following calls have their input as the outputReference of a previous call - let previousCall: RemoveLiquidityNestedCallAttributes; + let previousCall: RemoveLiquidityNestedCallAttributesV2; let outputReferenceIndex: number; if (isProportional) { previousCall = calls.find((call) => call.sortedTokens .map((token) => token.address) .includes(pool.address), - ) as RemoveLiquidityNestedCallAttributes; + ) as RemoveLiquidityNestedCallAttributesV2; outputReferenceIndex = previousCall.sortedTokens .map((token) => token.address) .indexOf(pool.address); @@ -248,7 +248,7 @@ const getBptAmountIn = ( // Sender's logic: if there is a previous call, then the sender is the // recipient of that call, otherwise it's the user. const getSenderProportional = ( - calls: RemoveLiquidityNestedCallAttributes[], + calls: RemoveLiquidityNestedCallAttributesV2[], poolAddress: Address, accountAddress: Address, ): Address => { diff --git a/src/entities/removeLiquidityNested/removeLiquidityNestedV2/index.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/index.ts index 98e075f5..481b539d 100644 --- a/src/entities/removeLiquidityNested/removeLiquidityNestedV2/index.ts +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/index.ts @@ -13,18 +13,21 @@ import { encodeCalls } from './encodeCalls'; import { doRemoveLiquidityNestedQuery } from './doRemoveLiquidityNestedQuery'; import { getPeekCalls } from './getPeekCalls'; import { getQueryCallsAttributes } from './getQueryCallsAttributes'; +import { validateQueryInput, validateBuildCallInput } from './validateInputs'; import { - RemoveLiquidityNestedQueryOutput, - RemoveLiquidityNestedCallInput, - RemoveLiquidityNestedInput, + RemoveLiquidityNestedCallInputV2, + RemoveLiquidityNestedProportionalInputV2, + RemoveLiquidityNestedQueryOutputV2, + RemoveLiquidityNestedSingleTokenInputV2, } from './types'; -import { validateQueryInput, validateBuildCallInput } from './validateInputs'; export class RemoveLiquidityNestedV2 { async query( - input: RemoveLiquidityNestedInput, + input: + | RemoveLiquidityNestedProportionalInputV2 + | RemoveLiquidityNestedSingleTokenInputV2, nestedPoolState: NestedPoolState, - ): Promise { + ): Promise { const isProportional = validateQueryInput(input, nestedPoolState); validateNestedPoolState(nestedPoolState); @@ -85,7 +88,7 @@ export class RemoveLiquidityNestedV2 { }; } - buildCall(input: RemoveLiquidityNestedCallInput): { + buildCall(input: RemoveLiquidityNestedCallInputV2): { callData: Hex; to: Address; minAmountsOut: TokenAmount[]; diff --git a/src/entities/removeLiquidityNested/removeLiquidityNestedV2/types.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/types.ts index 20f81cd3..4cd3195e 100644 --- a/src/entities/removeLiquidityNested/removeLiquidityNestedV2/types.ts +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/types.ts @@ -6,23 +6,19 @@ import { PoolKind } from '@/entities/types'; import { PoolType } from '@/types'; import { ChainId } from '@/utils'; -export type RemoveLiquidityNestedProportionalInput = { +export type RemoveLiquidityNestedProportionalInputV2 = { bptAmountIn: bigint; chainId: ChainId; rpcUrl: string; toInternalBalance?: boolean; }; -export type RemoveLiquidityNestedSingleTokenInput = - RemoveLiquidityNestedProportionalInput & { +export type RemoveLiquidityNestedSingleTokenInputV2 = + RemoveLiquidityNestedProportionalInputV2 & { tokenOut: Address; }; -export type RemoveLiquidityNestedInput = - | RemoveLiquidityNestedProportionalInput - | RemoveLiquidityNestedSingleTokenInput; - -export type RemoveLiquidityNestedCallAttributes = { +export type RemoveLiquidityNestedCallAttributesV2 = { chainId: ChainId; sortedTokens: Token[]; poolId: Address; @@ -45,17 +41,17 @@ export type RemoveLiquidityNestedCallAttributes = { wethIsEth?: boolean; }; -export type RemoveLiquidityNestedQueryOutput = { - protocolVersion: 1 | 2 | 3; - callsAttributes: RemoveLiquidityNestedCallAttributes[]; +export type RemoveLiquidityNestedQueryOutputV2 = { + protocolVersion: 2; + callsAttributes: RemoveLiquidityNestedCallAttributesV2[]; bptAmountIn: TokenAmount; amountsOut: TokenAmount[]; isProportional: boolean; chainId: ChainId; }; -export type RemoveLiquidityNestedCallInput = - RemoveLiquidityNestedQueryOutput & { +export type RemoveLiquidityNestedCallInputV2 = + RemoveLiquidityNestedQueryOutputV2 & { slippage: Slippage; accountAddress: Address; relayerApprovalSignature?: Hex; diff --git a/src/entities/removeLiquidityNested/removeLiquidityNestedV2/validateInputs.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/validateInputs.ts index 7bb0cfb9..f12d7af1 100644 --- a/src/entities/removeLiquidityNested/removeLiquidityNestedV2/validateInputs.ts +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/validateInputs.ts @@ -2,15 +2,15 @@ import { NATIVE_ASSETS } from '../../../utils'; import { Token } from '../../token'; import { NestedPoolState } from '../../types'; import { - RemoveLiquidityNestedCallInput, - RemoveLiquidityNestedProportionalInput, - RemoveLiquidityNestedSingleTokenInput, + RemoveLiquidityNestedCallInputV2, + RemoveLiquidityNestedProportionalInputV2, + RemoveLiquidityNestedSingleTokenInputV2, } from './types'; export const validateQueryInput = ( input: - | RemoveLiquidityNestedProportionalInput - | RemoveLiquidityNestedSingleTokenInput, + | RemoveLiquidityNestedProportionalInputV2 + | RemoveLiquidityNestedSingleTokenInputV2, nestedPoolState: NestedPoolState, ) => { const tokenOut = 'tokenOut' in input ? input.tokenOut : undefined; @@ -20,7 +20,7 @@ export const validateQueryInput = ( ); if (!isProportional) { validateInputsSingleToken( - input as RemoveLiquidityNestedSingleTokenInput, + input as RemoveLiquidityNestedSingleTokenInputV2, mainTokens, ); } @@ -29,7 +29,7 @@ export const validateQueryInput = ( }; const validateInputsSingleToken = ( - input: RemoveLiquidityNestedSingleTokenInput, + input: RemoveLiquidityNestedSingleTokenInputV2, mainTokens: Token[], ) => { const tokenOut = mainTokens.find((t) => t.isSameAddress(input.tokenOut)); @@ -42,7 +42,7 @@ const validateInputsSingleToken = ( }; export const validateBuildCallInput = ( - input: RemoveLiquidityNestedCallInput, + input: RemoveLiquidityNestedCallInputV2, ) => { if ( input.wethIsEth && diff --git a/src/entities/removeLiquidityNested/removeLiquidityNestedV3/index.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV3/index.ts new file mode 100644 index 00000000..724de4a0 --- /dev/null +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV3/index.ts @@ -0,0 +1,22 @@ +import { NestedPoolState } from '@/entities/types'; +import { + RemoveLiquidityNestedCallInputV3, + RemoveLiquidityNestedProportionalInputV3, + RemoveLiquidityNestedQueryOutputV3, +} from './types'; +import { RemoveLiquidityNestedBuildCallOutput } from '../types'; + +export class RemoveLiquidityNestedV3 { + async query( + _input: RemoveLiquidityNestedProportionalInputV3, + _nestedPoolState: NestedPoolState, + ): Promise { + return {} as RemoveLiquidityNestedQueryOutputV3; + } + + buildCall( + _input: RemoveLiquidityNestedCallInputV3, + ): RemoveLiquidityNestedBuildCallOutput { + return {} as RemoveLiquidityNestedBuildCallOutput; + } +} diff --git a/src/entities/removeLiquidityNested/removeLiquidityNestedV3/types.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV3/types.ts new file mode 100644 index 00000000..141d4b0c --- /dev/null +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV3/types.ts @@ -0,0 +1,24 @@ +import { Address, Hex } from 'viem'; +import { TokenAmount } from '@/entities/tokenAmount'; +import { ChainId } from '@/utils'; +import { Slippage } from '@/entities/slippage'; + +export type RemoveLiquidityNestedProportionalInputV3 = { + bptAmountIn: bigint; + chainId: ChainId; + rpcUrl: string; +}; + +export type RemoveLiquidityNestedQueryOutputV3 = { + protocolVersion: 3; + bptAmountIn: TokenAmount; + amountsOut: TokenAmount[]; + chainId: ChainId; + parentPool: Address; + userData: Hex; +}; + +export type RemoveLiquidityNestedCallInputV3 = + RemoveLiquidityNestedQueryOutputV3 & { + slippage: Slippage; + }; diff --git a/src/entities/removeLiquidityNested/types.ts b/src/entities/removeLiquidityNested/types.ts new file mode 100644 index 00000000..06cfcbb5 --- /dev/null +++ b/src/entities/removeLiquidityNested/types.ts @@ -0,0 +1,30 @@ +import { Address, Hex } from 'viem'; +import { + RemoveLiquidityNestedProportionalInputV2, + RemoveLiquidityNestedSingleTokenInputV2, + RemoveLiquidityNestedQueryOutputV2, + RemoveLiquidityNestedCallInputV2, +} from './removeLiquidityNestedV2/types'; +import { + RemoveLiquidityNestedCallInputV3, + RemoveLiquidityNestedQueryOutputV3, +} from './removeLiquidityNestedV3/types'; +import { TokenAmount } from '../tokenAmount'; + +export type RemoveLiquidityNestedInput = + | RemoveLiquidityNestedProportionalInputV2 + | RemoveLiquidityNestedSingleTokenInputV2; + +export type RemoveLiquidityNestedQueryOutput = + | RemoveLiquidityNestedQueryOutputV2 + | RemoveLiquidityNestedQueryOutputV3; + +export type RemoveLiquidityNestedCallInput = + | RemoveLiquidityNestedCallInputV2 + | RemoveLiquidityNestedCallInputV3; + +export type RemoveLiquidityNestedBuildCallOutput = { + callData: Hex; + to: Address; + minAmountsOut: TokenAmount[]; +}; diff --git a/src/entities/removeLiquidityNested/validateInputs.ts b/src/entities/removeLiquidityNested/validateInputs.ts deleted file mode 100644 index 5e905bb2..00000000 --- a/src/entities/removeLiquidityNested/validateInputs.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { NATIVE_ASSETS } from '@/utils'; -import { Token } from '../token'; -import { NestedPoolState } from '../types'; -import { - RemoveLiquidityNestedCallInput, - RemoveLiquidityNestedProportionalInput, - RemoveLiquidityNestedSingleTokenInput, -} from './removeLiquidityNestedV2/types'; - -export const validateQueryInput = ( - input: - | RemoveLiquidityNestedProportionalInput - | RemoveLiquidityNestedSingleTokenInput, - nestedPoolState: NestedPoolState, -) => { - const tokenOut = 'tokenOut' in input ? input.tokenOut : undefined; - const isProportional = tokenOut === undefined; - const mainTokens = nestedPoolState.mainTokens.map( - (token) => new Token(input.chainId, token.address, token.decimals), - ); - if (!isProportional) { - validateInputsSingleToken( - input as RemoveLiquidityNestedSingleTokenInput, - mainTokens, - ); - } - - return isProportional; -}; - -const validateInputsSingleToken = ( - input: RemoveLiquidityNestedSingleTokenInput, - mainTokens: Token[], -) => { - const tokenOut = mainTokens.find((t) => t.isSameAddress(input.tokenOut)); - - if (tokenOut === undefined) { - throw new Error( - `Removing liquidity to ${input.tokenOut} requires it to exist within main tokens`, - ); - } -}; - -export const validateBuildCallInput = ( - input: RemoveLiquidityNestedCallInput, -) => { - if ( - input.wethIsEth && - !input.amountsOut.some((a) => - a.token.isSameAddress(NATIVE_ASSETS[input.chainId].wrapped), - ) - ) { - throw new Error( - 'Removing liquidity to native asset requires wrapped native asset to exist within amounts out', - ); - } -}; diff --git a/test/v2/priceImpact/priceImpact.integration.test.ts b/test/v2/priceImpact/priceImpact.integration.test.ts index 6160ec4b..fc60be55 100644 --- a/test/v2/priceImpact/priceImpact.integration.test.ts +++ b/test/v2/priceImpact/priceImpact.integration.test.ts @@ -27,7 +27,7 @@ import { RemoveLiquiditySingleTokenExactInInput, RemoveLiquidityUnbalancedInput, SwapKind, - RemoveLiquidityNestedSingleTokenInput, + RemoveLiquidityNestedSingleTokenInputV2, } from 'src'; import { ANVIL_NETWORKS, startFork } from 'test/anvil/anvil-global-setup'; @@ -424,7 +424,7 @@ describe('price impact', () => { * ABA approach as price impact for other actions (addLiquidity, swap, etc.) */ describe('remove liquidity nested - single token', () => { - let input: RemoveLiquidityNestedSingleTokenInput; + let input: RemoveLiquidityNestedSingleTokenInputV2; beforeAll(() => { input = { chainId, diff --git a/test/v2/removeLiquidityNested/removeLiquidityNested.integration.test.ts b/test/v2/removeLiquidityNested/removeLiquidityNested.integration.test.ts index 68734904..1b643417 100644 --- a/test/v2/removeLiquidityNested/removeLiquidityNested.integration.test.ts +++ b/test/v2/removeLiquidityNested/removeLiquidityNested.integration.test.ts @@ -21,8 +21,8 @@ import { NestedPoolState, PoolType, Relayer, - RemoveLiquidityNestedProportionalInput, - RemoveLiquidityNestedSingleTokenInput, + RemoveLiquidityNestedProportionalInputV2, + RemoveLiquidityNestedSingleTokenInputV2, replaceWrapped, Slippage, TokenAmount, @@ -244,8 +244,8 @@ export const doTransaction = async ({ // setup remove liquidity helper const removeLiquidityNested = new RemoveLiquidityNested(); const removeLiquidityInput: - | RemoveLiquidityNestedProportionalInput - | RemoveLiquidityNestedSingleTokenInput = { + | RemoveLiquidityNestedProportionalInputV2 + | RemoveLiquidityNestedSingleTokenInputV2 = { bptAmountIn: amountIn, chainId, rpcUrl, diff --git a/test/v3/removeLiquidityNested/removeLiquidityNestedV3.integration.test.ts b/test/v3/removeLiquidityNested/removeLiquidityNestedV3.integration.test.ts new file mode 100644 index 00000000..87653f31 --- /dev/null +++ b/test/v3/removeLiquidityNested/removeLiquidityNestedV3.integration.test.ts @@ -0,0 +1,145 @@ +// pnpm test -- removeLiquidityNestedV3.integration.test.ts +import dotenv from 'dotenv'; +dotenv.config(); + +import { + createTestClient, + http, + parseUnits, + publicActions, + TestActions, + walletActions, +} from 'viem'; + +import { + Address, + ChainId, + CHAINS, + NestedPoolState, + PublicWalletClient, + Token, + RemoveLiquidityNestedInput, + RemoveLiquidityNested, +} from 'src'; + +import { ANVIL_NETWORKS, startFork } from 'test/anvil/anvil-global-setup'; +import { POOLS, setTokenBalances, TOKENS } from 'test/lib/utils'; + +const chainId = ChainId.SEPOLIA; +const NESTED_WITH_BOOSTED_POOL = POOLS[chainId].NESTED_WITH_BOOSTED_POOL; +const BOOSTED_POOL = POOLS[chainId].MOCK_BOOSTED_POOL; +const DAI = TOKENS[chainId].DAI_AAVE; +const USDC = TOKENS[chainId].USDC_AAVE; +const WETH = TOKENS[chainId].WETH; + +const parentBptToken = new Token( + chainId, + NESTED_WITH_BOOSTED_POOL.address, + NESTED_WITH_BOOSTED_POOL.decimals, +); + +describe('V3 remove liquidity nested test, with Permit direct approval', () => { + let rpcUrl: string; + let client: PublicWalletClient & TestActions; + let testAddress: Address; + const removeLiquidityNested = new RemoveLiquidityNested(); + + beforeAll(async () => { + ({ rpcUrl } = await startFork(ANVIL_NETWORKS.MAINNET)); + + client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + testAddress = (await client.getAddresses())[0]; + + // Mint BPT to testAddress + await setTokenBalances( + client, + testAddress, + [parentBptToken.address], + [0], + [parseUnits('10', 18)], + ); + }); + + test('query with underlying', async () => { + const addLiquidityInput: RemoveLiquidityNestedInput = { + bptAmountIn: parseUnits('7', 18), + chainId, + rpcUrl, + }; + const queryOutput = await removeLiquidityNested.query( + addLiquidityInput, + nestedPoolState, + ); + console.log(queryOutput); + /* + protocolVersion: 1 | 2 | 3; + callsAttributes: RemoveLiquidityNestedCallAttributes[]; + bptAmountIn: TokenAmount; + amountsOut: TokenAmount[]; + isProportional: boolean; + chainId: ChainId;*/ + }); +}); + +const nestedPoolState: NestedPoolState = { + protocolVersion: 3, + pools: [ + { + id: NESTED_WITH_BOOSTED_POOL.id, + address: NESTED_WITH_BOOSTED_POOL.address, + type: NESTED_WITH_BOOSTED_POOL.type, + level: 1, + tokens: [ + { + address: BOOSTED_POOL.address, + decimals: BOOSTED_POOL.decimals, + index: 0, + }, + { + address: WETH.address, + decimals: WETH.decimals, + index: 1, + }, + ], + }, + { + id: BOOSTED_POOL.id, + address: BOOSTED_POOL.address, + type: BOOSTED_POOL.type, + level: 0, + tokens: [ + { + address: USDC.address, + decimals: USDC.decimals, + index: 0, + }, + { + address: DAI.address, + decimals: DAI.decimals, + index: 1, + }, + ], + }, + ], + mainTokens: [ + { + address: WETH.address, + decimals: WETH.decimals, + }, + { + address: DAI.address, + decimals: DAI.decimals, + }, + { + address: USDC.address, + decimals: USDC.decimals, + }, + ], +}; diff --git a/test/v3/removeLiquidityNestedV3.integration.test.ts/removeLiquidityNestedV3.integration.test.ts b/test/v3/removeLiquidityNestedV3.integration.test.ts/removeLiquidityNestedV3.integration.test.ts deleted file mode 100644 index b70de744..00000000 --- a/test/v3/removeLiquidityNestedV3.integration.test.ts/removeLiquidityNestedV3.integration.test.ts +++ /dev/null @@ -1,414 +0,0 @@ -// pnpm test -- removeLiquidityNestedV3.integration.test.ts -import dotenv from 'dotenv'; -dotenv.config(); - -import { - createTestClient, - http, - parseUnits, - publicActions, - TestActions, - TransactionReceipt, - walletActions, -} from 'viem'; - -import { - Address, - BALANCER_RELAYER, - ChainId, - CHAINS, - Hex, - NestedPoolState, - PoolType, - Relayer, - RemoveLiquidityNestedProportionalInput, - RemoveLiquidityNestedSingleTokenInput, - replaceWrapped, - Slippage, - TokenAmount, - PublicWalletClient, -} from 'src'; - -import { ANVIL_NETWORKS, startFork } from 'test/anvil/anvil-global-setup'; -import { forkSetup, sendTransactionGetBalances } from 'test/lib/utils'; -import { RemoveLiquidityNested } from '@/entities/removeLiquidityNested'; - -type TxInput = { - poolId: Hex; - amountIn: bigint; - chainId: ChainId; - rpcUrl: string; - testAddress: Address; - client: PublicWalletClient & TestActions; - tokenOut?: Address; - wethIsEth?: boolean; -}; - -describe.skip('remove liquidity nested test', () => { - let chainId: ChainId; - let rpcUrl: string; - let client: PublicWalletClient & TestActions; - let poolId: Hex; - let testAddress: Address; - - beforeAll(async () => { - // setup chain and test client - chainId = ChainId.MAINNET; - ({ rpcUrl } = await startFork(ANVIL_NETWORKS.MAINNET)); - - client = createTestClient({ - mode: 'anvil', - chain: CHAINS[chainId], - transport: http(rpcUrl), - }) - .extend(publicActions) - .extend(walletActions); - - testAddress = (await client.getAddresses())[0]; - - poolId = - '0x08775ccb6674d6bdceb0797c364c2653ed84f3840002000000000000000004f0'; // WETH-3POOL-BPT - }); - - beforeEach(async () => { - await forkSetup( - client, - testAddress, - ['0x08775ccb6674d6bdceb0797c364c2653ed84f384'], - [0], - [parseUnits('1000', 18)], - ); - }); - - test('proportional', async () => { - const amountIn = parseUnits('1', 18); - - const { - transactionReceipt, - expectedDeltas, - balanceDeltas, - amountsOut, - slippage, - minAmountsOut, - } = await doTransaction({ - poolId, - amountIn, - chainId, - rpcUrl, - testAddress, - client, - }); - - assertResults( - transactionReceipt, - expectedDeltas, - balanceDeltas, - amountsOut, - slippage, - minAmountsOut, - ); - }); - - test('proportional - native asset', async () => { - const amountIn = parseUnits('1', 18); - const wethIsEth = true; - - const { - transactionReceipt, - expectedDeltas, - balanceDeltas, - amountsOut, - slippage, - minAmountsOut, - } = await doTransaction({ - poolId, - amountIn, - chainId, - rpcUrl, - testAddress, - client, - wethIsEth, - }); - - assertResults( - transactionReceipt, - expectedDeltas, - balanceDeltas, - amountsOut, - slippage, - minAmountsOut, - ); - }); - - test('single token - token index > bptIndex', async () => { - const amountIn = parseUnits('1', 18); - const tokenOut = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; // USDC - - const { - transactionReceipt, - expectedDeltas, - balanceDeltas, - amountsOut, - slippage, - minAmountsOut, - } = await doTransaction({ - poolId, - amountIn, - chainId, - rpcUrl, - testAddress, - client, - tokenOut, - }); - - assertResults( - transactionReceipt, - expectedDeltas, - balanceDeltas, - amountsOut, - slippage, - minAmountsOut, - ); - }); - - test('single token - native asset', async () => { - const amountIn = parseUnits('1', 18); - const tokenOut = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; // WETH - const wethIsEth = true; - - const { - transactionReceipt, - expectedDeltas, - balanceDeltas, - amountsOut, - slippage, - minAmountsOut, - } = await doTransaction({ - poolId, - amountIn, - chainId, - rpcUrl, - testAddress, - client, - tokenOut, - wethIsEth, - }); - - assertResults( - transactionReceipt, - expectedDeltas, - balanceDeltas, - amountsOut, - slippage, - minAmountsOut, - ); - }); - - test('single token - native asset - invalid input', async () => { - const amountIn = parseUnits('1', 18); - const tokenOut = '0x6b175474e89094c44da98b954eedeac495271d0f'; // DAI - const wethIsEth = true; - - await expect( - doTransaction({ - poolId, - amountIn, - chainId, - rpcUrl, - testAddress, - client, - tokenOut, - wethIsEth, - }), - ).rejects.toThrow( - 'Removing liquidity to native asset requires wrapped native asset to exist within amounts out', - ); - }); -}); - -export const doTransaction = async ({ - poolId, - amountIn, - chainId, - rpcUrl, - testAddress, - client, - tokenOut, - wethIsEth = false, -}: TxInput) => { - // setup mock api - const api = new MockApi(); - // get pool state from api - const nestedPoolFromApi = await api.getNestedPool(poolId); - - // setup remove liquidity helper - const removeLiquidityNested = new RemoveLiquidityNested(); - const removeLiquidityInput: - | RemoveLiquidityNestedProportionalInput - | RemoveLiquidityNestedSingleTokenInput = { - bptAmountIn: amountIn, - chainId, - rpcUrl, - tokenOut, - }; - const queryOutput = await removeLiquidityNested.query( - removeLiquidityInput, - nestedPoolFromApi, - ); - - // build remove liquidity call with expected minBpOut based on slippage - const slippage = Slippage.fromPercentage('1'); // 1% - - const signature = await Relayer.signRelayerApproval( - BALANCER_RELAYER[chainId], - testAddress, - client, - ); - - const { callData, to, minAmountsOut } = removeLiquidityNested.buildCall({ - ...queryOutput, - slippage, - accountAddress: testAddress, - relayerApprovalSignature: signature, - wethIsEth, - }); - - let tokensOut = minAmountsOut.map((a) => a.token); - if (wethIsEth) { - tokensOut = replaceWrapped(tokensOut, chainId); - } - - // send remove liquidity transaction and check balance changes - const { transactionReceipt, balanceDeltas } = - await sendTransactionGetBalances( - [ - queryOutput.bptAmountIn.token.address, - ...tokensOut.map((t) => t.address), - ], - client, - testAddress, - to, - callData, - ); - - const expectedDeltas = [ - queryOutput.bptAmountIn.amount, - ...queryOutput.amountsOut.map((amountOut) => amountOut.amount), - ]; - - return { - transactionReceipt, - expectedDeltas, - balanceDeltas, - amountsOut: queryOutput.amountsOut, - slippage, - minAmountsOut, - }; -}; - -/*********************** Mock To Represent API Requirements **********************/ - -class MockApi { - public async getNestedPool(poolId: Hex): Promise { - if ( - poolId !== - '0x08775ccb6674d6bdceb0797c364c2653ed84f3840002000000000000000004f0' - ) - throw Error(); - return { - protocolVersion: 3, - pools: [ - { - id: '0x08775ccb6674d6bdceb0797c364c2653ed84f3840002000000000000000004f0', - address: '0x08775ccb6674d6bdceb0797c364c2653ed84f384', - type: PoolType.Weighted, - level: 1, - tokens: [ - { - address: - '0x79c58f70905f734641735bc61e45c19dd9ad60bc', // 3POOL-BPT - decimals: 18, - index: 0, - }, - { - address: - '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // WETH - decimals: 18, - index: 1, - }, - ], - }, - { - id: '0x79c58f70905f734641735bc61e45c19dd9ad60bc0000000000000000000004e7', - address: '0x79c58f70905f734641735bc61e45c19dd9ad60bc', - type: PoolType.ComposableStable, - level: 0, - tokens: [ - { - address: - '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI - decimals: 18, - index: 0, - }, - { - address: - '0x79c58f70905f734641735bc61e45c19dd9ad60bc', // 3POOL-BPT - decimals: 18, - index: 1, - }, - { - address: - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC - decimals: 6, - index: 2, - }, - { - address: - '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT - decimals: 6, - index: 3, - }, - ], - }, - ], - mainTokens: [ - { - address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // WETH - decimals: 18, - }, - { - address: '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI - decimals: 18, - }, - { - address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC - decimals: 6, - }, - { - address: '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT - decimals: 6, - }, - ], - }; - } -} - -function assertResults( - transactionReceipt: TransactionReceipt, - expectedDeltas: bigint[], - balanceDeltas: bigint[], - amountsOut: TokenAmount[], - slippage: Slippage, - minAmountsOut: TokenAmount[], -) { - expect(transactionReceipt.status).to.eq('success'); - amountsOut.map((amountOut) => expect(amountOut.amount > 0n).to.be.true); - expect(expectedDeltas).to.deep.eq(balanceDeltas); - const expectedMinAmountsOut = amountsOut.map((amountOut) => - slippage.applyTo(amountOut.amount, -1), - ); - expect(expectedMinAmountsOut).to.deep.eq( - minAmountsOut.map((a) => a.amount), - ); -} -/******************************************************************************/ From 237304d0c62092cd7310d317b4b2a0c98c966ea0 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 24 Oct 2024 09:59:45 +0100 Subject: [PATCH 07/10] feat: Added support for V3 nested remove, direct approvals. Note - currently failing, assuming because of router. --- .../removeLiquidityNestedV2/index.ts | 3 - .../removeLiquidityNestedV3/index.ts | 115 ++++++++++++++- .../removeLiquidityNestedV3/types.ts | 2 + ...emoveLiquidityNestedV3.integration.test.ts | 138 ++++++++++++++++-- 4 files changed, 238 insertions(+), 20 deletions(-) diff --git a/src/entities/removeLiquidityNested/removeLiquidityNestedV2/index.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/index.ts index 481b539d..a5529123 100644 --- a/src/entities/removeLiquidityNested/removeLiquidityNestedV2/index.ts +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV2/index.ts @@ -7,7 +7,6 @@ import { BALANCER_RELAYER, ZERO_ADDRESS } from '../../../utils'; import { Relayer } from '../../relayer'; import { TokenAmount } from '../../tokenAmount'; import { NestedPoolState } from '../../types'; -import { validateNestedPoolState } from '../../utils'; import { encodeCalls } from './encodeCalls'; import { doRemoveLiquidityNestedQuery } from './doRemoveLiquidityNestedQuery'; @@ -29,8 +28,6 @@ export class RemoveLiquidityNestedV2 { nestedPoolState: NestedPoolState, ): Promise { const isProportional = validateQueryInput(input, nestedPoolState); - validateNestedPoolState(nestedPoolState); - const { callsAttributes, bptAmountIn } = getQueryCallsAttributes( input, nestedPoolState.pools, diff --git a/src/entities/removeLiquidityNested/removeLiquidityNestedV3/index.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV3/index.ts index 724de4a0..999f958b 100644 --- a/src/entities/removeLiquidityNested/removeLiquidityNestedV3/index.ts +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV3/index.ts @@ -1,3 +1,11 @@ +import { + Address, + createPublicClient, + encodeFunctionData, + Hex, + http, + zeroAddress, +} from 'viem'; import { NestedPoolState } from '@/entities/types'; import { RemoveLiquidityNestedCallInputV3, @@ -5,18 +13,115 @@ import { RemoveLiquidityNestedQueryOutputV3, } from './types'; import { RemoveLiquidityNestedBuildCallOutput } from '../types'; +import { BALANCER_COMPOSITE_LIQUIDITY_ROUTER, CHAINS } from '@/utils'; +import { + balancerCompositeLiquidityRouterAbi, + permit2Abi, + vaultExtensionAbi_V3, + vaultV3Abi, +} from '@/abi'; +import { Token } from '@/entities/token'; +import { TokenAmount } from '@/entities/tokenAmount'; export class RemoveLiquidityNestedV3 { async query( - _input: RemoveLiquidityNestedProportionalInputV3, - _nestedPoolState: NestedPoolState, + input: RemoveLiquidityNestedProportionalInputV3, + nestedPoolState: NestedPoolState, ): Promise { - return {} as RemoveLiquidityNestedQueryOutputV3; + // Address of the highest level pool (which contains BPTs of other pools), i.e. the pool we wish to join + const parentPool = nestedPoolState.pools.reduce((max, curr) => + curr.level > max.level ? curr : max, + ); + // query function input, `tokensIn` array, must have all tokens from child pools + // and all tokens that are not BPTs from the nested pool (parent pool). + const mainTokens = nestedPoolState.mainTokens.map( + (t) => new Token(input.chainId, t.address, t.decimals), + ); + + const bptToken = new Token(input.chainId, parentPool.address, 18); + + const amountsOut = + await this.doQueryRemoveLiquidityProportionalNestedPool( + input, + parentPool.address, + input.bptAmountIn, + mainTokens.map((t) => t.address), + input.sender ?? zeroAddress, + input.userData ?? '0x', + ); + + return { + protocolVersion: 3, + bptAmountIn: TokenAmount.fromRawAmount(bptToken, input.bptAmountIn), + chainId: input.chainId, + parentPool: parentPool.address, + userData: input.userData ?? '0x', + amountsOut: amountsOut.map((a, i) => + TokenAmount.fromRawAmount(mainTokens[i], a), + ), + }; } buildCall( - _input: RemoveLiquidityNestedCallInputV3, + input: RemoveLiquidityNestedCallInputV3, ): RemoveLiquidityNestedBuildCallOutput { - return {} as RemoveLiquidityNestedBuildCallOutput; + // validateBuildCallInput(input); TODO - Add this like V2 once weth/native is allowed + + // apply slippage to amountsOut + const minAmountsOut = input.amountsOut.map((amountOut) => + TokenAmount.fromRawAmount( + amountOut.token, + input.slippage.applyTo(amountOut.amount, -1), + ), + ); + + const callData = encodeFunctionData({ + abi: balancerCompositeLiquidityRouterAbi, + functionName: 'removeLiquidityProportionalNestedPool', + args: [ + input.parentPool, + input.bptAmountIn.amount, + minAmountsOut.map((a) => a.token.address), + minAmountsOut.map((a) => a.amount), + input.userData, + ], + }); + return { + callData, + to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[input.chainId], + minAmountsOut, + } as RemoveLiquidityNestedBuildCallOutput; } + + private doQueryRemoveLiquidityProportionalNestedPool = async ( + { rpcUrl, chainId }: RemoveLiquidityNestedProportionalInputV3, + parentPool: Address, + exactBptAmountIn: bigint, + tokensOut: Address[], + _sender: Address, + userData: Hex, + ) => { + const client = createPublicClient({ + transport: http(rpcUrl), + chain: CHAINS[chainId], + }); + + const { result: amountsOut } = await client.simulateContract({ + address: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + abi: [ + ...balancerCompositeLiquidityRouterAbi, + ...vaultV3Abi, + ...vaultExtensionAbi_V3, + ...permit2Abi, + ], + functionName: 'queryRemoveLiquidityProportionalNestedPool', + args: [ + parentPool, + exactBptAmountIn, + tokensOut, + /*sender,*/ userData, + ], + }); + return amountsOut; + }; } diff --git a/src/entities/removeLiquidityNested/removeLiquidityNestedV3/types.ts b/src/entities/removeLiquidityNested/removeLiquidityNestedV3/types.ts index 141d4b0c..76fdf9d5 100644 --- a/src/entities/removeLiquidityNested/removeLiquidityNestedV3/types.ts +++ b/src/entities/removeLiquidityNested/removeLiquidityNestedV3/types.ts @@ -7,6 +7,8 @@ export type RemoveLiquidityNestedProportionalInputV3 = { bptAmountIn: bigint; chainId: ChainId; rpcUrl: string; + sender?: Address; + userData?: Hex; }; export type RemoveLiquidityNestedQueryOutputV3 = { diff --git a/test/v3/removeLiquidityNested/removeLiquidityNestedV3.integration.test.ts b/test/v3/removeLiquidityNested/removeLiquidityNestedV3.integration.test.ts index 87653f31..fa74d60f 100644 --- a/test/v3/removeLiquidityNested/removeLiquidityNestedV3.integration.test.ts +++ b/test/v3/removeLiquidityNested/removeLiquidityNestedV3.integration.test.ts @@ -20,10 +20,25 @@ import { Token, RemoveLiquidityNestedInput, RemoveLiquidityNested, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER, + TokenAmount, + Slippage, + balancerCompositeLiquidityRouterAbi, + vaultAdminAbi_V3, + vaultExtensionAbi_V3, + vaultV3Abi, + permit2Abi, } from 'src'; import { ANVIL_NETWORKS, startFork } from 'test/anvil/anvil-global-setup'; -import { POOLS, setTokenBalances, TOKENS } from 'test/lib/utils'; +import { + approveSpenderOnToken, + POOLS, + sendTransactionGetBalances, + setTokenBalances, + TOKENS, +} from 'test/lib/utils'; +import { RemoveLiquidityNestedCallInputV3 } from '@/entities/removeLiquidityNested/removeLiquidityNestedV3/types'; const chainId = ChainId.SEPOLIA; const NESTED_WITH_BOOSTED_POOL = POOLS[chainId].NESTED_WITH_BOOSTED_POOL; @@ -37,6 +52,11 @@ const parentBptToken = new Token( NESTED_WITH_BOOSTED_POOL.address, NESTED_WITH_BOOSTED_POOL.decimals, ); +// These are the underlying tokens +const daiToken = new Token(chainId, DAI.address, DAI.decimals); +const usdcToken = new Token(chainId, USDC.address, USDC.decimals); +const wethToken = new Token(chainId, WETH.address, WETH.decimals); +const mainTokens = [wethToken, daiToken, usdcToken]; describe('V3 remove liquidity nested test, with Permit direct approval', () => { let rpcUrl: string; @@ -45,7 +65,7 @@ describe('V3 remove liquidity nested test, with Permit direct approval', () => { const removeLiquidityNested = new RemoveLiquidityNested(); beforeAll(async () => { - ({ rpcUrl } = await startFork(ANVIL_NETWORKS.MAINNET)); + ({ rpcUrl } = await startFork(ANVIL_NETWORKS.SEPOLIA)); client = createTestClient({ mode: 'anvil', @@ -68,23 +88,117 @@ describe('V3 remove liquidity nested test, with Permit direct approval', () => { }); test('query with underlying', async () => { - const addLiquidityInput: RemoveLiquidityNestedInput = { - bptAmountIn: parseUnits('7', 18), + const removeLiquidityInput: RemoveLiquidityNestedInput = { + bptAmountIn: parseUnits('0.7', 18), chainId, rpcUrl, }; const queryOutput = await removeLiquidityNested.query( - addLiquidityInput, + removeLiquidityInput, nestedPoolState, ); console.log(queryOutput); - /* - protocolVersion: 1 | 2 | 3; - callsAttributes: RemoveLiquidityNestedCallAttributes[]; - bptAmountIn: TokenAmount; - amountsOut: TokenAmount[]; - isProportional: boolean; - chainId: ChainId;*/ + // TODO add tests + }); + + test('remove liquidity transaction, direct approval on router', async () => { + const removeLiquidityInput: RemoveLiquidityNestedInput = { + bptAmountIn: parseUnits('0.7', 18), + chainId, + rpcUrl, + }; + await approveSpenderOnToken( + client, + testAddress, + parentBptToken.address, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + ); + + // TODO - Add this once on Deploy10 + // const queryOutput = await removeLiquidityNested.query( + // removeLiquidityInput, + // nestedPoolState, + // ); + + // TODO remove once we have real query + const queryOutput = { + protocolVersion: 3, + bptAmountIn: TokenAmount.fromRawAmount( + parentBptToken, + removeLiquidityInput.bptAmountIn, + ), + amountsOut: mainTokens.map((t) => + TokenAmount.fromHumanAmount(t, '0.000000001'), + ), + chainId, + parentPool: parentBptToken.address, + userData: '0x', + }; + + const removeLiquidityBuildInput = { + ...queryOutput, + slippage: Slippage.fromPercentage('1'), // 1%, + } as RemoveLiquidityNestedCallInputV3; + + const addLiquidityBuildCallOutput = removeLiquidityNested.buildCall( + removeLiquidityBuildInput, + ); + + // TODO - this is just for debug + // Currently reverting with: + // Error: ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed) + // 0x89ca59bc46c00d90c496fc99f16668b00dd6b5cc, 0, 700000000000000000 + // Assuming this is a router bug fixed in deploy10 as we are set allowance earlier + await client.simulateContract({ + address: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + abi: [ + ...balancerCompositeLiquidityRouterAbi, + ...vaultAdminAbi_V3, + ...vaultV3Abi, + ...vaultExtensionAbi_V3, + ...permit2Abi, + ], + functionName: 'removeLiquidityProportionalNestedPool', + args: [ + '0xee76b8f75e20d4bb9eb483cdec176dfc8d02bb3a', + 700000000000000000n, + [ + '0x7b79995e5f793a07bc00c21412e50ecae098e7f9', + '0xff34b3d4aee8ddcd6f9afffb6fe49bd371b8a357', + '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8', + ], + [990000000n, 990000000n, 0n], + '0x', + ], + }); + + // send remove liquidity transaction and check balance changes + const { transactionReceipt, balanceDeltas } = + await sendTransactionGetBalances( + [ + queryOutput.bptAmountIn.token.address, + ...mainTokens.map((t) => t.address), + ], + client, + testAddress, + addLiquidityBuildCallOutput.to, + addLiquidityBuildCallOutput.callData, + ); + expect(transactionReceipt.status).to.eq('success'); + const expectedDeltas = [ + queryOutput.bptAmountIn.amount, + ...queryOutput.amountsOut.map((amountOut) => amountOut.amount), + ]; + queryOutput.amountsOut.map( + (amountOut) => expect(amountOut.amount > 0n).to.be.true, + ); + expect(expectedDeltas).to.deep.eq(balanceDeltas); + const expectedMinAmountsOut = queryOutput.amountsOut.map((amountOut) => + removeLiquidityBuildInput.slippage.applyTo(amountOut.amount, -1), + ); + expect(expectedMinAmountsOut).to.deep.eq( + addLiquidityBuildCallOutput.minAmountsOut.map((a) => a.amount), + ); }); }); From 11db1982dbff76f68c01ecc6cc39ebcdf49955e8 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 24 Oct 2024 10:19:03 +0100 Subject: [PATCH 08/10] feat: Support for V3 remove nested with signature - note tests failing, possibly due to router. --- src/entities/permitHelper/index.ts | 35 ++- src/entities/removeLiquidityNested/index.ts | 29 +- ...idityNestedV3Signature.integration.test.ts | 265 ++++++++++++++++++ 3 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 test/v3/removeLiquidityNested/removeLiquidityNestedV3Signature.integration.test.ts diff --git a/src/entities/permitHelper/index.ts b/src/entities/permitHelper/index.ts index f61b875a..ee7238b5 100644 --- a/src/entities/permitHelper/index.ts +++ b/src/entities/permitHelper/index.ts @@ -1,9 +1,15 @@ import { weightedPoolAbi_V3 } from '@/abi'; import { Hex } from '@/types'; -import { BALANCER_ROUTER, MAX_UINT256, PublicWalletClient } from '@/utils'; +import { + BALANCER_ROUTER, + ChainId, + MAX_UINT256, + PublicWalletClient, +} from '@/utils'; import { getNonce } from './helper'; import { RemoveLiquidityBaseBuildCallInput } from '../removeLiquidity/types'; import { getAmountsCall } from '../removeLiquidity/helper'; +import { TokenAmount } from '../tokenAmount'; type PermitApproval = { /** Address of the token to approve */ @@ -53,6 +59,33 @@ export class PermitHelper { ); return { batch: [permitApproval], signatures: [permitSignature] }; }; + + static signRemoveLiquidityNestedApproval = async (input: { + bptAmountIn: TokenAmount; + chainId: ChainId; + client: PublicWalletClient; + owner: Hex; + nonce?: bigint; + deadline?: bigint; + }): Promise => { + const nonce = + input.nonce ?? + (await getNonce( + input.client, + input.bptAmountIn.token.address, + input.owner, + )); + const { permitApproval, permitSignature } = await signPermit( + input.client, + input.bptAmountIn.token.address, + input.owner, + BALANCER_ROUTER[input.chainId], + nonce, + input.bptAmountIn.amount, // maxBptIn + input.deadline, + ); + return { batch: [permitApproval], signatures: [permitSignature] }; + }; } /** diff --git a/src/entities/removeLiquidityNested/index.ts b/src/entities/removeLiquidityNested/index.ts index 048a40e3..c809563c 100644 --- a/src/entities/removeLiquidityNested/index.ts +++ b/src/entities/removeLiquidityNested/index.ts @@ -1,4 +1,4 @@ -import { NestedPoolState } from '@/entities'; +import { NestedPoolState, Permit } from '@/entities'; import { validateNestedPoolState } from '@/entities/utils'; import { RemoveLiquidityNestedV2 } from './removeLiquidityNestedV2'; import { @@ -9,6 +9,8 @@ import { } from './types'; import { RemoveLiquidityNestedV3 } from './removeLiquidityNestedV3'; import { validateBuildCallInput } from './removeLiquidityNestedV2/validateInputs'; +import { encodeFunctionData, zeroAddress } from 'viem'; +import { balancerCompositeLiquidityRouterAbi } from '@/abi'; export class RemoveLiquidityNested { async query( @@ -48,4 +50,29 @@ export class RemoveLiquidityNested { } } } + + public buildCallWithPermit( + input: RemoveLiquidityNestedCallInput, + permit: Permit, + ): RemoveLiquidityNestedBuildCallOutput { + const buildCallOutput = this.buildCall(input); + + const args = [ + permit.batch, + permit.signatures, + { details: [], spender: zeroAddress, sigDeadline: 0n }, + '0x', + [buildCallOutput.callData], + ] as const; + const callData = encodeFunctionData({ + abi: balancerCompositeLiquidityRouterAbi, + functionName: 'permitBatchAndCall', + args, + }); + + return { + ...buildCallOutput, + callData, + }; + } } diff --git a/test/v3/removeLiquidityNested/removeLiquidityNestedV3Signature.integration.test.ts b/test/v3/removeLiquidityNested/removeLiquidityNestedV3Signature.integration.test.ts new file mode 100644 index 00000000..753e0e58 --- /dev/null +++ b/test/v3/removeLiquidityNested/removeLiquidityNestedV3Signature.integration.test.ts @@ -0,0 +1,265 @@ +// pnpm test -- removeLiquidityNestedV3Signature.integration.test.ts +import dotenv from 'dotenv'; +dotenv.config(); + +import { + createTestClient, + http, + parseUnits, + publicActions, + TestActions, + walletActions, +} from 'viem'; + +import { + Address, + ChainId, + CHAINS, + NestedPoolState, + PublicWalletClient, + Token, + RemoveLiquidityNestedInput, + RemoveLiquidityNested, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER, + TokenAmount, + Slippage, + PermitHelper, + balancerCompositeLiquidityRouterAbi, + vaultAdminAbi_V3, + vaultV3Abi, + vaultExtensionAbi_V3, + permit2Abi, +} from 'src'; + +import { ANVIL_NETWORKS, startFork } from 'test/anvil/anvil-global-setup'; +import { + approveSpenderOnToken, + POOLS, + sendTransactionGetBalances, + setTokenBalances, + TOKENS, +} from 'test/lib/utils'; +import { RemoveLiquidityNestedCallInputV3 } from '@/entities/removeLiquidityNested/removeLiquidityNestedV3/types'; + +const chainId = ChainId.SEPOLIA; +const NESTED_WITH_BOOSTED_POOL = POOLS[chainId].NESTED_WITH_BOOSTED_POOL; +const BOOSTED_POOL = POOLS[chainId].MOCK_BOOSTED_POOL; +const DAI = TOKENS[chainId].DAI_AAVE; +const USDC = TOKENS[chainId].USDC_AAVE; +const WETH = TOKENS[chainId].WETH; + +const parentBptToken = new Token( + chainId, + NESTED_WITH_BOOSTED_POOL.address, + NESTED_WITH_BOOSTED_POOL.decimals, +); +// These are the underlying tokens +const daiToken = new Token(chainId, DAI.address, DAI.decimals); +const usdcToken = new Token(chainId, USDC.address, USDC.decimals); +const wethToken = new Token(chainId, WETH.address, WETH.decimals); +const mainTokens = [wethToken, daiToken, usdcToken]; + +describe('V3 remove liquidity nested test, with Permit signature', () => { + let rpcUrl: string; + let client: PublicWalletClient & TestActions; + let testAddress: Address; + const removeLiquidityNested = new RemoveLiquidityNested(); + + beforeAll(async () => { + ({ rpcUrl } = await startFork(ANVIL_NETWORKS.SEPOLIA)); + + client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl), + }) + .extend(publicActions) + .extend(walletActions); + + testAddress = (await client.getAddresses())[0]; + + // Mint BPT to testAddress + await setTokenBalances( + client, + testAddress, + [parentBptToken.address], + [0], + [parseUnits('10', 18)], + ); + }); + + test('remove liquidity transaction, direct approval on router', async () => { + const removeLiquidityInput: RemoveLiquidityNestedInput = { + bptAmountIn: parseUnits('0.7', 18), + chainId, + rpcUrl, + }; + await approveSpenderOnToken( + client, + testAddress, + parentBptToken.address, + BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + ); + + // TODO - Add this once on Deploy10 + // const queryOutput = await removeLiquidityNested.query( + // removeLiquidityInput, + // nestedPoolState, + // ); + + // TODO remove once we have real query + const queryOutput = { + protocolVersion: 3, + bptAmountIn: TokenAmount.fromRawAmount( + parentBptToken, + removeLiquidityInput.bptAmountIn, + ), + amountsOut: mainTokens.map((t) => + TokenAmount.fromHumanAmount(t, '0.000000001'), + ), + chainId, + parentPool: parentBptToken.address, + userData: '0x', + }; + + const removeLiquidityBuildInput = { + ...queryOutput, + slippage: Slippage.fromPercentage('1'), // 1%, + } as RemoveLiquidityNestedCallInputV3; + + const permit = await PermitHelper.signRemoveLiquidityNestedApproval({ + ...removeLiquidityBuildInput, + client, + owner: testAddress, + }); + + const addLiquidityBuildCallOutput = + removeLiquidityNested.buildCallWithPermit( + removeLiquidityBuildInput, + permit, + ); + + // TODO - used for debug atm. Remove once deploy10 hopefully fixes things + // const args = [ + // [ + // { + // token: '0xee76b8f75e20d4bb9eb483cdec176dfc8d02bb3a', + // owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + // spender: '0x77eDc69766409C599F06Ef0B551a0990CBfe13A7', + // amount: 700000000000000000n, + // nonce: 0n, + // deadline: + // 115792089237316195423570985008687907853269984665640564039457584007913129639935n, + // }, + // ], + // [ + // '0xcbe54f21bde6d544980f94a7545b1ec10b2690d1de8ede996a77126b00ce20fc5be8b982ad79d45620631e10d4715dd56673a880fc392314da46f27af4841e901b', + // ], + // { + // details: [], + // spender: '0x0000000000000000000000000000000000000000', + // sigDeadline: 0n, + // }, + // '0x', + // [ + // '0xcb25ee65000000000000000000000000ee76b8f75e20d4bb9eb483cdec176dfc8d02bb3a00000000000000000000000000000000000000000000000009b6e64a8ec6000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000030000000000000000000000007b79995e5f793a07bc00c21412e50ecae098e7f9000000000000000000000000ff34b3d4aee8ddcd6f9afffb6fe49bd371b8a35700000000000000000000000094a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c80000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000003b023380000000000000000000000000000000000000000000000000000000003b02338000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + // ], + // ] as const; + // await client.simulateContract({ + // address: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId], + // abi: [ + // ...balancerCompositeLiquidityRouterAbi, + // ...vaultAdminAbi_V3, + // ...vaultV3Abi, + // ...vaultExtensionAbi_V3, + // ...permit2Abi, + // ], + // functionName: 'permitBatchAndCall', + // args, + // }); + + // send remove liquidity transaction and check balance changes + const { transactionReceipt, balanceDeltas } = + await sendTransactionGetBalances( + [ + queryOutput.bptAmountIn.token.address, + ...mainTokens.map((t) => t.address), + ], + client, + testAddress, + addLiquidityBuildCallOutput.to, + addLiquidityBuildCallOutput.callData, + ); + expect(transactionReceipt.status).to.eq('success'); + const expectedDeltas = [ + queryOutput.bptAmountIn.amount, + ...queryOutput.amountsOut.map((amountOut) => amountOut.amount), + ]; + queryOutput.amountsOut.map( + (amountOut) => expect(amountOut.amount > 0n).to.be.true, + ); + expect(expectedDeltas).to.deep.eq(balanceDeltas); + const expectedMinAmountsOut = queryOutput.amountsOut.map((amountOut) => + removeLiquidityBuildInput.slippage.applyTo(amountOut.amount, -1), + ); + expect(expectedMinAmountsOut).to.deep.eq( + addLiquidityBuildCallOutput.minAmountsOut.map((a) => a.amount), + ); + }); +}); + +const nestedPoolState: NestedPoolState = { + protocolVersion: 3, + pools: [ + { + id: NESTED_WITH_BOOSTED_POOL.id, + address: NESTED_WITH_BOOSTED_POOL.address, + type: NESTED_WITH_BOOSTED_POOL.type, + level: 1, + tokens: [ + { + address: BOOSTED_POOL.address, + decimals: BOOSTED_POOL.decimals, + index: 0, + }, + { + address: WETH.address, + decimals: WETH.decimals, + index: 1, + }, + ], + }, + { + id: BOOSTED_POOL.id, + address: BOOSTED_POOL.address, + type: BOOSTED_POOL.type, + level: 0, + tokens: [ + { + address: USDC.address, + decimals: USDC.decimals, + index: 0, + }, + { + address: DAI.address, + decimals: DAI.decimals, + index: 1, + }, + ], + }, + ], + mainTokens: [ + { + address: WETH.address, + decimals: WETH.decimals, + }, + { + address: DAI.address, + decimals: DAI.decimals, + }, + { + address: USDC.address, + decimals: USDC.decimals, + }, + ], +}; From be5b564ede549611f95cb91248ded894f232b170 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 24 Oct 2024 10:22:37 +0100 Subject: [PATCH 09/10] chore: Fix lint. --- ...veLiquidityNestedV3Signature.integration.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/v3/removeLiquidityNested/removeLiquidityNestedV3Signature.integration.test.ts b/test/v3/removeLiquidityNested/removeLiquidityNestedV3Signature.integration.test.ts index 753e0e58..03d018b8 100644 --- a/test/v3/removeLiquidityNested/removeLiquidityNestedV3Signature.integration.test.ts +++ b/test/v3/removeLiquidityNested/removeLiquidityNestedV3Signature.integration.test.ts @@ -24,11 +24,12 @@ import { TokenAmount, Slippage, PermitHelper, - balancerCompositeLiquidityRouterAbi, - vaultAdminAbi_V3, - vaultV3Abi, - vaultExtensionAbi_V3, - permit2Abi, + // TODO remove once debug finished + // balancerCompositeLiquidityRouterAbi, + // vaultAdminAbi_V3, + // vaultV3Abi, + // vaultExtensionAbi_V3, + // permit2Abi, } from 'src'; import { ANVIL_NETWORKS, startFork } from 'test/anvil/anvil-global-setup'; @@ -208,7 +209,7 @@ describe('V3 remove liquidity nested test, with Permit signature', () => { }); }); -const nestedPoolState: NestedPoolState = { +const _nestedPoolState: NestedPoolState = { protocolVersion: 3, pools: [ { From 8d4d0eec75c1ab3912b33858f455d6175f3d42b2 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 24 Oct 2024 15:21:41 +0100 Subject: [PATCH 10/10] chore: Changeset. --- .changeset/chilly-donuts-rule.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/chilly-donuts-rule.md diff --git a/.changeset/chilly-donuts-rule.md b/.changeset/chilly-donuts-rule.md new file mode 100644 index 00000000..587f0810 --- /dev/null +++ b/.changeset/chilly-donuts-rule.md @@ -0,0 +1,5 @@ +--- +"@balancer/sdk": minor +--- + +Add add/remove support for V3 nested pools.