Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat boosted pool add and remove #450

Merged
merged 43 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4ba2977
draft: boosted pool adds
mkflow27 Oct 23, 2024
4b0a12a
docs: remove comments.
mkflow27 Oct 23, 2024
6252354
undo mock data return
mkflow27 Oct 27, 2024
3150707
add liquidity boosted
mkflow27 Oct 27, 2024
f2965d0
remove liquidity boosted
mkflow27 Oct 28, 2024
5f3bdd2
Merge branch 'main' into feat-boosted-pool-add-and-remove
mkflow27 Oct 28, 2024
963604f
add/remove boosted
mkflow27 Oct 28, 2024
a92cbe2
undo version
mkflow27 Oct 28, 2024
d63b0f4
Update package.json
mkflow27 Oct 28, 2024
b871178
fix build
mkflow27 Oct 28, 2024
2bd0d05
Merge branch 'feat-boosted-pool-add-and-remove' of https://github.com…
mkflow27 Oct 29, 2024
03a662a
refactor: don't use helpers in tests
mkflow27 Oct 29, 2024
bb1b0e3
docs: remove debug info
mkflow27 Oct 29, 2024
344930f
refactor: change array access
mkflow27 Oct 29, 2024
4cc050f
test: remove unnecessary input
mkflow27 Oct 29, 2024
82e21f5
refactor: allow optional user data but use default values if none pro…
mkflow27 Oct 29, 2024
03e4537
refactor: remove api dependency & update shared poolState
mkflow27 Oct 29, 2024
acae0a2
refactor: harcode data, type refactoring
mkflow27 Oct 29, 2024
c71efcc
chore: lint
mkflow27 Oct 29, 2024
304c4b8
chore: update casing
mkflow27 Oct 29, 2024
325e5a6
test: skip nested tests
mkflow27 Oct 29, 2024
ac966a3
chore: syntax
mkflow27 Oct 29, 2024
4ab2ca3
refactor: return userData in query outputs
mkflow27 Oct 29, 2024
30bad2b
chore: lint
mkflow27 Oct 29, 2024
4782051
first pass input validator
mkflow27 Oct 29, 2024
4eb61ea
handle different input amounts lengths
mkflow27 Oct 30, 2024
e3e1766
chore: rename folder
mkflow27 Oct 30, 2024
b2007f6
docs: remove debugging comment
mkflow27 Oct 30, 2024
a79a673
feat: handle reference amount not pool token
mkflow27 Oct 30, 2024
88b755b
refactor: delete unnecessary additions
mkflow27 Oct 30, 2024
87d7742
fix: remove inaccurate assessment due to getAmounts being used
mkflow27 Oct 30, 2024
30a62d8
feat: expose optional userData arg for buildCall
mkflow27 Oct 30, 2024
8934f9d
feat: userData for remove liq
mkflow27 Oct 30, 2024
2518815
Merge branch 'main' into feat-boosted-pool-add-and-remove
mkflow27 Oct 30, 2024
ee784f5
chore: remove duplicate
mkflow27 Oct 30, 2024
fd9b7c4
chore: lint
mkflow27 Oct 30, 2024
2b615f0
refactor: addLiquidity.
johngrantuk Oct 30, 2024
d3a4091
refactor: removeLiquidity.
johngrantuk Oct 30, 2024
3a5b3ca
refactor: InputValidator.
johngrantuk Oct 30, 2024
f3ede14
fix: access underlyingToken
mkflow27 Oct 30, 2024
c4d8410
style: remove logs
mkflow27 Oct 30, 2024
98734f7
Merge pull request #456 from balancer/feat-boosted-pool-add-and-remov…
mkflow27 Oct 30, 2024
1ce9784
chore: add changeset
mkflow27 Oct 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tame-books-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@balancer/sdk": minor
---

added add and remove liquidity for boosted pools
4 changes: 4 additions & 0 deletions src/data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ export interface MinimalToken {
export interface PoolTokenWithBalance extends MinimalToken {
balance: HumanAmount;
}

export interface PoolTokenWithUnderlying extends MinimalToken {
underlyingToken: MinimalToken;
}
29 changes: 29 additions & 0 deletions src/entities/addLiquidityBoosted/doAddLiquidityPropotionalQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createPublicClient, Hex, http } from 'viem';

import { BALANCER_COMPOSITE_LIQUIDITY_ROUTER, ChainId, CHAINS } from '@/utils';

import { Address } from '@/types';

import { balancerCompositeLiquidityRouterAbi } from '@/abi';

export const doAddLiquidityProportionalQuery = async (
rpcUrl: string,
chainId: ChainId,
sender: Address,
userData: Hex,
poolAddress: Address,
exactBptAmountOut: bigint,
): Promise<bigint[]> => {
const client = createPublicClient({
transport: http(rpcUrl),
chain: CHAINS[chainId],
});

const { result: exactAmountsIn } = await client.simulateContract({
address: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId],
abi: balancerCompositeLiquidityRouterAbi,
functionName: 'queryAddLiquidityProportionalToERC4626Pool',
args: [poolAddress, exactBptAmountOut, sender, userData],
});
return [...exactAmountsIn];
};
29 changes: 29 additions & 0 deletions src/entities/addLiquidityBoosted/doAddLiquidityUnbalancedQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createPublicClient, Hex, http } from 'viem';

import { BALANCER_COMPOSITE_LIQUIDITY_ROUTER, ChainId, CHAINS } from '@/utils';

import { Address } from '@/types';

import { balancerCompositeLiquidityRouterAbi } from '@/abi';

export const doAddLiquidityUnbalancedQuery = async (
rpcUrl: string,
chainId: ChainId,
sender: Address,
userData: Hex,
poolAddress: Address,
exactUnderlyingAmountsIn: bigint[],
): Promise<bigint> => {
const client = createPublicClient({
transport: http(rpcUrl),
chain: CHAINS[chainId],
});

const { result: bptAmountOut } = await client.simulateContract({
address: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[chainId],
abi: balancerCompositeLiquidityRouterAbi,
functionName: 'queryAddLiquidityUnbalancedToERC4626Pool',
args: [poolAddress, exactUnderlyingAmountsIn, sender, userData],
});
return bptAmountOut;
};
211 changes: 211 additions & 0 deletions src/entities/addLiquidityBoosted/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// A user can add liquidity to a boosted pool in various forms. The following ways are
// available:
// 1. Unbalanced - addLiquidityUnbalancedToERC4626Pool
// 2. Proportional - addLiquidityProportionalToERC4626Pool
import { encodeFunctionData, zeroAddress } from 'viem';
import { TokenAmount } from '@/entities/tokenAmount';

import { Permit2 } from '@/entities/permit2Helper';

import { getAmountsCall } from '../addLiquidity/helpers';

import { PoolStateWithUnderlyings } from '@/entities/types';

import { getAmounts, getSortedTokens } from '@/entities/utils';

import {
AddLiquidityBuildCallOutput,
AddLiquidityKind,
} from '../addLiquidity/types';

import { doAddLiquidityUnbalancedQuery } from './doAddLiquidityUnbalancedQuery';
import { doAddLiquidityProportionalQuery } from './doAddLiquidityPropotionalQuery';
import { Token } from '../token';
import { BALANCER_COMPOSITE_LIQUIDITY_ROUTER } from '@/utils';
import { balancerCompositeLiquidityRouterAbi, balancerRouterAbi } from '@/abi';

import { InputValidator } from '../inputValidator/inputValidator';

import { Hex } from '@/types';
import {
AddLiquidityBoostedBuildCallInput,
AddLiquidityBoostedInput,
AddLiquidityBoostedQueryOutput,
} from './types';

export class AddLiquidityBoostedV3 {
private readonly inputValidator: InputValidator = new InputValidator();

async query(
input: AddLiquidityBoostedInput,
poolState: PoolStateWithUnderlyings,
): Promise<AddLiquidityBoostedQueryOutput> {
this.inputValidator.validateAddLiquidityBoosted(input, {
...poolState,
type: 'Boosted',
});

const bptToken = new Token(input.chainId, poolState.address, 18);

let bptOut: TokenAmount;
let amountsIn: TokenAmount[];

switch (input.kind) {
case AddLiquidityKind.Unbalanced: {
// It is allowed not not provide the same amount of TokenAmounts as inputs
// as the pool has tokens, in this case, the input tokens are filled with
// a default value ( 0 in this case ) to assure correct amounts in as the pool has tokens.
const underlyingTokens = poolState.tokens.map((t) => {
return t.underlyingToken;
});
const sortedTokens = getSortedTokens(
underlyingTokens,
input.chainId,
);
const maxAmountsIn = getAmounts(sortedTokens, input.amountsIn);

const bptAmountOut = await doAddLiquidityUnbalancedQuery(
input.rpcUrl,
input.chainId,
input.userAddress ?? zeroAddress,
input.userData ?? '0x',
poolState.address,
maxAmountsIn,
);
bptOut = TokenAmount.fromRawAmount(bptToken, bptAmountOut);

amountsIn = input.amountsIn.map((t) => {
return TokenAmount.fromRawAmount(
new Token(input.chainId, t.address, t.decimals),
t.rawAmount,
);
});

break;
}
case AddLiquidityKind.Proportional: {
if (input.referenceAmount.address !== poolState.address) {
// TODO: add getBptAmountFromReferenceAmount
throw new Error('Reference token must be the pool token');
}

const exactAmountsInNumbers =
johngrantuk marked this conversation as resolved.
Show resolved Hide resolved
await doAddLiquidityProportionalQuery(
input.rpcUrl,
input.chainId,
input.userAddress ?? zeroAddress,
input.userData ?? '0x',
poolState.address,
input.referenceAmount.rawAmount,
);

// Since the user adds tokens which are technically not pool tokens, the TokenAmount to return
// uses the pool's tokens underlyingTokens to indicate which tokens are being added from the user
// perspective
amountsIn = poolState.tokens.map((t, i) =>
TokenAmount.fromRawAmount(
new Token(
input.chainId,
t.underlyingToken.address,
t.underlyingToken.decimals,
),
exactAmountsInNumbers[i],
),
);

bptOut = TokenAmount.fromRawAmount(
bptToken,
input.referenceAmount.rawAmount,
);
break;
}
}

const output: AddLiquidityBoostedQueryOutput = {
poolId: poolState.id,
poolType: poolState.type,
addLiquidityKind: input.kind,
bptOut,
amountsIn,
chainId: input.chainId,
protocolVersion: 3,
userData: input.userData ?? '0x',
};

return output;
}

buildCall(
input: AddLiquidityBoostedBuildCallInput,
): AddLiquidityBuildCallOutput {
const amounts = getAmountsCall(input);
const args = [
input.poolId,
amounts.maxAmountsIn,
amounts.minimumBpt,
false,
input.userData,
] as const;
let callData: Hex;
switch (input.addLiquidityKind) {
case AddLiquidityKind.Unbalanced: {
callData = encodeFunctionData({
abi: balancerCompositeLiquidityRouterAbi,
functionName: 'addLiquidityUnbalancedToERC4626Pool',
args,
});
break;
}
case AddLiquidityKind.Proportional: {
callData = encodeFunctionData({
abi: balancerCompositeLiquidityRouterAbi,
functionName: 'addLiquidityProportionalToERC4626Pool',
args,
});
break;
}
case AddLiquidityKind.SingleToken: {
throw new Error('SingleToken not supported');
}
}
return {
callData,
to: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[input.chainId],
value: 0n, // Default to 0 as native not supported
minBptOut: TokenAmount.fromRawAmount(
input.bptOut.token,
amounts.minimumBpt,
),
maxAmountsIn: input.amountsIn.map((a, i) =>
TokenAmount.fromRawAmount(a.token, amounts.maxAmountsIn[i]),
),
};
}

public buildCallWithPermit2(
input: AddLiquidityBoostedBuildCallInput,
permit2: Permit2,
): AddLiquidityBuildCallOutput {
// generate same calldata as buildCall
const buildCallOutput = this.buildCall(input);

const args = [
[],
[],
permit2.batch,
permit2.signature,
[buildCallOutput.callData],
] as const;

const callData = encodeFunctionData({
abi: balancerRouterAbi,
functionName: 'permitBatchAndCall',
args,
});

return {
...buildCallOutput,
callData,
};
}
}
42 changes: 42 additions & 0 deletions src/entities/addLiquidityBoosted/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { InputAmount } from '@/types';
import { AddLiquidityKind } from '../addLiquidity/types';
import { Address, Hex } from 'viem';
import { TokenAmount } from '../tokenAmount';
import { Slippage } from '../slippage';

export type AddLiquidityBoostedProportionalInput = {
chainId: number;
rpcUrl: string;
referenceAmount: InputAmount;
kind: AddLiquidityKind.Proportional;
userAddress?: Address;
userData?: Hex;
};

export type AddLiquidityBoostedUnbalancedInput = {
chainId: number;
rpcUrl: string;
amountsIn: InputAmount[];
kind: AddLiquidityKind.Unbalanced;
userAddress?: Address;
userData?: Hex;
};

export type AddLiquidityBoostedInput =
| AddLiquidityBoostedUnbalancedInput
| AddLiquidityBoostedProportionalInput;

export type AddLiquidityBoostedQueryOutput = {
poolId: Hex;
poolType: string;
addLiquidityKind: AddLiquidityKind;
bptOut: TokenAmount;
amountsIn: TokenAmount[];
chainId: number;
protocolVersion: 3;
userData: Hex;
};

export type AddLiquidityBoostedBuildCallInput = {
slippage: Slippage;
} & AddLiquidityBoostedQueryOutput;
4 changes: 4 additions & 0 deletions src/entities/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export * from './addLiquidity';
export * from './addLiquidity/types';
export * from './addLiquidityBoosted';
export * from './addLiquidityBoosted/types';
export * from './addLiquidityNested';
export * from './addLiquidityNested/types';
export * from './addLiquidityNested/addLiquidityNestedV2/types';
Expand All @@ -14,6 +16,8 @@ export * from './priceImpactAmount';
export * from './relayer';
export * from './removeLiquidity';
export * from './removeLiquidity/types';
export * from './removeLiquidityBoosted';
export * from './removeLiquidityBoosted/types';
export * from './removeLiquidityNested/index';
export * from './removeLiquidityNested/types';
export * from './removeLiquidityNested/removeLiquidityNestedV2';
Expand Down
34 changes: 34 additions & 0 deletions src/entities/inputValidator/boosted/inputValidatorBoosted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { PoolStateWithUnderlyings } from '@/entities/types';
import { InputValidatorBase } from '../inputValidatorBase';
import { AddLiquidityKind } from '@/entities/addLiquidity/types';
import { AddLiquidityBoostedInput } from '@/entities/addLiquidityBoosted/types';

export class InputValidatorBoosted extends InputValidatorBase {
validateAddLiquidityBoosted(
addLiquidityInput: AddLiquidityBoostedInput,
poolState: PoolStateWithUnderlyings,
): void {
//check if poolState.protocolVersion is 3
if (poolState.protocolVersion !== 3) {
throw new Error('protocol version must be 3');
}

if (addLiquidityInput.kind === AddLiquidityKind.Unbalanced) {
// check if addLiquidityInput.amountsIn.address is contained in poolState.tokens.underlyingToken.address
const underlyingTokens = poolState.tokens.map((t) =>
t.underlyingToken.address.toLowerCase(),
);
addLiquidityInput.amountsIn.forEach((a) => {
if (
!underlyingTokens.includes(
a.address.toLowerCase() as `0x${string}`,
)
) {
throw new Error(
`Address ${a.address} is not contained in the pool's underlying tokens.`,
);
}
});
}
}
}
Loading
Loading