Skip to content

Commit

Permalink
Merge pull request #493 from balancer/fix-price-impact-circular-dep
Browse files Browse the repository at this point in the history
Fix circular dependency on Price Impact implementation
  • Loading branch information
brunoguerios authored Nov 19, 2024
2 parents 0117f7b + d616fe4 commit ec2042e
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 188 deletions.
5 changes: 5 additions & 0 deletions .changeset/two-kiwis-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@balancer/sdk": patch
---

Fix circular dependency on price impact implementation
7 changes: 4 additions & 3 deletions src/entities/priceImpact/addLiquidityNested.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -111,7 +112,7 @@ async function getAddUnbalancedResult(
...pool,
protocolVersion,
};
const priceImpactAmount = await PriceImpact.addLiquidityUnbalanced(
const priceImpactAmount = await addLiquidityUnbalanced(
addLiquidityInput,
poolState,
);
Expand All @@ -135,7 +136,7 @@ async function getAddBoostedUnbalancedResult(
kind: AddLiquidityKind.Unbalanced,
};

const priceImpactAmount = await PriceImpact.addLiquidityUnbalancedBoosted(
const priceImpactAmount = await addLiquidityUnbalancedBoosted(
addLiquidityInput,
{ ...pool, protocolVersion: 3 },
);
Expand Down
184 changes: 184 additions & 0 deletions src/entities/priceImpact/addLiquidityUnbalanced.ts
Original file line number Diff line number Diff line change
@@ -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<PriceImpactAmount> => {
// 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<bigint> {
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;
}
};
2 changes: 1 addition & 1 deletion src/entities/priceImpact/addLiquidityUnbalancedBoosted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
18 changes: 18 additions & 0 deletions src/entities/priceImpact/helper.ts
Original file line number Diff line number Diff line change
@@ -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);
};
Loading

0 comments on commit ec2042e

Please sign in to comment.