Skip to content

Commit abf565b

Browse files
authored
Merge pull request #166 from balancer/price-impact-base
Add Price Impact
2 parents 9c50e5d + 7d57489 commit abf565b

16 files changed

+1079
-153
lines changed

.changeset/silver-bags-end.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@balancer/sdk": minor
3+
---
4+
5+
Add price impact calculations for add/remove liquidity and swaps

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@
3030
"dependencies": {
3131
"async-retry": "^1.3.3",
3232
"decimal.js-light": "^2.5.1",
33+
"lodash": "^4.17.21",
3334
"pino": "^8.11.0",
3435
"viem": "^1.9.3"
3536
},
3637
"devDependencies": {
3738
"@changesets/cli": "^2.26.1",
3839
"@types/async-retry": "^1.4.4",
40+
"@types/lodash": "^4.14.202",
3941
"@types/node": "^18.11.18",
4042
"@viem/anvil": "^0.0.6",
4143
"dotenv": "^16.0.3",

pnpm-lock.yaml

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/data/onChainPoolDataViaReadContract.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type Result =
1919
type Results = Result[];
2020

2121
const abi = parseAbi([
22-
'function getPoolTokens(bytes32 poolId) view returns (address[] tokens, uint256 lastChangeBlock)',
22+
'function getPoolTokens(bytes32 poolId) view returns (address[] tokens, uint256[] balances, uint256 lastChangeBlock)',
2323
'function getSwapFeePercentage() view returns (uint256)',
2424
'function percentFee() view returns (uint256)',
2525
'function protocolPercentFee() view returns (uint256)',

src/entities/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
export * from './encoders';
21
export * from './addLiquidity';
3-
export * from './removeLiquidity';
2+
export * from './encoders';
43
export * from './path';
4+
export * from './pools/';
5+
export * from './priceImpact/';
6+
export * from './removeLiquidity';
57
export * from './swap';
68
export * from './slippage';
79
export * from './token';
810
export * from './tokenAmount';
9-
export * from './pools/';
10-
export * from './utils';
1111
export * from './types';
12+
export * from './utils';

src/entities/priceImpact/index.ts

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import { formatUnits } from 'viem';
2+
import { MathSol, abs, max, min } from '../../utils';
3+
import {
4+
AddLiquidity,
5+
AddLiquidityKind,
6+
AddLiquiditySingleTokenInput,
7+
AddLiquidityUnbalancedInput,
8+
} from '../addLiquidity';
9+
import { PriceImpactAmount } from '../priceImpactAmount';
10+
import {
11+
RemoveLiquidity,
12+
RemoveLiquidityInput,
13+
RemoveLiquidityKind,
14+
RemoveLiquiditySingleTokenInput,
15+
RemoveLiquidityUnbalancedInput,
16+
} from '../removeLiquidity';
17+
import { TokenAmount } from '../tokenAmount';
18+
import { PoolStateInput } from '../types';
19+
import { getSortedTokens } from '../utils';
20+
import { SingleSwap, SwapKind } from '../../types';
21+
import { SingleSwapInput, doSingleSwapQuery } from '../utils/doSingleSwapQuery';
22+
23+
export class PriceImpact {
24+
static addLiquiditySingleToken = async (
25+
input: AddLiquiditySingleTokenInput,
26+
poolState: PoolStateInput,
27+
): Promise<PriceImpactAmount> => {
28+
// inputs are being validated within AddLiquidity
29+
30+
// simulate adding liquidity to get amounts in
31+
const addLiquidity = new AddLiquidity();
32+
const { amountsIn } = await addLiquidity.query(input, poolState);
33+
34+
// simulate removing liquidity to get amounts out
35+
const removeLiquidity = new RemoveLiquidity();
36+
const removeLiquidityInput: RemoveLiquidityInput = {
37+
chainId: input.chainId,
38+
rpcUrl: input.rpcUrl,
39+
bptIn: input.bptOut,
40+
tokenOut: input.tokenIn,
41+
kind: RemoveLiquidityKind.SingleToken,
42+
};
43+
const { amountsOut } = await removeLiquidity.query(
44+
removeLiquidityInput,
45+
poolState,
46+
);
47+
48+
// get relevant amounts for price impact calculation
49+
const sortedTokens = getSortedTokens(poolState.tokens, input.chainId);
50+
const tokenIndex = sortedTokens.findIndex((t) =>
51+
t.isSameAddress(input.tokenIn),
52+
);
53+
const amountInitial = parseFloat(amountsIn[tokenIndex].toSignificant());
54+
const amountFinal = parseFloat(amountsOut[tokenIndex].toSignificant());
55+
56+
// calculate price impact using ABA method
57+
const priceImpact = (amountInitial - amountFinal) / amountInitial / 2;
58+
return PriceImpactAmount.fromDecimal(`${priceImpact}`);
59+
};
60+
61+
static addLiquidityUnbalanced = async (
62+
input: AddLiquidityUnbalancedInput,
63+
poolState: PoolStateInput,
64+
): Promise<PriceImpactAmount> => {
65+
// inputs are being validated within AddLiquidity
66+
67+
// simulate adding liquidity to get amounts in
68+
const addLiquidity = new AddLiquidity();
69+
const { amountsIn, bptOut } = await addLiquidity.query(
70+
input,
71+
poolState,
72+
);
73+
const poolTokens = amountsIn.map((a) => a.token);
74+
75+
// simulate removing liquidity to get amounts out
76+
const removeLiquidity = new RemoveLiquidity();
77+
const removeLiquidityInput: RemoveLiquidityInput = {
78+
chainId: input.chainId,
79+
rpcUrl: input.rpcUrl,
80+
bptIn: bptOut.toInputAmount(),
81+
kind: RemoveLiquidityKind.Proportional,
82+
};
83+
const { amountsOut } = await removeLiquidity.query(
84+
removeLiquidityInput,
85+
poolState,
86+
);
87+
88+
// deltas between unbalanced and proportional amounts
89+
const deltas = amountsOut.map((a, i) => a.amount - amountsIn[i].amount);
90+
91+
// get how much BPT each delta would mint
92+
const deltaBPTs: bigint[] = [];
93+
for (let i = 0; i < deltas.length; i++) {
94+
if (deltas[i] === 0n) {
95+
deltaBPTs.push(0n);
96+
} else {
97+
deltaBPTs.push(await queryAddLiquidityForTokenDelta(i));
98+
}
99+
}
100+
101+
// zero out deltas by swapping between tokens from proportionalAmounts
102+
// to exactAmountsIn, leaving the remaining delta within a single token
103+
const remainingDeltaIndex = await zeroOutDeltas(deltas, deltaBPTs);
104+
105+
// get relevant amounts for price impact calculation
106+
const amountInitial = parseFloat(
107+
formatUnits(
108+
amountsIn[remainingDeltaIndex].amount,
109+
amountsIn[remainingDeltaIndex].token.decimals,
110+
),
111+
);
112+
const amountDelta = parseFloat(
113+
formatUnits(
114+
abs(deltas[remainingDeltaIndex]),
115+
amountsIn[remainingDeltaIndex].token.decimals,
116+
),
117+
);
118+
119+
// calculate price impact using ABA method
120+
const priceImpact = amountDelta / amountInitial / 2;
121+
return PriceImpactAmount.fromDecimal(`${priceImpact}`);
122+
123+
// helper functions
124+
125+
async function zeroOutDeltas(deltas: bigint[], deltaBPTs: bigint[]) {
126+
let minNegativeDeltaIndex = 0;
127+
const nonZeroDeltas = deltas.filter((d) => d !== 0n);
128+
for (let i = 0; i < nonZeroDeltas.length - 1; i++) {
129+
const minPositiveDeltaIndex = deltaBPTs.findIndex(
130+
(deltaBPT) =>
131+
deltaBPT === min(deltaBPTs.filter((a) => a > 0n)),
132+
);
133+
minNegativeDeltaIndex = deltaBPTs.findIndex(
134+
(deltaBPT) =>
135+
deltaBPT === max(deltaBPTs.filter((a) => a < 0n)),
136+
);
137+
138+
let kind: SwapKind;
139+
let givenTokenIndex: number;
140+
let resultTokenIndex: number;
141+
if (
142+
deltaBPTs[minPositiveDeltaIndex] <
143+
abs(deltaBPTs[minNegativeDeltaIndex])
144+
) {
145+
kind = SwapKind.GivenIn;
146+
givenTokenIndex = minPositiveDeltaIndex;
147+
resultTokenIndex = minNegativeDeltaIndex;
148+
} else {
149+
kind = SwapKind.GivenOut;
150+
givenTokenIndex = minNegativeDeltaIndex;
151+
resultTokenIndex = minPositiveDeltaIndex;
152+
}
153+
154+
const singleSwap: SingleSwap = {
155+
poolId: poolState.id,
156+
kind,
157+
assetIn: poolTokens[minPositiveDeltaIndex].address,
158+
assetOut: poolTokens[minNegativeDeltaIndex].address,
159+
amount: abs(deltas[givenTokenIndex]),
160+
userData: '0x',
161+
};
162+
163+
const resultAmount = await doSingleSwapQuery({
164+
...singleSwap,
165+
rpcUrl: input.rpcUrl,
166+
chainId: input.chainId,
167+
});
168+
169+
deltas[givenTokenIndex] = 0n;
170+
deltaBPTs[givenTokenIndex] = 0n;
171+
deltas[resultTokenIndex] =
172+
deltas[resultTokenIndex] + resultAmount;
173+
deltaBPTs[resultTokenIndex] =
174+
await queryAddLiquidityForTokenDelta(resultTokenIndex);
175+
}
176+
return minNegativeDeltaIndex;
177+
}
178+
179+
async function queryAddLiquidityForTokenDelta(
180+
tokenIndex: number,
181+
): Promise<bigint> {
182+
const absDelta = TokenAmount.fromRawAmount(
183+
poolTokens[tokenIndex],
184+
abs(deltas[tokenIndex]),
185+
);
186+
const { bptOut: deltaBPT } = await addLiquidity.query(
187+
{
188+
...input,
189+
amountsIn: [absDelta.toInputAmount()],
190+
},
191+
poolState,
192+
);
193+
const signal = deltas[tokenIndex] >= 0n ? 1n : -1n;
194+
return deltaBPT.amount * signal;
195+
}
196+
};
197+
198+
static removeLiquidity = async (
199+
input: RemoveLiquiditySingleTokenInput | RemoveLiquidityUnbalancedInput,
200+
poolState: PoolStateInput,
201+
): Promise<PriceImpactAmount> => {
202+
// inputs are being validated within RemoveLiquidity
203+
204+
// simulate removing liquidity to get amounts out
205+
const removeLiquidity = new RemoveLiquidity();
206+
const { bptIn, amountsOut } = await removeLiquidity.query(
207+
input,
208+
poolState,
209+
);
210+
211+
// simulate adding liquidity to get amounts in
212+
const addLiquidity = new AddLiquidity();
213+
const addLiquidityInput: AddLiquidityUnbalancedInput = {
214+
chainId: input.chainId,
215+
rpcUrl: input.rpcUrl,
216+
amountsIn: amountsOut.map((a) => a.toInputAmount()),
217+
kind: AddLiquidityKind.Unbalanced,
218+
};
219+
const { bptOut } = await addLiquidity.query(
220+
addLiquidityInput,
221+
poolState,
222+
);
223+
224+
// get relevant amounts for price impact calculation
225+
const amountInitial = parseFloat(bptIn.toSignificant());
226+
const amountFinal = parseFloat(bptOut.toSignificant());
227+
228+
// calculate price impact using ABA method
229+
const priceImpact = (amountInitial - amountFinal) / amountInitial / 2;
230+
return PriceImpactAmount.fromDecimal(`${priceImpact}`);
231+
};
232+
233+
static singleSwap = async ({
234+
poolId,
235+
kind,
236+
assetIn,
237+
assetOut,
238+
amount,
239+
userData,
240+
rpcUrl,
241+
chainId,
242+
}: SingleSwapInput): Promise<PriceImpactAmount> => {
243+
// simulate swap in original direction
244+
const amountResult = await doSingleSwapQuery({
245+
poolId,
246+
kind,
247+
assetIn,
248+
assetOut,
249+
amount,
250+
userData,
251+
rpcUrl,
252+
chainId,
253+
});
254+
255+
// simulate swap in the reverse direction
256+
const amountFinal = await doSingleSwapQuery({
257+
poolId: poolId,
258+
kind: kind,
259+
assetIn: assetOut,
260+
assetOut: assetIn,
261+
amount: amountResult,
262+
userData,
263+
rpcUrl,
264+
chainId,
265+
});
266+
267+
// calculate price impact using ABA method
268+
const priceImpact = MathSol.divDownFixed(
269+
abs(amount - amountFinal),
270+
amount * 2n,
271+
);
272+
273+
return PriceImpactAmount.fromRawAmount(priceImpact);
274+
};
275+
}

0 commit comments

Comments
 (0)