From 4cde07da6589cefc48965631af3619f4ccac0049 Mon Sep 17 00:00:00 2001 From: Bruno Eidam Guerios Date: Mon, 11 Nov 2024 14:55:30 -0300 Subject: [PATCH 1/9] Refactor nestedPoolState logic to fetch mainTokens from api --- .../modules/nested-pool-state/index.ts | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/data/providers/balancer-api/modules/nested-pool-state/index.ts b/src/data/providers/balancer-api/modules/nested-pool-state/index.ts index 4dcbe52d..a14df892 100644 --- a/src/data/providers/balancer-api/modules/nested-pool-state/index.ts +++ b/src/data/providers/balancer-api/modules/nested-pool-state/index.ts @@ -14,11 +14,6 @@ export type PoolGetPool = { protocolVersion: 1 | 2 | 3; address: Address; type: string; - allTokens: { - address: Address; - decimals: number; - isMainToken: boolean; - }[]; poolTokens: Token[]; }; @@ -48,11 +43,6 @@ export class NestedPools { protocolVersion address type - allTokens { - address - decimals - isMainToken - } poolTokens { index address @@ -258,14 +248,25 @@ export function mapPoolToNestedPoolStateV2(pool: PoolGetPool): NestedPoolState { }); }); - const mainTokens = pool.allTokens - .filter((t) => t.isMainToken) - .map((t) => { - return { - address: t.address, - decimals: t.decimals, - }; - }); + // mainTokens are pool tokens filtering out nested pools and phantomBPTs + const mainTokens = pool.poolTokens.flatMap((t) => { + if (t.nestedPool) { + const nestedPool = t.nestedPool; + return nestedPool.tokens + .filter((t) => t.address !== nestedPool.address) // remove phantomBPT + .map((t) => { + return { + address: t.address, + decimals: t.decimals, + }; + }); + } + + return { + address: t.address, + decimals: t.decimals, + }; + }); return { protocolVersion: 2, From c5e3ffd14b128131e66b0b476b5a163c25f74900 Mon Sep 17 00:00:00 2001 From: Bruno Eidam Guerios Date: Mon, 11 Nov 2024 14:55:56 -0300 Subject: [PATCH 2/9] Add changeset --- .changeset/fuzzy-coins-sin.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fuzzy-coins-sin.md diff --git a/.changeset/fuzzy-coins-sin.md b/.changeset/fuzzy-coins-sin.md new file mode 100644 index 00000000..2053c322 --- /dev/null +++ b/.changeset/fuzzy-coins-sin.md @@ -0,0 +1,5 @@ +--- +"@balancer/sdk": patch +--- + +Refactor nestedPoolState logic to fetch mainTokens from api From cf3917968ce7267b3b3371dbace845a9773f6855 Mon Sep 17 00:00:00 2001 From: Bruno Eidam Guerios Date: Mon, 11 Nov 2024 16:18:06 -0300 Subject: [PATCH 3/9] Fix getPoolStateWithBalancesV3 with less 18 decimals tokens --- .../utils/getPoolStateWithBalancesV3.ts | 11 +- ...oolStateWithBalancesV3.integration.test.ts | 101 ++++++++++++++++++ 2 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 test/v3/utils/getPoolStateWithBalancesV3.integration.test.ts diff --git a/src/entities/utils/getPoolStateWithBalancesV3.ts b/src/entities/utils/getPoolStateWithBalancesV3.ts index 10403119..a6fe51be 100644 --- a/src/entities/utils/getPoolStateWithBalancesV3.ts +++ b/src/entities/utils/getPoolStateWithBalancesV3.ts @@ -6,6 +6,7 @@ import { CHAINS, VAULT_V3 } from '@/utils'; import { getSortedTokens } from './getSortedTokens'; import { PoolState, PoolStateWithBalances } from '../types'; import { vaultExtensionAbi_V3 } from '@/abi'; +import { TokenAmount } from '../tokenAmount'; export const getPoolStateWithBalancesV3 = async ( poolState: PoolState, @@ -40,9 +41,12 @@ export const getPoolStateWithBalancesV3 = async ( } const totalShares = outputs[0].result as bigint; - const balances = outputs[1].result as bigint[]; + const balancesScale18 = outputs[1].result as bigint[]; const sortedTokens = getSortedTokens(poolState.tokens, chainId); + const balances = sortedTokens.map((token, i) => + TokenAmount.fromScale18Amount(token, balancesScale18[i]), + ); const poolStateWithBalances: PoolStateWithBalances = { ...poolState, @@ -50,7 +54,10 @@ export const getPoolStateWithBalancesV3 = async ( address: token.address, decimals: token.decimals, index: i, - balance: formatUnits(balances[i], token.decimals) as HumanAmount, + balance: formatUnits( + balances[i].amount, + token.decimals, + ) as HumanAmount, })), totalShares: formatEther(totalShares) as HumanAmount, }; diff --git a/test/v3/utils/getPoolStateWithBalancesV3.integration.test.ts b/test/v3/utils/getPoolStateWithBalancesV3.integration.test.ts new file mode 100644 index 00000000..e97bdde9 --- /dev/null +++ b/test/v3/utils/getPoolStateWithBalancesV3.integration.test.ts @@ -0,0 +1,101 @@ +// pnpm test -- v3/utils/getPoolStateWithBalancesV3.integration.test.ts + +import { config } from 'dotenv'; +config(); + +import { + Hex, + PoolState, + ChainId, + PoolType, + getPoolStateWithBalancesV3, + PoolStateWithBalances, +} from '@/index'; +import { POOLS, TOKENS } from '../../lib/utils'; +import { ANVIL_NETWORKS, startFork } from '../../anvil/anvil-global-setup'; + +const protocolVersion = 3; + +const chainId = ChainId.SEPOLIA; + +const poolId = POOLS[chainId].MOCK_USDC_DAI_POOL.id; +const USDC = TOKENS[chainId].USDC; +const DAI = TOKENS[chainId].DAI; + +describe('add liquidity test', () => { + let poolState: PoolState; + let rpcUrl: string; + + beforeAll(async () => { + // setup mock api + const api = new MockApi(); + + // get pool state from api + poolState = await api.getPool(poolId); + + ({ rpcUrl } = await startFork( + ANVIL_NETWORKS[ChainId[chainId]], + undefined, + 7057106n, + )); + }); + + describe('getPoolStateWithBalancesV3', () => { + test('<18 decimals tokens', async () => { + const poolStateWithBalances = await getPoolStateWithBalancesV3( + poolState, + chainId, + rpcUrl, + ); + + const mockData: PoolStateWithBalances = { + ...poolState, + tokens: [ + { + address: USDC.address, + decimals: USDC.decimals, + index: 0, + balance: '9585.21526', + }, + { + address: DAI.address, + decimals: DAI.decimals, + index: 1, + balance: '10256.288668913000293429', + }, + ], + totalShares: '9912.817276660069114899', + }; + + expect(poolStateWithBalances).to.deep.eq(mockData); + }); + }); +}); + +/*********************** Mock To Represent API Requirements **********************/ +class MockApi { + public async getPool(id: Hex): Promise { + const tokens = [ + { + address: USDC.address, + decimals: USDC.decimals, + index: 0, + }, + { + address: DAI.address, + decimals: DAI.decimals, + index: 1, + }, + ]; + + return { + id, + address: id, + type: PoolType.Weighted, + tokens, + protocolVersion, + }; + } +} + +/******************************************************************************/ From 2712ee015d8d2d5f3ad90453707a4d501f0fea5c Mon Sep 17 00:00:00 2001 From: Bruno Eidam Guerios Date: Mon, 11 Nov 2024 16:18:25 -0300 Subject: [PATCH 4/9] Add changeset --- .changeset/pink-phones-suffer.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/pink-phones-suffer.md diff --git a/.changeset/pink-phones-suffer.md b/.changeset/pink-phones-suffer.md new file mode 100644 index 00000000..87ca46ac --- /dev/null +++ b/.changeset/pink-phones-suffer.md @@ -0,0 +1,5 @@ +--- +"@balancer/sdk": patch +--- + +Fix getPoolStateWithBalancesV3 with less 18 decimals tokens From f105cbb0d4f3995771d1e875096b4ef5c66bd95b Mon Sep 17 00:00:00 2001 From: Bruno Eidam Guerios Date: Tue, 12 Nov 2024 12:18:43 -0300 Subject: [PATCH 5/9] Refactor mainToken logic to improve readability --- .../modules/nested-pool-state/index.ts | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/src/data/providers/balancer-api/modules/nested-pool-state/index.ts b/src/data/providers/balancer-api/modules/nested-pool-state/index.ts index a14df892..5512f036 100644 --- a/src/data/providers/balancer-api/modules/nested-pool-state/index.ts +++ b/src/data/providers/balancer-api/modules/nested-pool-state/index.ts @@ -249,24 +249,9 @@ export function mapPoolToNestedPoolStateV2(pool: PoolGetPool): NestedPoolState { }); // mainTokens are pool tokens filtering out nested pools and phantomBPTs - const mainTokens = pool.poolTokens.flatMap((t) => { - if (t.nestedPool) { - const nestedPool = t.nestedPool; - return nestedPool.tokens - .filter((t) => t.address !== nestedPool.address) // remove phantomBPT - .map((t) => { - return { - address: t.address, - decimals: t.decimals, - }; - }); - } - - return { - address: t.address, - decimals: t.decimals, - }; - }); + const mainTokens = pools + .flatMap((p) => p.tokens) + .filter((t) => !pools.find((p) => p.address === t.address)); return { protocolVersion: 2, From 20fab7d244cc5c6df4195246cb20eeccaca0aac1 Mon Sep 17 00:00:00 2001 From: Bruno Eidam Guerios Date: Mon, 18 Nov 2024 14:13:47 -0300 Subject: [PATCH 6/9] Extract priceImpactABA to helper file --- .../addLiquidityUnbalancedBoosted.ts | 2 +- src/entities/priceImpact/helper.ts | 18 ++++++++++++++++++ src/entities/priceImpact/index.ts | 17 ++--------------- 3 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 src/entities/priceImpact/helper.ts diff --git a/src/entities/priceImpact/addLiquidityUnbalancedBoosted.ts b/src/entities/priceImpact/addLiquidityUnbalancedBoosted.ts index 98b84263..ae5a9919 100644 --- a/src/entities/priceImpact/addLiquidityUnbalancedBoosted.ts +++ b/src/entities/priceImpact/addLiquidityUnbalancedBoosted.ts @@ -5,7 +5,7 @@ import { RemoveLiquidityKind } from '../removeLiquidity/types'; import { Swap, SwapInput, TokenApi } from '../swap'; import { TokenAmount } from '../tokenAmount'; import { PoolStateWithUnderlyings, PoolTokenWithUnderlying } from '../types'; -import { priceImpactABA } from '.'; +import { priceImpactABA } from './helper'; import { AddLiquidityBoostedUnbalancedInput } from '../addLiquidityBoosted/types'; import { AddLiquidityBoostedV3 } from '../addLiquidityBoosted'; import { RemoveLiquidityBoostedV3 } from '../removeLiquidityBoosted'; diff --git a/src/entities/priceImpact/helper.ts b/src/entities/priceImpact/helper.ts new file mode 100644 index 00000000..a96f1a01 --- /dev/null +++ b/src/entities/priceImpact/helper.ts @@ -0,0 +1,18 @@ +import { MathSol } from '@/utils'; +import { PriceImpactAmount } from '../priceImpactAmount'; +import { TokenAmount } from '../tokenAmount'; + +/** + * Applies the ABA method to calculate the price impact of an operation. + * @param initialA amount of token A at the begginig of the ABA process, i.e. A -> B amountIn + * @param finalA amount of token A at the end of the ABA process, i.e. B -> A amountOut + * @returns + */ + +export const priceImpactABA = (initialA: TokenAmount, finalA: TokenAmount) => { + const priceImpact = MathSol.divDownFixed( + initialA.scale18 - finalA.scale18, + initialA.scale18 * 2n, + ); + return PriceImpactAmount.fromRawAmount(priceImpact); +}; diff --git a/src/entities/priceImpact/index.ts b/src/entities/priceImpact/index.ts index 5d228868..be6be47b 100644 --- a/src/entities/priceImpact/index.ts +++ b/src/entities/priceImpact/index.ts @@ -1,4 +1,4 @@ -import { MathSol, abs, max, min } from '../../utils'; +import { abs, max, min } from '../../utils'; import { SwapKind } from '../../types'; import { AddLiquidity } from '../addLiquidity'; @@ -27,6 +27,7 @@ import { AddLiquidityBoostedUnbalancedInput } from '../addLiquidityBoosted/types import { addLiquidityUnbalancedBoosted } from './addLiquidityUnbalancedBoosted'; import { addLiquidityNested } from './addLiquidityNested'; import { Token } from '../token'; +import { priceImpactABA } from './helper'; export class PriceImpact { /** @@ -393,17 +394,3 @@ export class PriceImpact { return priceImpactABA(bptAmountIn, bptOut); }; } - -/** - * Applies the ABA method to calculate the price impact of an operation. - * @param initialA amount of token A at the begginig of the ABA process, i.e. A -> B amountIn - * @param finalA amount of token A at the end of the ABA process, i.e. B -> A amountOut - * @returns - */ -export const priceImpactABA = (initialA: TokenAmount, finalA: TokenAmount) => { - const priceImpact = MathSol.divDownFixed( - initialA.scale18 - finalA.scale18, - initialA.scale18 * 2n, - ); - return PriceImpactAmount.fromRawAmount(priceImpact); -}; From f384994b6eafb87e9f1fba002dfef858e4227059 Mon Sep 17 00:00:00 2001 From: Bruno Eidam Guerios Date: Mon, 18 Nov 2024 14:14:34 -0300 Subject: [PATCH 7/9] Extract price impact for add liquidity unbalanced to fix circular dependency --- .../priceImpact/addLiquidityNested.ts | 7 +- .../priceImpact/addLiquidityUnbalanced.ts | 184 ++++++++++++++++++ src/entities/priceImpact/index.ts | 172 +--------------- 3 files changed, 190 insertions(+), 173 deletions(-) create mode 100644 src/entities/priceImpact/addLiquidityUnbalanced.ts diff --git a/src/entities/priceImpact/addLiquidityNested.ts b/src/entities/priceImpact/addLiquidityNested.ts index 12a6cc90..9ec46e34 100644 --- a/src/entities/priceImpact/addLiquidityNested.ts +++ b/src/entities/priceImpact/addLiquidityNested.ts @@ -13,11 +13,12 @@ import { AddLiquidityKind, AddLiquidityUnbalancedInput, } from '../addLiquidity/types'; -import { PriceImpact } from '.'; import { ChainId } from '@/utils'; import { TokenAmount } from '../tokenAmount'; import { AddLiquidityBoostedUnbalancedInput } from '../addLiquidityBoosted/types'; import { AddLiquidityBoostedV3 } from '../addLiquidityBoosted'; +import { addLiquidityUnbalanced } from './addLiquidityUnbalanced'; +import { addLiquidityUnbalancedBoosted } from './addLiquidityUnbalancedBoosted'; type AddResult = { priceImpactAmount: PriceImpactAmount; @@ -111,7 +112,7 @@ async function getAddUnbalancedResult( ...pool, protocolVersion, }; - const priceImpactAmount = await PriceImpact.addLiquidityUnbalanced( + const priceImpactAmount = await addLiquidityUnbalanced( addLiquidityInput, poolState, ); @@ -135,7 +136,7 @@ async function getAddBoostedUnbalancedResult( kind: AddLiquidityKind.Unbalanced, }; - const priceImpactAmount = await PriceImpact.addLiquidityUnbalancedBoosted( + const priceImpactAmount = await addLiquidityUnbalancedBoosted( addLiquidityInput, { ...pool, protocolVersion: 3 }, ); diff --git a/src/entities/priceImpact/addLiquidityUnbalanced.ts b/src/entities/priceImpact/addLiquidityUnbalanced.ts new file mode 100644 index 00000000..61bfd2ec --- /dev/null +++ b/src/entities/priceImpact/addLiquidityUnbalanced.ts @@ -0,0 +1,184 @@ +import { abs, max, min } from '@/utils'; +import { AddLiquidity } from '../addLiquidity'; +import { AddLiquidityUnbalancedInput } from '../addLiquidity/types'; +import { PriceImpactAmount } from '../priceImpactAmount'; +import { RemoveLiquidity } from '../removeLiquidity'; +import { + RemoveLiquidityInput, + RemoveLiquidityKind, +} from '../removeLiquidity/types'; +import { Token } from '../token'; +import { TokenAmount } from '../tokenAmount'; +import { PoolState } from '../types'; +import { priceImpactABA } from './helper'; +import { SwapKind } from '@/types'; +import { Swap, SwapInput } from '../swap'; + +export const addLiquidityUnbalanced = async ( + input: AddLiquidityUnbalancedInput, + poolState: PoolState, +): Promise => { + // inputs are being validated within AddLiquidity + + // simulate adding liquidity to get amounts in + const addLiquidity = new AddLiquidity(); + let amountsIn: TokenAmount[]; + let bptOut: TokenAmount; + let poolTokens: Token[]; + try { + const queryResult = await addLiquidity.query(input, poolState); + amountsIn = queryResult.amountsIn; + bptOut = queryResult.bptOut; + poolTokens = amountsIn.map((a) => a.token); + } catch (err) { + throw new Error( + `addLiquidityUnbalanced operation will fail at SC level with user defined input.\n${err}`, + ); + } + + // simulate removing liquidity to get amounts out + const removeLiquidity = new RemoveLiquidity(); + const removeLiquidityInput: RemoveLiquidityInput = { + chainId: input.chainId, + rpcUrl: input.rpcUrl, + bptIn: bptOut.toInputAmount(), + kind: RemoveLiquidityKind.Proportional, + }; + const { amountsOut } = await removeLiquidity.query( + removeLiquidityInput, + poolState, + ); + + // deltas between unbalanced and proportional amounts + const deltas = amountsOut.map((a, i) => a.amount - amountsIn[i].amount); + + // get how much BPT each delta would mint + const deltaBPTs: bigint[] = []; + for (let i = 0; i < deltas.length; i++) { + if (deltas[i] === 0n) { + deltaBPTs.push(0n); + } else { + try { + deltaBPTs.push(await queryAddLiquidityForTokenDelta(i)); + } catch (err) { + throw new Error( + `Unexpected error while calculating addLiquidityUnbalanced PI at Delta add step:\n${err}`, + ); + } + } + } + + // zero out deltas by swapping between tokens from proportionalAmounts + // to exactAmountsIn, leaving the remaining delta within a single token + let remainingDeltaIndex = 0; + if (deltaBPTs.some((deltaBPT) => deltaBPT !== 0n)) { + remainingDeltaIndex = await zeroOutDeltas(deltas, deltaBPTs); + } + + // get relevant amount for price impact calculation + const deltaAmount = TokenAmount.fromRawAmount( + amountsIn[remainingDeltaIndex].token, + abs(deltas[remainingDeltaIndex]), + ); + + // calculate price impact using ABA method + return priceImpactABA( + amountsIn[remainingDeltaIndex], + amountsIn[remainingDeltaIndex].sub(deltaAmount), + ); + + // helper functions + + async function zeroOutDeltas(deltas: bigint[], deltaBPTs: bigint[]) { + let minNegativeDeltaIndex = deltaBPTs.findIndex( + (deltaBPT) => deltaBPT === max(deltaBPTs.filter((a) => a < 0n)), + ); + const nonZeroDeltasBPTs = deltaBPTs.filter((d) => d !== 0n); + for (let i = 0; i < nonZeroDeltasBPTs.length - 1; i++) { + const minPositiveDeltaIndex = deltaBPTs.findIndex( + (deltaBPT) => deltaBPT === min(deltaBPTs.filter((a) => a > 0n)), + ); + minNegativeDeltaIndex = deltaBPTs.findIndex( + (deltaBPT) => deltaBPT === max(deltaBPTs.filter((a) => a < 0n)), + ); + + let swapKind: SwapKind; + let givenTokenIndex: number; + let resultTokenIndex: number; + let inputAmountRaw = 0n; + let outputAmountRaw = 0n; + if ( + deltaBPTs[minPositiveDeltaIndex] < + abs(deltaBPTs[minNegativeDeltaIndex]) + ) { + swapKind = SwapKind.GivenIn; + givenTokenIndex = minPositiveDeltaIndex; + resultTokenIndex = minNegativeDeltaIndex; + inputAmountRaw = abs(deltas[givenTokenIndex]); + } else { + swapKind = SwapKind.GivenOut; + givenTokenIndex = minNegativeDeltaIndex; + resultTokenIndex = minPositiveDeltaIndex; + outputAmountRaw = abs(deltas[givenTokenIndex]); + } + try { + const swapInput: SwapInput = { + chainId: input.chainId, + paths: [ + { + tokens: [ + poolTokens[ + minPositiveDeltaIndex + ].toInputToken(), + poolTokens[ + minNegativeDeltaIndex + ].toInputToken(), + ], + pools: [poolState.id], + inputAmountRaw, + outputAmountRaw, + protocolVersion: poolState.protocolVersion, + }, + ], + swapKind, + }; + const swap = new Swap(swapInput); + const result = await swap.query(input.rpcUrl); + const resultAmount = + result.swapKind === SwapKind.GivenIn + ? result.expectedAmountOut + : result.expectedAmountIn; + + deltas[givenTokenIndex] = 0n; + deltaBPTs[givenTokenIndex] = 0n; + deltas[resultTokenIndex] = + deltas[resultTokenIndex] + resultAmount.amount; + deltaBPTs[resultTokenIndex] = + await queryAddLiquidityForTokenDelta(resultTokenIndex); + } catch (err) { + throw new Error( + `Unexpected error while calculating addLiquidityUnbalanced PI at Swap step:\n${err}`, + ); + } + } + return minNegativeDeltaIndex; + } + + async function queryAddLiquidityForTokenDelta( + tokenIndex: number, + ): Promise { + const absDelta = TokenAmount.fromRawAmount( + poolTokens[tokenIndex], + abs(deltas[tokenIndex]), + ); + const { bptOut: deltaBPT } = await addLiquidity.query( + { + ...input, + amountsIn: [absDelta.toInputAmount()], + }, + poolState, + ); + const signal = deltas[tokenIndex] >= 0n ? 1n : -1n; + return deltaBPT.amount * signal; + } +}; diff --git a/src/entities/priceImpact/index.ts b/src/entities/priceImpact/index.ts index be6be47b..e305f5d9 100644 --- a/src/entities/priceImpact/index.ts +++ b/src/entities/priceImpact/index.ts @@ -1,6 +1,3 @@ -import { abs, max, min } from '../../utils'; -import { SwapKind } from '../../types'; - import { AddLiquidity } from '../addLiquidity'; import { AddLiquidityKind, @@ -17,7 +14,6 @@ import { } from '../removeLiquidity/types'; import { RemoveLiquidityNested } from '../removeLiquidityNested'; import { RemoveLiquidityNestedSingleTokenInputV2 } from '../removeLiquidityNested/removeLiquidityNestedV2/types'; -import { Swap, SwapInput } from '../swap'; import { TokenAmount } from '../tokenAmount'; import { NestedPoolState, PoolState, PoolStateWithUnderlyings } from '../types'; import { getSortedTokens } from '../utils'; @@ -26,8 +22,8 @@ import { AddLiquidityNested } from '../addLiquidityNested'; import { AddLiquidityBoostedUnbalancedInput } from '../addLiquidityBoosted/types'; import { addLiquidityUnbalancedBoosted } from './addLiquidityUnbalancedBoosted'; import { addLiquidityNested } from './addLiquidityNested'; -import { Token } from '../token'; import { priceImpactABA } from './helper'; +import { addLiquidityUnbalanced } from './addLiquidityUnbalanced'; export class PriceImpact { /** @@ -107,171 +103,7 @@ export class PriceImpact { input: AddLiquidityUnbalancedInput, poolState: PoolState, ): Promise => { - // inputs are being validated within AddLiquidity - - // simulate adding liquidity to get amounts in - const addLiquidity = new AddLiquidity(); - let amountsIn: TokenAmount[]; - let bptOut: TokenAmount; - let poolTokens: Token[]; - try { - const queryResult = await addLiquidity.query(input, poolState); - amountsIn = queryResult.amountsIn; - bptOut = queryResult.bptOut; - poolTokens = amountsIn.map((a) => a.token); - } catch (err) { - throw new Error( - `addLiquidityUnbalanced operation will fail at SC level with user defined input.\n${err}`, - ); - } - - // simulate removing liquidity to get amounts out - const removeLiquidity = new RemoveLiquidity(); - const removeLiquidityInput: RemoveLiquidityInput = { - chainId: input.chainId, - rpcUrl: input.rpcUrl, - bptIn: bptOut.toInputAmount(), - kind: RemoveLiquidityKind.Proportional, - }; - const { amountsOut } = await removeLiquidity.query( - removeLiquidityInput, - poolState, - ); - - // deltas between unbalanced and proportional amounts - const deltas = amountsOut.map((a, i) => a.amount - amountsIn[i].amount); - - // get how much BPT each delta would mint - const deltaBPTs: bigint[] = []; - for (let i = 0; i < deltas.length; i++) { - if (deltas[i] === 0n) { - deltaBPTs.push(0n); - } else { - try { - deltaBPTs.push(await queryAddLiquidityForTokenDelta(i)); - } catch (err) { - throw new Error( - `Unexpected error while calculating addLiquidityUnbalanced PI at Delta add step:\n${err}`, - ); - } - } - } - - // zero out deltas by swapping between tokens from proportionalAmounts - // to exactAmountsIn, leaving the remaining delta within a single token - let remainingDeltaIndex = 0; - if (deltaBPTs.some((deltaBPT) => deltaBPT !== 0n)) { - remainingDeltaIndex = await zeroOutDeltas(deltas, deltaBPTs); - } - - // get relevant amount for price impact calculation - const deltaAmount = TokenAmount.fromRawAmount( - amountsIn[remainingDeltaIndex].token, - abs(deltas[remainingDeltaIndex]), - ); - - // calculate price impact using ABA method - return priceImpactABA( - amountsIn[remainingDeltaIndex], - amountsIn[remainingDeltaIndex].sub(deltaAmount), - ); - - // helper functions - - async function zeroOutDeltas(deltas: bigint[], deltaBPTs: bigint[]) { - let minNegativeDeltaIndex = deltaBPTs.findIndex( - (deltaBPT) => deltaBPT === max(deltaBPTs.filter((a) => a < 0n)), - ); - const nonZeroDeltasBPTs = deltaBPTs.filter((d) => d !== 0n); - for (let i = 0; i < nonZeroDeltasBPTs.length - 1; i++) { - const minPositiveDeltaIndex = deltaBPTs.findIndex( - (deltaBPT) => - deltaBPT === min(deltaBPTs.filter((a) => a > 0n)), - ); - minNegativeDeltaIndex = deltaBPTs.findIndex( - (deltaBPT) => - deltaBPT === max(deltaBPTs.filter((a) => a < 0n)), - ); - - let swapKind: SwapKind; - let givenTokenIndex: number; - let resultTokenIndex: number; - let inputAmountRaw = 0n; - let outputAmountRaw = 0n; - if ( - deltaBPTs[minPositiveDeltaIndex] < - abs(deltaBPTs[minNegativeDeltaIndex]) - ) { - swapKind = SwapKind.GivenIn; - givenTokenIndex = minPositiveDeltaIndex; - resultTokenIndex = minNegativeDeltaIndex; - inputAmountRaw = abs(deltas[givenTokenIndex]); - } else { - swapKind = SwapKind.GivenOut; - givenTokenIndex = minNegativeDeltaIndex; - resultTokenIndex = minPositiveDeltaIndex; - outputAmountRaw = abs(deltas[givenTokenIndex]); - } - try { - const swapInput: SwapInput = { - chainId: input.chainId, - paths: [ - { - tokens: [ - poolTokens[ - minPositiveDeltaIndex - ].toInputToken(), - poolTokens[ - minNegativeDeltaIndex - ].toInputToken(), - ], - pools: [poolState.id], - inputAmountRaw, - outputAmountRaw, - protocolVersion: poolState.protocolVersion, - }, - ], - swapKind, - }; - const swap = new Swap(swapInput); - const result = await swap.query(input.rpcUrl); - const resultAmount = - result.swapKind === SwapKind.GivenIn - ? result.expectedAmountOut - : result.expectedAmountIn; - - deltas[givenTokenIndex] = 0n; - deltaBPTs[givenTokenIndex] = 0n; - deltas[resultTokenIndex] = - deltas[resultTokenIndex] + resultAmount.amount; - deltaBPTs[resultTokenIndex] = - await queryAddLiquidityForTokenDelta(resultTokenIndex); - } catch (err) { - throw new Error( - `Unexpected error while calculating addLiquidityUnbalanced PI at Swap step:\n${err}`, - ); - } - } - return minNegativeDeltaIndex; - } - - async function queryAddLiquidityForTokenDelta( - tokenIndex: number, - ): Promise { - const absDelta = TokenAmount.fromRawAmount( - poolTokens[tokenIndex], - abs(deltas[tokenIndex]), - ); - const { bptOut: deltaBPT } = await addLiquidity.query( - { - ...input, - amountsIn: [absDelta.toInputAmount()], - }, - poolState, - ); - const signal = deltas[tokenIndex] >= 0n ? 1n : -1n; - return deltaBPT.amount * signal; - } + return addLiquidityUnbalanced(input, poolState); }; static async addLiquidityUnbalancedBoosted( From 1455fded5fd046cbfc9c90a5a7d999764543e731 Mon Sep 17 00:00:00 2001 From: Bruno Eidam Guerios Date: Mon, 18 Nov 2024 14:15:49 -0300 Subject: [PATCH 8/9] Add changeset --- .changeset/two-kiwis-wash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/two-kiwis-wash.md diff --git a/.changeset/two-kiwis-wash.md b/.changeset/two-kiwis-wash.md new file mode 100644 index 00000000..69813990 --- /dev/null +++ b/.changeset/two-kiwis-wash.md @@ -0,0 +1,5 @@ +--- +"@balancer/sdk": patch +--- + +Fix circular dependency on price impact implementation From d616fe4b71123cd2759f643f03201d748e09164b Mon Sep 17 00:00:00 2001 From: Bruno Eidam Guerios Date: Mon, 18 Nov 2024 14:39:19 -0300 Subject: [PATCH 9/9] Fix missing export --- src/entities/priceImpact/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/entities/priceImpact/index.ts b/src/entities/priceImpact/index.ts index e305f5d9..79048f9b 100644 --- a/src/entities/priceImpact/index.ts +++ b/src/entities/priceImpact/index.ts @@ -25,6 +25,8 @@ import { addLiquidityNested } from './addLiquidityNested'; import { priceImpactABA } from './helper'; import { addLiquidityUnbalanced } from './addLiquidityUnbalanced'; +export * from './helper'; + export class PriceImpact { /** * Calculate price impact on add liquidity single token operations