Skip to content

Commit c89ebc3

Browse files
authored
Merge pull request #448 from balancer/nested-pools-release
Nested pools release
2 parents 884dec3 + 0596af4 commit c89ebc3

File tree

23 files changed

+1109
-437
lines changed

23 files changed

+1109
-437
lines changed

.changeset/light-turkeys-kick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@balancer/sdk": patch
3+
---
4+
5+
Full nested pool support and tests.
Lines changed: 89 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
/**
2-
* Example showing how to add liquidity to a pool.
2+
* Example showing how to add liquidity to a pool from a V2 nested pool.
3+
* User use signature to approve Balancer Relayer to use any existing V2 token approvals.
34
* (Runs against a local Anvil fork)
45
*
56
* Run with:
6-
* pnpm example ./examples/addLiquidity/addLiquidityNested.ts
7+
* pnpm example ./examples/addLiquidity/addLiquidityNested.V2.ts
78
*/
89
import {
910
createTestClient,
@@ -21,18 +22,20 @@ import {
2122
CHAINS,
2223
PriceImpact,
2324
Relayer,
24-
replaceWrapped,
2525
Slippage,
26+
AddLiquidityNested,
27+
AddLiquidityNestedInput,
2628
} from '../../src';
27-
import { ANVIL_NETWORKS, startFork } from '../../test/anvil/anvil-global-setup';
29+
import {
30+
ANVIL_NETWORKS,
31+
NetworkSetup,
32+
startFork,
33+
} from '../../test/anvil/anvil-global-setup';
2834
import { makeForkTx } from 'examples/lib/makeForkTx';
2935
import { getSlot } from 'examples/lib/getSlot';
30-
import { AddLiquidityNestedInput } from '@/entities/addLiquidityNested/addLiquidityNestedV2/types';
31-
import { AddLiquidityNested } from '@/entities/addLiquidityNested';
3236

33-
async function runAgainstFork() {
37+
const addLiquidityNested = async () => {
3438
// User defined inputs
35-
const { rpcUrl } = await startFork(ANVIL_NETWORKS.MAINNET);
3639
const chainId = ChainId.MAINNET;
3740
// WETH-3POOL
3841
const pool = {
@@ -48,61 +51,17 @@ async function runAgainstFork() {
4851
},
4952
];
5053
const slippage = Slippage.fromPercentage('1'); // 1%
51-
// This example requires the account to sign relayer approval
52-
const client = createTestClient({
53-
mode: 'anvil',
54-
chain: CHAINS[chainId],
55-
transport: http(rpcUrl),
56-
})
57-
.extend(publicActions)
58-
.extend(walletActions);
59-
const userAccount = (await client.getAddresses())[0];
60-
const relayerApprovalSignature = await Relayer.signRelayerApproval(
61-
BALANCER_RELAYER[chainId],
62-
userAccount,
63-
client,
64-
);
65-
66-
const call = await addLiquidityNested({
67-
rpcUrl,
68-
chainId,
69-
userAccount,
70-
relayerApprovalSignature,
71-
amountsIn,
72-
poolId: pool.id,
73-
slippage,
74-
});
54+
const wethIsEth = false;
7555

76-
await makeForkTx(
77-
call,
78-
{
79-
rpcUrl,
80-
chainId,
81-
impersonateAccount: userAccount,
82-
forkTokens: amountsIn.map((a) => ({
83-
address: a.address,
84-
slot: getSlot(chainId, a.address),
85-
rawBalance: a.rawAmount,
86-
})),
87-
},
88-
[...amountsIn.map((a) => a.address), pool.address],
89-
2, // TODO - Currently only V2 support, update when SC ready
90-
);
91-
}
56+
// This sets up a local fork with a test client/account
57+
const { rpcUrl, client } = await setup(chainId, ANVIL_NETWORKS.MAINNET);
58+
const userAccount = (await client.getAddresses())[0];
9259

93-
const addLiquidityNested = async ({
94-
rpcUrl,
95-
userAccount,
96-
relayerApprovalSignature,
97-
chainId,
98-
poolId,
99-
amountsIn,
100-
slippage,
101-
}) => {
10260
// API is used to fetch relevant pool data
10361
const balancerApi = new BalancerApi(API_ENDPOINT, chainId);
104-
const nestedPoolState =
105-
await balancerApi.nestedPools.fetchNestedPoolState(poolId);
62+
const nestedPoolState = await balancerApi.nestedPools.fetchNestedPoolState(
63+
pool.id,
64+
);
10665

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

136-
const call = addLiquidityNested.buildCall({
137-
...queryOutput,
138-
slippage,
139-
accountAddress: userAccount,
140-
relayerApprovalSignature,
141-
wethIsEth,
142-
});
94+
// Use signature to approve Balancer Relayer to use any existing V2 token approvals
95+
const relayerApprovalSignature = await Relayer.signRelayerApproval(
96+
BALANCER_RELAYER[chainId],
97+
userAccount,
98+
client,
99+
);
143100

144-
let tokensIn = queryOutput.amountsIn.map((a) => a.token);
145-
if (wethIsEth) {
146-
tokensIn = replaceWrapped(tokensIn, chainId);
147-
}
101+
const call = addLiquidityNested.buildCall(
102+
addLiquidityNested.buildAddLiquidityInput(queryOutput, {
103+
slippage,
104+
accountAddress: userAccount,
105+
relayerApprovalSignature,
106+
wethIsEth,
107+
}),
108+
);
148109

149110
console.log('\nWith slippage applied:');
150111
console.log(`Min BPT Out: ${call.minBptOut.toString()}`);
151-
return call;
112+
113+
return {
114+
rpcUrl,
115+
chainId,
116+
txInfo: {
117+
to: call.to,
118+
callData: call.callData,
119+
},
120+
account: userAccount,
121+
bptOut: queryOutput.bptOut,
122+
amountsIn: queryOutput.amountsIn,
123+
protocolVersion: queryOutput.protocolVersion,
124+
};
152125
};
153126

127+
async function runAgainstFork() {
128+
const {
129+
rpcUrl,
130+
chainId,
131+
txInfo,
132+
account,
133+
bptOut,
134+
amountsIn,
135+
protocolVersion,
136+
} = await addLiquidityNested();
137+
138+
await makeForkTx(
139+
txInfo,
140+
{
141+
rpcUrl,
142+
chainId,
143+
impersonateAccount: account,
144+
forkTokens: amountsIn.map((a) => ({
145+
address: a.token.address,
146+
slot: getSlot(chainId, a.token.address),
147+
rawBalance: a.amount,
148+
})),
149+
},
150+
[...amountsIn.map((a) => a.token.address), bptOut.token.address],
151+
protocolVersion,
152+
);
153+
}
154+
155+
async function setup(chainId: ChainId, network: NetworkSetup) {
156+
const { rpcUrl } = await startFork(network);
157+
158+
const client = createTestClient({
159+
mode: 'anvil',
160+
chain: CHAINS[chainId],
161+
transport: http(rpcUrl),
162+
})
163+
.extend(publicActions)
164+
.extend(walletActions);
165+
return {
166+
client,
167+
rpcUrl,
168+
};
169+
}
170+
154171
export default runAgainstFork;
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/**
2+
* Example showing how to add liquidity to a pool from a V3 nested pool.
3+
* Uses direct ERC20 approvals. Signature method can be seen in tests.
4+
* (Runs against a local Anvil fork)
5+
*
6+
* Run with:
7+
* pnpm example ./examples/addLiquidity/addLiquidityNested.V3.ts
8+
*/
9+
import {
10+
createTestClient,
11+
http,
12+
parseEther,
13+
parseUnits,
14+
publicActions,
15+
walletActions,
16+
} from 'viem';
17+
import {
18+
Address,
19+
BalancerApi,
20+
ChainId,
21+
CHAINS,
22+
Slippage,
23+
AddLiquidityNested,
24+
AddLiquidityNestedInput,
25+
TEST_API_ENDPOINT,
26+
PERMIT2,
27+
BALANCER_COMPOSITE_LIQUIDITY_ROUTER,
28+
} from '../../src';
29+
import {
30+
ANVIL_NETWORKS,
31+
NetworkSetup,
32+
startFork,
33+
} from '../../test/anvil/anvil-global-setup';
34+
import { getSlot } from 'examples/lib/getSlot';
35+
import {
36+
approveSpenderOnPermit2,
37+
approveSpenderOnToken,
38+
sendTransactionGetBalances,
39+
setTokenBalances,
40+
} from 'test/lib/utils';
41+
42+
const addLiquidityNested = async () => {
43+
// User defined inputs
44+
const chainId = ChainId.SEPOLIA;
45+
// WETH-BOOSTED-USD Pool
46+
const pool = {
47+
id: '0x0270daf4ee12ccb1abc8aa365054eecb1b7f4f6b',
48+
address: '0x0270daf4ee12ccb1abc8aa365054eecb1b7f4f6b' as Address,
49+
};
50+
51+
const amountsIn = [
52+
// USDT
53+
{
54+
rawAmount: parseUnits('19', 6),
55+
decimals: 6,
56+
address: '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8' as Address,
57+
},
58+
// WETH
59+
{
60+
rawAmount: parseEther('0.001'),
61+
decimals: 18,
62+
address: '0x7b79995e5f793a07bc00c21412e50ecae098e7f9' as Address,
63+
},
64+
];
65+
const slippage = Slippage.fromPercentage('1'); // 1%
66+
67+
// This sets up a local fork with a test client/account
68+
const { rpcUrl, client } = await setup(chainId, ANVIL_NETWORKS.SEPOLIA);
69+
const userAccount = (await client.getAddresses())[0];
70+
71+
// API is used to fetch relevant pool data
72+
const balancerApi = new BalancerApi(TEST_API_ENDPOINT, chainId);
73+
const nestedPoolState = await balancerApi.nestedPools.fetchNestedPoolState(
74+
pool.id,
75+
);
76+
77+
// setup add liquidity helper
78+
const addLiquidityNested = new AddLiquidityNested();
79+
80+
const addLiquidityInput: AddLiquidityNestedInput = {
81+
amountsIn,
82+
chainId,
83+
rpcUrl,
84+
};
85+
86+
// TODO - Add back once V3 support
87+
// Calculate price impact to ensure it's acceptable
88+
// const priceImpact = await PriceImpact.addLiquidityNested(
89+
// addLiquidityInput,
90+
// nestedPoolState,
91+
// );
92+
// console.log(`\nPrice Impact: ${priceImpact.percentage.toFixed(2)}%`);
93+
94+
const queryOutput = await addLiquidityNested.query(
95+
addLiquidityInput,
96+
nestedPoolState,
97+
);
98+
99+
console.log('\nAdd Liquidity Query Output:');
100+
console.table({
101+
tokensIn: queryOutput.amountsIn.map((a) => a.token.address),
102+
amountsIn: queryOutput.amountsIn.map((a) => a.amount),
103+
});
104+
console.log(`BPT Out: ${queryOutput.bptOut.amount.toString()}`);
105+
106+
const call = addLiquidityNested.buildCall(
107+
addLiquidityNested.buildAddLiquidityInput(queryOutput, {
108+
slippage,
109+
}),
110+
);
111+
112+
console.log('\nWith slippage applied:');
113+
console.log(`Min BPT Out: ${call.minBptOut.toString()}`);
114+
115+
await setTokenBalances(
116+
client,
117+
userAccount,
118+
queryOutput.amountsIn.map((t) => t.token.address),
119+
queryOutput.amountsIn.map((a) => getSlot(chainId, a.token.address)),
120+
queryOutput.amountsIn.map((a) => a.amount),
121+
);
122+
123+
for (const amount of queryOutput.amountsIn) {
124+
// Approve Permit2 to spend account tokens
125+
await approveSpenderOnToken(
126+
client,
127+
userAccount,
128+
amount.token.address,
129+
PERMIT2[chainId],
130+
);
131+
// Approve Router to spend account tokens using Permit2
132+
await approveSpenderOnPermit2(
133+
client,
134+
userAccount,
135+
amount.token.address,
136+
BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId],
137+
);
138+
}
139+
140+
// send add liquidity transaction and check balance changes
141+
const { transactionReceipt, balanceDeltas } =
142+
await sendTransactionGetBalances(
143+
[
144+
...queryOutput.amountsIn.map((t) => t.token.address),
145+
queryOutput.bptOut.token.address,
146+
],
147+
client,
148+
userAccount,
149+
call.to,
150+
call.callData,
151+
);
152+
153+
console.log(transactionReceipt.status);
154+
console.log(balanceDeltas);
155+
};
156+
157+
async function setup(chainId: ChainId, network: NetworkSetup) {
158+
const { rpcUrl } = await startFork(network);
159+
160+
const client = createTestClient({
161+
mode: 'anvil',
162+
chain: CHAINS[chainId],
163+
transport: http(rpcUrl),
164+
})
165+
.extend(publicActions)
166+
.extend(walletActions);
167+
return {
168+
client,
169+
rpcUrl,
170+
};
171+
}
172+
173+
export default addLiquidityNested;

0 commit comments

Comments
 (0)