Skip to content

Commit 8a88d09

Browse files
authored
Merge pull request #747 from balancer/unbalanced-add-Router
Add Liquidity Unbalanced Via Swap
2 parents 4be3976 + dcba69f commit 8a88d09

File tree

19 files changed

+1459
-27
lines changed

19 files changed

+1459
-27
lines changed

.changeset/khaki-paths-kick.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 support for Add Liquidity Unbalanced Via Swap

src/abi/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
gyro2CLPAbi_V3,
2626
gyroECLPAbi_V3,
2727
lBPMigrationRouterAbi_V3,
28+
unbalancedAddViaSwapRouterAbi_V3,
2829
} from './v3';
2930

3031
export * from './batchRelayerLibrary';
@@ -85,6 +86,12 @@ export const balancerMigrationRouterAbiExtended = [
8586
...commonABIsV3,
8687
...poolABIsV3,
8788
];
89+
90+
export const balancerUnbalancedAddViaSwapRouterAbiExtended = [
91+
...unbalancedAddViaSwapRouterAbi_V3,
92+
...commonABIsV3,
93+
...poolABIsV3,
94+
];
8895
// V3 Pool Factories ABIs Extended
8996

9097
export const weightedPoolFactoryAbiExtended_V3 = [

src/data/providers/balancer-api/generated/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export type GqlChain =
9292
| 'HYPEREVM'
9393
| 'MAINNET'
9494
| 'MODE'
95+
| 'MONAD'
9596
| 'OPTIMISM'
9697
| 'PLASMA'
9798
| 'POLYGON'
@@ -1153,6 +1154,7 @@ export type GqlPoolLiquidityBootstrappingV3 = GqlPoolBase & {
11531154
/** @deprecated Removed without replacement */
11541155
investConfig: GqlPoolInvestConfig;
11551156
isProjectTokenSwapInBlocked: Scalars['Boolean']['output'];
1157+
isSeedless: Scalars['Boolean']['output'];
11561158
lbpName?: Maybe<Scalars['String']['output']>;
11571159
lbpOwner: Scalars['String']['output'];
11581160
liquidityManagement?: Maybe<LiquidityManagement>;
@@ -2761,6 +2763,7 @@ export type LiquidityBootstrappingPoolV3Params = {
27612763
endTime: Scalars['Int']['output'];
27622764
farcaster?: Maybe<Scalars['String']['output']>;
27632765
isProjectTokenSwapInBlocked: Scalars['Boolean']['output'];
2766+
isSeedless: Scalars['Boolean']['output'];
27642767
lbpName?: Maybe<Scalars['String']['output']>;
27652768
lbpOwner: Scalars['String']['output'];
27662769
projectToken: Scalars['String']['output'];
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { createPublicClient, http } from 'viem';
2+
import { ChainId, CHAINS } from '@/utils';
3+
import { Address, Hex } from '@/types';
4+
import { balancerUnbalancedAddViaSwapRouterAbiExtended } from '@/abi';
5+
import { AddressProvider } from '@/entities/inputValidator/utils/addressProvider';
6+
7+
export const doAddLiquidityUnbalancedViaSwapQuery = async (
8+
rpcUrl: string,
9+
chainId: ChainId,
10+
pool: Address,
11+
sender: Address,
12+
exactBptAmountOut: bigint,
13+
exactToken: Address,
14+
exactAmount: bigint,
15+
maxAdjustableAmount: bigint,
16+
addLiquidityUserData: Hex,
17+
swapUserData: Hex,
18+
block?: bigint,
19+
): Promise<bigint[]> => {
20+
const client = createPublicClient({
21+
transport: http(rpcUrl),
22+
chain: CHAINS[chainId],
23+
});
24+
25+
const { result: amountsIn } = await client.simulateContract({
26+
address: AddressProvider.UnbalancedAddViaSwapRouter(chainId),
27+
abi: balancerUnbalancedAddViaSwapRouterAbiExtended,
28+
functionName: 'queryAddLiquidityUnbalanced',
29+
args: [
30+
pool,
31+
sender,
32+
{
33+
exactBptAmountOut,
34+
exactToken,
35+
exactAmount,
36+
maxAdjustableAmount,
37+
addLiquidityUserData,
38+
swapUserData,
39+
},
40+
],
41+
blockNumber: block,
42+
});
43+
44+
return [...amountsIn];
45+
};
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import { encodeFunctionData, parseEther } from 'viem';
2+
import { TokenAmount } from '@/entities/tokenAmount';
3+
import { Token } from '@/entities/token';
4+
import { PoolState } from '@/entities/types';
5+
import { Permit2 } from '@/entities/permit2Helper';
6+
import {
7+
balancerUnbalancedAddViaSwapRouterAbiExtended,
8+
balancerRouterAbiExtended,
9+
} from '@/abi';
10+
import { AddressProvider } from '@/entities/inputValidator/utils/addressProvider';
11+
import {
12+
getValue,
13+
getSortedTokens,
14+
getBptAmountFromReferenceAmount,
15+
} from '@/entities/utils';
16+
import { validateAddLiquidityUnbalancedViaSwapInput } from './validateInputs';
17+
import {
18+
AddLiquidityUnbalancedViaSwapInput,
19+
AddLiquidityUnbalancedViaSwapQueryOutput,
20+
AddLiquidityUnbalancedViaSwapBuildCallInput,
21+
AddLiquidityUnbalancedViaSwapBuildCallOutput,
22+
} from './types';
23+
import { queryAndAdjustBptAmount } from '../utils/unbalancedAddViaSwapHelpers';
24+
import { AddLiquidityKind } from '../addLiquidity/types';
25+
import { MathSol, SDKError, WAD } from '@/utils';
26+
27+
// Export types
28+
export type {
29+
AddLiquidityUnbalancedViaSwapInput,
30+
AddLiquidityUnbalancedViaSwapQueryOutput,
31+
AddLiquidityUnbalancedViaSwapBuildCallInput,
32+
AddLiquidityUnbalancedViaSwapBuildCallOutput,
33+
} from './types';
34+
35+
export class AddLiquidityUnbalancedViaSwapV3 {
36+
async query(
37+
input: AddLiquidityUnbalancedViaSwapInput,
38+
poolState: PoolState,
39+
block?: bigint,
40+
): Promise<AddLiquidityUnbalancedViaSwapQueryOutput> {
41+
// Single-sided add liquidity: exact token amount is zero,
42+
// adjustable token has a finite budget.
43+
validateAddLiquidityUnbalancedViaSwapInput(input, poolState);
44+
45+
// Convert pool state amounts to TokenAmount
46+
const sortedTokens = getSortedTokens(poolState.tokens, input.chainId);
47+
48+
const expectedAdjustableTokenIndex = sortedTokens.findIndex((token) =>
49+
token.isSameAddress(input.expectedAdjustableAmountIn.address),
50+
);
51+
const exactAmountTokenIndex =
52+
expectedAdjustableTokenIndex === 0 ? 1 : 0;
53+
54+
// We treat the adjustable token as the reference and derive a BPT
55+
// target from it.
56+
57+
// Initial guess is 50% of the provided maxAjustableAmountIn.
58+
// It should result close to the desired bptAmount when
59+
// centeredness = 1 and result in a smaller bptAmount than the
60+
// desired when centeredness decreases.
61+
const initialBptAmount = await getBptAmountFromReferenceAmount(
62+
{
63+
chainId: input.chainId,
64+
rpcUrl: input.rpcUrl,
65+
referenceAmount: {
66+
...input.expectedAdjustableAmountIn,
67+
rawAmount: input.expectedAdjustableAmountIn.rawAmount / 2n,
68+
},
69+
kind: AddLiquidityKind.Proportional,
70+
},
71+
poolState,
72+
);
73+
74+
// Iteratively adjust BPT amount until we find an approximation that:
75+
// 1. Is from below (calculated <= expected) to favor leaving dust
76+
// 2. Is within 0.1% tolerance of the expected adjustable amount
77+
const MAX_ITERATIONS = 4;
78+
const TOLERANCE = parseEther('0.001'); // 0.1%
79+
80+
let currentBptAmount = initialBptAmount.rawAmount;
81+
let foundValidApproximation = false;
82+
83+
for (let i = 0; i < MAX_ITERATIONS; i++) {
84+
const { correctedBptAmount, calculatedAdjustableAmount } =
85+
await queryAndAdjustBptAmount(
86+
input,
87+
poolState.address,
88+
currentBptAmount,
89+
sortedTokens[exactAmountTokenIndex].address,
90+
expectedAdjustableTokenIndex,
91+
block,
92+
);
93+
94+
// Check if current BPT amount produces a valid approximation:
95+
// - Must be from below (ratio <= 1) to avoid transaction reverts
96+
// - Must be within 0.1% tolerance
97+
const ratio = MathSol.divDownFixed(
98+
calculatedAdjustableAmount,
99+
input.expectedAdjustableAmountIn.rawAmount,
100+
);
101+
102+
const isFromBelow = ratio <= WAD;
103+
const isWithinTolerance = WAD - ratio <= TOLERANCE;
104+
105+
if (isFromBelow && isWithinTolerance) {
106+
foundValidApproximation = true;
107+
break;
108+
}
109+
110+
// Not within tolerance yet, continue with corrected BPT amount
111+
currentBptAmount = correctedBptAmount;
112+
}
113+
114+
if (!foundValidApproximation) {
115+
throw new SDKError(
116+
'Error',
117+
'Add Liquidity Unbalanced Via Swap',
118+
'Exact BPT out calculation failed. Please add a smaller (less unbalanced) amount.',
119+
);
120+
}
121+
122+
const bptToken = new Token(input.chainId, poolState.address, 18);
123+
const bptOut = TokenAmount.fromRawAmount(bptToken, currentBptAmount);
124+
125+
const output: AddLiquidityUnbalancedViaSwapQueryOutput = {
126+
pool: poolState.address,
127+
bptOut,
128+
exactAmountIn: TokenAmount.fromRawAmount(
129+
sortedTokens[exactAmountTokenIndex],
130+
0n,
131+
),
132+
expectedAdjustableAmountIn: TokenAmount.fromInputAmount(
133+
input.expectedAdjustableAmountIn,
134+
input.chainId,
135+
),
136+
chainId: input.chainId,
137+
protocolVersion: 3,
138+
to: AddressProvider.UnbalancedAddViaSwapRouter(input.chainId),
139+
addLiquidityUserData: input.addLiquidityUserData ?? '0x',
140+
swapUserData: input.swapUserData ?? '0x',
141+
};
142+
return output;
143+
}
144+
145+
buildCall(
146+
input: AddLiquidityUnbalancedViaSwapBuildCallInput,
147+
): AddLiquidityUnbalancedViaSwapBuildCallOutput {
148+
const maxAdjustableAmountIn = TokenAmount.fromRawAmount(
149+
input.expectedAdjustableAmountIn.token,
150+
input.slippage.applyTo(input.expectedAdjustableAmountIn.amount),
151+
);
152+
153+
const wethIsEth = input.wethIsEth ?? false;
154+
155+
const callData = encodeFunctionData({
156+
abi: balancerUnbalancedAddViaSwapRouterAbiExtended,
157+
functionName: 'addLiquidityUnbalanced',
158+
args: [
159+
input.pool,
160+
input.deadline,
161+
wethIsEth,
162+
{
163+
exactBptAmountOut: input.bptOut.amount,
164+
exactToken: input.exactAmountIn.token.address,
165+
exactAmount: input.exactAmountIn.amount,
166+
maxAdjustableAmount: maxAdjustableAmountIn.amount,
167+
addLiquidityUserData: input.addLiquidityUserData,
168+
swapUserData: input.swapUserData,
169+
},
170+
] as const,
171+
});
172+
173+
const value = getValue([maxAdjustableAmountIn], wethIsEth);
174+
175+
return {
176+
callData,
177+
to: AddressProvider.UnbalancedAddViaSwapRouter(input.chainId),
178+
value,
179+
bptOut: input.bptOut,
180+
expectedAdjustableAmountIn: input.expectedAdjustableAmountIn,
181+
maxAdjustableAmountIn,
182+
};
183+
}
184+
185+
public buildCallWithPermit2(
186+
input: AddLiquidityUnbalancedViaSwapBuildCallInput,
187+
permit2: Permit2,
188+
): AddLiquidityUnbalancedViaSwapBuildCallOutput {
189+
// Generate same calldata as buildCall
190+
const buildCallOutput = this.buildCall(input);
191+
192+
const args = [
193+
[],
194+
[],
195+
permit2.batch,
196+
permit2.signature,
197+
[buildCallOutput.callData],
198+
] as const;
199+
200+
const callData = encodeFunctionData({
201+
abi: balancerRouterAbiExtended,
202+
functionName: 'permitBatchAndCall',
203+
args,
204+
});
205+
206+
return {
207+
...buildCallOutput,
208+
callData,
209+
};
210+
}
211+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Address, Hex } from 'viem';
2+
import { Slippage } from '../slippage';
3+
import { TokenAmount } from '../tokenAmount';
4+
import { InputAmount } from '@/types';
5+
6+
export type AddLiquidityUnbalancedViaSwapInput = {
7+
chainId: number;
8+
rpcUrl: string;
9+
expectedAdjustableAmountIn: InputAmount;
10+
addLiquidityUserData?: Hex;
11+
swapUserData?: Hex;
12+
sender?: Address;
13+
};
14+
15+
export type AddLiquidityUnbalancedViaSwapQueryOutput = {
16+
pool: Address;
17+
bptOut: TokenAmount;
18+
exactAmountIn: TokenAmount;
19+
expectedAdjustableAmountIn: TokenAmount;
20+
chainId: number;
21+
protocolVersion: 3;
22+
to: Address;
23+
addLiquidityUserData: Hex;
24+
swapUserData: Hex;
25+
};
26+
27+
export type AddLiquidityUnbalancedViaSwapBuildCallInput = {
28+
slippage: Slippage;
29+
wethIsEth?: boolean;
30+
deadline: bigint;
31+
} & AddLiquidityUnbalancedViaSwapQueryOutput;
32+
33+
export type AddLiquidityUnbalancedViaSwapBuildCallOutput = {
34+
callData: Hex;
35+
to: Address;
36+
value: bigint;
37+
bptOut: TokenAmount;
38+
expectedAdjustableAmountIn: TokenAmount;
39+
maxAdjustableAmountIn: TokenAmount;
40+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { SDKError } from '@/utils';
2+
import { AddLiquidityUnbalancedViaSwapInput } from './types';
3+
import { PoolState } from '../types';
4+
import { validateTokensAddLiquidity } from '../inputValidator/utils/validateTokens';
5+
import { AddLiquidityKind } from '../addLiquidity/types';
6+
7+
export const validateAddLiquidityUnbalancedViaSwapInput = (
8+
input: AddLiquidityUnbalancedViaSwapInput,
9+
poolState: PoolState,
10+
): void => {
11+
validateTokensAddLiquidity(
12+
{
13+
...input,
14+
kind: AddLiquidityKind.Unbalanced,
15+
amountsIn: [input.expectedAdjustableAmountIn],
16+
},
17+
poolState,
18+
);
19+
20+
if (poolState.tokens.length !== 2) {
21+
throw new SDKError(
22+
'Input Validation',
23+
'AddLiquidityUnbalancedViaSwap',
24+
'Pool should have exactly 2 tokens',
25+
);
26+
}
27+
28+
if (input.expectedAdjustableAmountIn.rawAmount <= 0n) {
29+
throw new SDKError(
30+
'Input Validation',
31+
'AddLiquidityUnbalancedViaSwap',
32+
'expectedAdjustableAmountIn should be greater than zero',
33+
);
34+
}
35+
};

src/entities/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './addLiquidity';
22
export * from './addLiquidity/types';
3+
export * from './addLiquidityUnbalancedViaSwap';
34
export * from './addLiquidityBoosted';
45
export * from './addLiquidityBoosted/types';
56
export * from './addLiquidityBuffer';

0 commit comments

Comments
 (0)