Skip to content

Commit

Permalink
Merge pull request #450 from balancer/feat-boosted-pool-add-and-remove
Browse files Browse the repository at this point in the history
Feat boosted pool add and remove
  • Loading branch information
mkflow27 authored Oct 30, 2024
2 parents c687227 + 1ce9784 commit 06be001
Show file tree
Hide file tree
Showing 21 changed files with 1,564 additions and 3 deletions.
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 =
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

0 comments on commit 06be001

Please sign in to comment.