Skip to content

Commit

Permalink
Merge pull request #387 from balancer/fix-proportional-amounts-cow-amm
Browse files Browse the repository at this point in the history
Fix add liquidity proportional query for cow-amm
  • Loading branch information
brunoguerios authored Aug 14, 2024
2 parents ea47225 + de32a7c commit 9cb6d51
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/sixty-squids-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@balancer/sdk": patch
---

Fix add liquidity proportional query for cow-amm
141 changes: 141 additions & 0 deletions examples/addLiquidity/addLiquidityCowAmm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* Example showing how to add liquidity to a pool.
* (Runs against a local Anvil fork)
*
* Run with:
* pnpm example ./examples/addLiquidity/addLiquidityCowAmm.ts
*/
import { Address } from 'viem';
import {
AddLiquidityInput,
AddLiquidityKind,
AddLiquidity,
BalancerApi,
calculateProportionalAmountsCowAmm,
ChainId,
getPoolStateWithBalancesCowAmm,
Slippage,
} from '../../src';
import { ANVIL_NETWORKS, startFork } from '../../test/anvil/anvil-global-setup';
import { makeForkTx } from '../lib/makeForkTx';
import { getSlot } from 'examples/lib/getSlot';

async function runAgainstFork() {
// User defined inputs
const { rpcUrl } = await startFork(
ANVIL_NETWORKS.MAINNET,
undefined,
20520774n,
);
const chainId = ChainId.MAINNET;
const userAccount = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045';
// USDC-WETH CowAmm pool
const pool = {
id: '0xf08d4dea369c456d26a3168ff0024b904f2d8b91',
address: '0xf08d4dea369c456d26a3168ff0024b904f2d8b91' as Address,
};

const referenceAmountIn = {
rawAmount: 158708n,
decimals: 6,
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' as Address, // USDC
};

const slippage = Slippage.fromPercentage('0'); // 1%

const call = await addLiquidityProportional({
rpcUrl,
chainId,
referenceAmountIn,
poolId: pool.id,
slippage,
});

// Make the tx against the local fork and print the result
await makeForkTx(
call,
{
rpcUrl,
chainId,
impersonateAccount: userAccount,
forkTokens: call.maxAmountsIn.map((a) => ({
address: a.token.address,
slot: getSlot(chainId, a.token.address),
rawBalance: a.amount,
})),
},
[...call.maxAmountsIn.map((a) => a.token.address), pool.address],
call.protocolVersion,
);
}

const addLiquidityProportional = async ({
rpcUrl,
chainId,
poolId,
referenceAmountIn,
slippage,
}) => {
// API + on-chain calls are used to fetch relevant pool data
const balancerApi = new BalancerApi('https://api-v3.balancer.fi/', chainId);
const poolState = await balancerApi.pools.fetchPoolState(poolId);
const poolStateWithBalances = await getPoolStateWithBalancesCowAmm(
poolState,
chainId,
rpcUrl,
);
console.log('Pool State with Balances:');
console.log(poolStateWithBalances);

const { tokenAmounts, bptAmount } = calculateProportionalAmountsCowAmm(
poolStateWithBalances,
referenceAmountIn,
);

console.log('Token Amounts:');
tokenAmounts.map((a) => console.log(a.address, a.rawAmount.toString()));

// Construct the AddLiquidityInput, in this case an AddLiquidityUnbalanced
const addLiquidityInput: AddLiquidityInput = {
bptOut: bptAmount,
chainId,
rpcUrl,
kind: AddLiquidityKind.Proportional,
};

// Simulate addLiquidity to get the amount of BPT out
const addLiquidity = new AddLiquidity();
const queryOutput = await addLiquidity.query(
addLiquidityInput,
poolStateWithBalances,
);

console.log('\nAdd Liquidity Query Output:');
console.log('Tokens In:');
queryOutput.amountsIn.map((a) =>
console.log(a.token.address, a.amount.toString()),
);
console.log(`BPT Out: ${queryOutput.bptOut.amount.toString()}`);

// Apply slippage to the BPT amount received from the query and construct the call
const call = addLiquidity.buildCall({
...queryOutput,
slippage,
chainId,
wethIsEth: false,
});

console.log('\nWith slippage applied:');
console.log('Max tokens in:');
call.maxAmountsIn.forEach((a) =>
console.log(a.token.address, a.amount.toString()),
);
console.log(`Min BPT Out: ${call.minBptOut.amount.toString()}`);

return {
...call,
protocolVersion: queryOutput.protocolVersion,
};
};

export default runAgainstFork;
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class Pools {
query: this.poolStateWithRawTokensQuery,
variables: {
id: id.toLowerCase(),
chain: API_CHAIN_NAMES[this.balancerApiClient.chainId],
},
});
const poolStateWithBalances: PoolStateWithBalances = {
Expand Down
8 changes: 5 additions & 3 deletions src/entities/addLiquidity/addLiquidityCowAmm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { cowAmmPoolAbi } from '@/abi/cowAmmPool';
import { Token } from '@/entities/token';
import { TokenAmount } from '@/entities/tokenAmount';
import { PoolState } from '@/entities/types';
import { calculateProportionalAmounts } from '@/entities/utils';
import {
calculateProportionalAmountsCowAmm,
getPoolStateWithBalancesCowAmm,
} from '@/entities/utils';

import { getAmountsCall } from '../helpers';
import {
Expand All @@ -14,7 +17,6 @@ import {
AddLiquidityKind,
AddLiquidityProportionalInput,
} from '../types';
import { getPoolStateWithBalancesCowAmm } from '@/entities/utils/cowAmmHelpers';

export class AddLiquidityCowAmm implements AddLiquidityBase {
async query(
Expand All @@ -27,7 +29,7 @@ export class AddLiquidityCowAmm implements AddLiquidityBase {
input.rpcUrl,
);

const { tokenAmounts, bptAmount } = calculateProportionalAmounts(
const { tokenAmounts, bptAmount } = calculateProportionalAmountsCowAmm(
poolStateWithBalances,
input.bptOut,
);
Expand Down
115 changes: 111 additions & 4 deletions src/entities/utils/cowAmmHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import {
formatUnits,
getContract,
http,
parseUnits,
} from 'viem';

import { cowAmmPoolAbi } from '@/abi/cowAmmPool';
import { Address } from '@/types';
import { PoolState, PoolStateWithBalances } from '../types';
import { CHAINS } from '@/utils';
import { getSortedTokens } from './getSortedTokens';
import { HumanAmount } from '@/data';
import { Address, InputAmount } from '@/types';
import { CHAINS, WAD } from '@/utils';

import { getSortedTokens } from './getSortedTokens';
import { PoolState, PoolStateWithBalances } from '../types';

type MulticallContract = {
address: Address;
Expand Down Expand Up @@ -134,3 +137,107 @@ export const getTotalSupplyCowAmm = async (
);
}
};

/**
* For a given pool and reference token amount, calculate all token amounts proportional to their balances within the pool.
*
* Note: when using this helper to build an AddLiquidityProportional input,
* please mind that referenceAmount should be relative to the token that the user
* has the lowest balance compared to the pool's proportions. Otherwise the transaction
* may require more balance than the user has.
* @param pool
* @param referenceAmount
* @returns Proportional amounts
*/
export function calculateProportionalAmountsCowAmm(
pool: {
address: Address;
totalShares: HumanAmount;
tokens: { address: Address; balance: HumanAmount; decimals: number }[];
},
referenceAmount: InputAmount,
): {
tokenAmounts: InputAmount[];
bptAmount: InputAmount;
} {
const tokensWithBpt = [
...pool.tokens,
{
address: pool.address,
balance: pool.totalShares,
decimals: 18,
},
];

// validate that input amount is relative to a token in the pool or its BPT
const referenceTokenIndex = tokensWithBpt.findIndex(
(t) =>
t.address.toLowerCase() === referenceAmount.address.toLowerCase(),
);
if (referenceTokenIndex === -1) {
throw new Error(
'Reference amount must be relative to a token in the pool or its BPT',
);
}

// scale up balances from HumanAmount to RawAmount
const balances = tokensWithBpt.map((t) =>
parseUnits(t.balance, t.decimals),
);

// calculate proportional amounts
const referenceTokenBalance = balances[referenceTokenIndex];
const ratio = bdiv(referenceAmount.rawAmount, referenceTokenBalance);
const proportionalAmounts = balances.map((b) => bmul(b, ratio));

const amounts = tokensWithBpt.map(({ address, decimals }, index) => ({
address,
decimals,
rawAmount: proportionalAmounts[index],
}));

const bptAmount = amounts.pop() as InputAmount;

return {
tokenAmounts: amounts,
bptAmount,
};
}

// from cow-amm solidity implementation [bmul](https://github.com/balancer/cow-amm/blob/04c915d1ef6150b5334f4b69c7af7ddd59e050e2/src/contracts/BNum.sol#L91)
function bmul(a: bigint, b: bigint): bigint {
const c0 = a * b;
if (a !== BigInt(0) && c0 / a !== b) {
throw new Error('BNum_MulOverflow');
}

// NOTE: using >> 1 instead of / 2
const c1 = c0 + (WAD >> 1n);
if (c1 < c0) {
throw new Error('BNum_MulOverflow');
}

const c2 = c1 / WAD;
return c2;
}

// from cow-amm solidity implementation [bdiv](https://github.com/balancer/cow-amm/blob/04c915d1ef6150b5334f4b69c7af7ddd59e050e2/src/contracts/BNum.sol#L107)
function bdiv(a: bigint, b: bigint): bigint {
if (b === 0n) {
throw new Error('BNum_DivZero');
}

const c0 = a * WAD;
if (a !== 0n && c0 / a !== WAD) {
throw new Error('BNum_DivInternal'); // bmul overflow
}

// NOTE: using >> 1 instead of / 2
const c1 = c0 + (b >> 1n);
if (c1 < c0) {
throw new Error('BNum_DivInternal'); // badd require
}

const c2 = c1 / b;
return c2;
}
1 change: 1 addition & 0 deletions src/entities/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './calculateProportionalAmounts';
export * from './cowAmmHelpers';
export * from './doAddLiquidityQuery';
export * from './getAmounts';
export * from './getSortedTokens';
Expand Down

0 comments on commit 9cb6d51

Please sign in to comment.