Skip to content

Commit

Permalink
Merge pull request #448 from balancer/nested-pools-release
Browse files Browse the repository at this point in the history
Nested pools release
  • Loading branch information
johngrantuk authored Oct 30, 2024
2 parents 884dec3 + 0596af4 commit c89ebc3
Show file tree
Hide file tree
Showing 23 changed files with 1,109 additions and 437 deletions.
5 changes: 5 additions & 0 deletions .changeset/light-turkeys-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@balancer/sdk": patch
---

Full nested pool support and tests.
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/**
* Example showing how to add liquidity to a pool.
* Example showing how to add liquidity to a pool from a V2 nested pool.
* User use signature to approve Balancer Relayer to use any existing V2 token approvals.
* (Runs against a local Anvil fork)
*
* Run with:
* pnpm example ./examples/addLiquidity/addLiquidityNested.ts
* pnpm example ./examples/addLiquidity/addLiquidityNested.V2.ts
*/
import {
createTestClient,
Expand All @@ -21,18 +22,20 @@ import {
CHAINS,
PriceImpact,
Relayer,
replaceWrapped,
Slippage,
AddLiquidityNested,
AddLiquidityNestedInput,
} from '../../src';
import { ANVIL_NETWORKS, startFork } from '../../test/anvil/anvil-global-setup';
import {
ANVIL_NETWORKS,
NetworkSetup,
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() {
const addLiquidityNested = async () => {
// User defined inputs
const { rpcUrl } = await startFork(ANVIL_NETWORKS.MAINNET);
const chainId = ChainId.MAINNET;
// WETH-3POOL
const pool = {
Expand All @@ -48,61 +51,17 @@ async function runAgainstFork() {
},
];
const slippage = Slippage.fromPercentage('1'); // 1%
// This example requires the account to sign relayer approval
const client = createTestClient({
mode: 'anvil',
chain: CHAINS[chainId],
transport: http(rpcUrl),
})
.extend(publicActions)
.extend(walletActions);
const userAccount = (await client.getAddresses())[0];
const relayerApprovalSignature = await Relayer.signRelayerApproval(
BALANCER_RELAYER[chainId],
userAccount,
client,
);

const call = await addLiquidityNested({
rpcUrl,
chainId,
userAccount,
relayerApprovalSignature,
amountsIn,
poolId: pool.id,
slippage,
});
const wethIsEth = false;

await makeForkTx(
call,
{
rpcUrl,
chainId,
impersonateAccount: userAccount,
forkTokens: amountsIn.map((a) => ({
address: a.address,
slot: getSlot(chainId, a.address),
rawBalance: a.rawAmount,
})),
},
[...amountsIn.map((a) => a.address), pool.address],
2, // TODO - Currently only V2 support, update when SC ready
);
}
// This sets up a local fork with a test client/account
const { rpcUrl, client } = await setup(chainId, ANVIL_NETWORKS.MAINNET);
const userAccount = (await client.getAddresses())[0];

const addLiquidityNested = async ({
rpcUrl,
userAccount,
relayerApprovalSignature,
chainId,
poolId,
amountsIn,
slippage,
}) => {
// API is used to fetch relevant pool data
const balancerApi = new BalancerApi(API_ENDPOINT, chainId);
const nestedPoolState =
await balancerApi.nestedPools.fetchNestedPoolState(poolId);
const nestedPoolState = await balancerApi.nestedPools.fetchNestedPoolState(
pool.id,
);

// setup add liquidity helper
const addLiquidityNested = new AddLiquidityNested();
Expand Down Expand Up @@ -131,24 +90,82 @@ const addLiquidityNested = async ({
amountsIn: queryOutput.amountsIn.map((a) => a.amount),
});
console.log(`BPT Out: ${queryOutput.bptOut.amount.toString()}`);
const wethIsEth = false;

const call = addLiquidityNested.buildCall({
...queryOutput,
slippage,
accountAddress: userAccount,
relayerApprovalSignature,
wethIsEth,
});
// Use signature to approve Balancer Relayer to use any existing V2 token approvals
const relayerApprovalSignature = await Relayer.signRelayerApproval(
BALANCER_RELAYER[chainId],
userAccount,
client,
);

let tokensIn = queryOutput.amountsIn.map((a) => a.token);
if (wethIsEth) {
tokensIn = replaceWrapped(tokensIn, chainId);
}
const call = addLiquidityNested.buildCall(
addLiquidityNested.buildAddLiquidityInput(queryOutput, {
slippage,
accountAddress: userAccount,
relayerApprovalSignature,
wethIsEth,
}),
);

console.log('\nWith slippage applied:');
console.log(`Min BPT Out: ${call.minBptOut.toString()}`);
return call;

return {
rpcUrl,
chainId,
txInfo: {
to: call.to,
callData: call.callData,
},
account: userAccount,
bptOut: queryOutput.bptOut,
amountsIn: queryOutput.amountsIn,
protocolVersion: queryOutput.protocolVersion,
};
};

async function runAgainstFork() {
const {
rpcUrl,
chainId,
txInfo,
account,
bptOut,
amountsIn,
protocolVersion,
} = await addLiquidityNested();

await makeForkTx(
txInfo,
{
rpcUrl,
chainId,
impersonateAccount: account,
forkTokens: amountsIn.map((a) => ({
address: a.token.address,
slot: getSlot(chainId, a.token.address),
rawBalance: a.amount,
})),
},
[...amountsIn.map((a) => a.token.address), bptOut.token.address],
protocolVersion,
);
}

async function setup(chainId: ChainId, network: NetworkSetup) {
const { rpcUrl } = await startFork(network);

const client = createTestClient({
mode: 'anvil',
chain: CHAINS[chainId],
transport: http(rpcUrl),
})
.extend(publicActions)
.extend(walletActions);
return {
client,
rpcUrl,
};
}

export default runAgainstFork;
173 changes: 173 additions & 0 deletions examples/addLiquidity/addLiquidityNested.V3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/**
* Example showing how to add liquidity to a pool from a V3 nested pool.
* Uses direct ERC20 approvals. Signature method can be seen in tests.
* (Runs against a local Anvil fork)
*
* Run with:
* pnpm example ./examples/addLiquidity/addLiquidityNested.V3.ts
*/
import {
createTestClient,
http,
parseEther,
parseUnits,
publicActions,
walletActions,
} from 'viem';
import {
Address,
BalancerApi,
ChainId,
CHAINS,
Slippage,
AddLiquidityNested,
AddLiquidityNestedInput,
TEST_API_ENDPOINT,
PERMIT2,
BALANCER_COMPOSITE_LIQUIDITY_ROUTER,
} from '../../src';
import {
ANVIL_NETWORKS,
NetworkSetup,
startFork,
} from '../../test/anvil/anvil-global-setup';
import { getSlot } from 'examples/lib/getSlot';
import {
approveSpenderOnPermit2,
approveSpenderOnToken,
sendTransactionGetBalances,
setTokenBalances,
} from 'test/lib/utils';

const addLiquidityNested = async () => {
// User defined inputs
const chainId = ChainId.SEPOLIA;
// WETH-BOOSTED-USD Pool
const pool = {
id: '0x0270daf4ee12ccb1abc8aa365054eecb1b7f4f6b',
address: '0x0270daf4ee12ccb1abc8aa365054eecb1b7f4f6b' as Address,
};

const amountsIn = [
// USDT
{
rawAmount: parseUnits('19', 6),
decimals: 6,
address: '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8' as Address,
},
// WETH
{
rawAmount: parseEther('0.001'),
decimals: 18,
address: '0x7b79995e5f793a07bc00c21412e50ecae098e7f9' as Address,
},
];
const slippage = Slippage.fromPercentage('1'); // 1%

// This sets up a local fork with a test client/account
const { rpcUrl, client } = await setup(chainId, ANVIL_NETWORKS.SEPOLIA);
const userAccount = (await client.getAddresses())[0];

// API is used to fetch relevant pool data
const balancerApi = new BalancerApi(TEST_API_ENDPOINT, chainId);
const nestedPoolState = await balancerApi.nestedPools.fetchNestedPoolState(
pool.id,
);

// setup add liquidity helper
const addLiquidityNested = new AddLiquidityNested();

const addLiquidityInput: AddLiquidityNestedInput = {
amountsIn,
chainId,
rpcUrl,
};

// TODO - Add back once V3 support
// Calculate price impact to ensure it's acceptable
// const priceImpact = await PriceImpact.addLiquidityNested(
// addLiquidityInput,
// nestedPoolState,
// );
// console.log(`\nPrice Impact: ${priceImpact.percentage.toFixed(2)}%`);

const queryOutput = await addLiquidityNested.query(
addLiquidityInput,
nestedPoolState,
);

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

const call = addLiquidityNested.buildCall(
addLiquidityNested.buildAddLiquidityInput(queryOutput, {
slippage,
}),
);

console.log('\nWith slippage applied:');
console.log(`Min BPT Out: ${call.minBptOut.toString()}`);

await setTokenBalances(
client,
userAccount,
queryOutput.amountsIn.map((t) => t.token.address),
queryOutput.amountsIn.map((a) => getSlot(chainId, a.token.address)),
queryOutput.amountsIn.map((a) => a.amount),
);

for (const amount of queryOutput.amountsIn) {
// Approve Permit2 to spend account tokens
await approveSpenderOnToken(
client,
userAccount,
amount.token.address,
PERMIT2[chainId],
);
// Approve Router to spend account tokens using Permit2
await approveSpenderOnPermit2(
client,
userAccount,
amount.token.address,
BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId],
);
}

// send add liquidity transaction and check balance changes
const { transactionReceipt, balanceDeltas } =
await sendTransactionGetBalances(
[
...queryOutput.amountsIn.map((t) => t.token.address),
queryOutput.bptOut.token.address,
],
client,
userAccount,
call.to,
call.callData,
);

console.log(transactionReceipt.status);
console.log(balanceDeltas);
};

async function setup(chainId: ChainId, network: NetworkSetup) {
const { rpcUrl } = await startFork(network);

const client = createTestClient({
mode: 'anvil',
chain: CHAINS[chainId],
transport: http(rpcUrl),
})
.extend(publicActions)
.extend(walletActions);
return {
client,
rpcUrl,
};
}

export default addLiquidityNested;
Loading

0 comments on commit c89ebc3

Please sign in to comment.