diff --git a/apps/frontend-v3/app/(app)/debug/pools/page.tsx b/apps/frontend-v3/app/(app)/debug/pools/page.tsx
index 888f798b6..4338e6233 100644
--- a/apps/frontend-v3/app/(app)/debug/pools/page.tsx
+++ b/apps/frontend-v3/app/(app)/debug/pools/page.tsx
@@ -16,8 +16,11 @@ export default function DebugPools() {
Sepolia WEIGHTED with Proportional joins (Balancer 50 BAL 50 WETH -ExitFee Hook)
-
- Sepolia reference BOOSTED pool (Balancer DAI/USDC/USDT)
+
+ Sepolia reference BOOSTED pool (Balancer USDC/USDT)
+
+
+ Sepolia reference NESTED pool (Balancer 50 WETH 50 USD)
diff --git a/packages/lib/config/config.types.ts b/packages/lib/config/config.types.ts
index 3d9487b72..fddfe723e 100644
--- a/packages/lib/config/config.types.ts
+++ b/packages/lib/config/config.types.ts
@@ -43,6 +43,7 @@ export interface ContractsConfig {
*/
router?: Address
batchRouter?: Address
+ compositeLiquidityRouter?: Address
relayerV6: Address
minter: Address
}
diff --git a/packages/lib/config/networks/sepolia.ts b/packages/lib/config/networks/sepolia.ts
index 2fbd9df31..8fbf70ccc 100644
--- a/packages/lib/config/networks/sepolia.ts
+++ b/packages/lib/config/networks/sepolia.ts
@@ -1,7 +1,13 @@
import { GqlChain } from '@repo/lib/shared/services/api/generated/graphql'
import { NetworkConfig } from '../config.types'
import { convertHexToLowerCase } from '@repo/lib/shared/utils/objects'
-import { BALANCER_BATCH_ROUTER, BALANCER_ROUTER, PERMIT2, VAULT_V3 } from '@balancer/sdk'
+import {
+ BALANCER_BATCH_ROUTER,
+ BALANCER_COMPOSITE_LIQUIDITY_ROUTER,
+ BALANCER_ROUTER,
+ PERMIT2,
+ VAULT_V3,
+} from '@balancer/sdk'
import { sepolia } from 'viem/chains'
const networkConfig: NetworkConfig = {
@@ -36,6 +42,7 @@ const networkConfig: NetworkConfig = {
vaultV3: VAULT_V3[sepolia.id],
router: BALANCER_ROUTER[sepolia.id],
batchRouter: BALANCER_BATCH_ROUTER[sepolia.id],
+ compositeLiquidityRouter: BALANCER_COMPOSITE_LIQUIDITY_ROUTER[sepolia.id],
relayerV6: '0x7852fB9d0895e6e8b3EedA553c03F6e2F9124dF9',
minter: '0x1783Cd84b3d01854A96B4eD5843753C2CcbD574A',
},
diff --git a/packages/lib/debug-helpers.ts b/packages/lib/debug-helpers.ts
index 723100d54..27c443d0f 100644
--- a/packages/lib/debug-helpers.ts
+++ b/packages/lib/debug-helpers.ts
@@ -40,6 +40,8 @@ export const vaultV3Address = sepoliaNetworkConfig.contracts.balancer.vaultV3 as
export const poolId = '0x68e3266c9c8bbd44ad9dca5afbfe629022aee9fe000200000000000000000512' as const // Balancer Weighted wjAura and WETH
export const sepoliaRouter = sepoliaNetworkConfig.contracts.balancer.router
+export const sepoliaCompositeRouter =
+ sepoliaNetworkConfig.contracts.balancer.compositeLiquidityRouter
/*
Used to pretty print objects when debugging
diff --git a/packages/lib/modules/pool/actions/LiquidityActionHelpers.integration.spec.ts b/packages/lib/modules/pool/actions/LiquidityActionHelpers.integration.spec.ts
index 187cd4ac8..7637a7804 100644
--- a/packages/lib/modules/pool/actions/LiquidityActionHelpers.integration.spec.ts
+++ b/packages/lib/modules/pool/actions/LiquidityActionHelpers.integration.spec.ts
@@ -10,7 +10,7 @@ import {
wETHAddress,
} from '@repo/lib/debug-helpers'
import { HumanTokenAmountWithAddress } from '@repo/lib/modules/tokens/token.types'
-import { GqlChain } from '@repo/lib/shared/services/api/generated/graphql'
+import { GqlChain, GqlPoolElement } from '@repo/lib/shared/services/api/generated/graphql'
import { getPoolMock } from '../__mocks__/getPoolMock'
import { allPoolTokens } from '../pool.helpers'
import { LiquidityActionHelpers } from './LiquidityActionHelpers'
@@ -46,12 +46,16 @@ describe('Calculates toInputAmounts from allPoolTokens', () => {
{ humanAmount: '100', tokenAddress: daiAddress },
]
- expect(allPoolTokens(nestedPool).map(t => t.address)).toEqual([
- usdcDaiUsdtBptAddress, // Phantom BPT
+ expect(
+ allPoolTokens(nestedPool)
+ .map(t => t.address)
+ .sort()
+ ).toEqual([
daiAddress,
+ usdcDaiUsdtBptAddress, // Phantom BPT
usdcAddress,
- usdtAddress,
wETHAddress,
+ usdtAddress,
])
const helpers = new LiquidityActionHelpers(nestedPool)
@@ -91,26 +95,160 @@ describe('Calculates toInputAmounts from allPoolTokens', () => {
},
])
})
+})
+
+// Unskip when sepolia V3 pools are available in production api
+describe.skip('Liquidity helpers for V3 Boosted pools', async () => {
+ const poolId = '0x6dbdd7a36d900083a5b86a55583d90021e9f33e8' // Sepolia stataEthUSDC stataEthUSDT
+
+ const usdcSepoliaAddress = '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8'
+ const usdtSepoliaAddress = '0xaa8e23fb1079ea71e0a56f48a2aa51851d8433d0'
+ // const v3Pool = await getPoolMock(poolId, GqlChain.Sepolia)
+ const v3Pool = {} as GqlPoolElement
+ const helpers = new LiquidityActionHelpers(v3Pool)
- // Unskip when sepolia V3 pools are available in production api
- it.skip('for v3 BOOSTED pool', async () => {
- const poolId = '0x6dbdd7a36d900083a5b86a55583d90021e9f33e8' // Sepolia stataEthUSDC stataEthUSDT
+ const humanAmountsIn: HumanTokenAmountWithAddress[] = [
+ { humanAmount: '0.1', tokenAddress: usdcSepoliaAddress },
+ ]
- const usdcSepoliaAddress = '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8'
- const usdtSepoliaAddress = '0xaa8e23fb1079ea71e0a56f48a2aa51851d8433d0'
- const v3Pool = await getPoolMock(poolId, GqlChain.Sepolia)
+ it('allPoolTokens', async () => {
+ expect(allPoolTokens(v3Pool).map(t => t.address)).toEqual([
+ usdcSepoliaAddress,
+ usdtSepoliaAddress,
+ ])
+ })
+ it('allPoolTokens snapshot', async () => {
+ expect(allPoolTokens(v3Pool)).toMatchInlineSnapshot(`
+ [
+ {
+ "address": "0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8",
+ "decimals": 6,
+ "index": 0,
+ "name": "USDC (AAVE Faucet)",
+ "symbol": "usdc-aave",
+ },
+ {
+ "address": "0xaa8e23fb1079ea71e0a56f48a2aa51851d8433d0",
+ "decimals": 6,
+ "index": 1,
+ "name": "USDT (AAVE Faucet)",
+ "symbol": "usdt-aave",
+ },
+ ]
+ `)
+ })
+
+ it('toInputAmounts', async () => {
+ expect(helpers.toInputAmounts(humanAmountsIn)).toEqual([
+ {
+ address: usdcSepoliaAddress,
+ decimals: 6,
+ rawAmount: 100000n,
+ },
+ ])
+ })
+
+ it('boostedPoolState', async () => {
+ const helpers = new LiquidityActionHelpers(v3Pool)
+ expect(helpers.boostedPoolState).toMatchObject({
+ address: '0x6dbdd7a36d900083a5b86a55583d90021e9f33e8',
+ id: '0x6dbdd7a36d900083a5b86a55583d90021e9f33e8',
+ protocolVersion: 3,
+ tokens: [
+ {
+ address: '0x8a88124522dbbf1e56352ba3de1d9f78c143751e',
+ balance: expect.any(String),
+ balanceUSD: expect.any(String),
+ decimals: 6,
+ hasNestedPool: false,
+ id: '0x6dbdd7a36d900083a5b86a55583d90021e9f33e8-0x8a88124522dbbf1e56352ba3de1d9f78c143751e',
+ index: 0,
+ isAllowed: true,
+ isErc4626: true,
+ name: 'Static Aave Ethereum USDC',
+ nestedPool: null,
+ priceRate: expect.any(String),
+ priceRateProvider: '0x34101091673238545de8a846621823d9993c3085',
+ priceRateProviderData: null,
+ symbol: 'stataEthUSDC',
+ underlyingToken: {
+ address: '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8',
+ decimals: 6,
+ index: 0,
+ name: 'USDC (AAVE Faucet)',
+ symbol: 'usdc-aave',
+ },
+ weight: null,
+ },
+ {
+ address: '0x978206fae13faf5a8d293fb614326b237684b750',
+ balance: expect.any(String),
+ balanceUSD: expect.any(String),
+ decimals: 6,
+ hasNestedPool: false,
+ id: '0x6dbdd7a36d900083a5b86a55583d90021e9f33e8-0x978206fae13faf5a8d293fb614326b237684b750',
+ index: 1,
+ isAllowed: true,
+ isErc4626: true,
+ name: 'Static Aave Ethereum USDT',
+ nestedPool: null,
+ priceRate: expect.any(String),
+ priceRateProvider: '0xb1b171a07463654cc1fe3df4ec05f754e41f0a65',
+ priceRateProviderData: null,
+ symbol: 'stataEthUSDT',
+ underlyingToken: {
+ address: '0xaa8e23fb1079ea71e0a56f48a2aa51851d8433d0',
+ decimals: 6,
+ index: 1,
+ name: 'USDT (AAVE Faucet)',
+ symbol: 'usdt-aave',
+ },
+ weight: null,
+ },
+ ],
+ type: 'Stable',
+ })
+ })
+})
+
+// Unskip when sepolia V3 pools are available in production api
+describe.skip('Liquidity helpers for V3 NESTED pool', async () => {
+ const poolId = '0x0270daf4ee12ccb1abc8aa365054eecb1b7f4f6b' // Sepolia Balancer 50 WETH 50 USD
+
+ const usdcSepoliaAddress = '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8'
+ const usdtSepoliaAddress = '0xaa8e23fb1079ea71e0a56f48a2aa51851d8433d0'
+ // const v3Pool = await getPoolMock(poolId, GqlChain.Sepolia)
+ const v3Pool = {} as GqlPoolElement
+
+ const helpers = new LiquidityActionHelpers(v3Pool)
+ const wethAddress = '0x7b79995e5f793a07bc00c21412e50ecae098e7f9'
+
+ const usdcUsdtSepoliaBptAddress = '0x6dbdd7a36d900083a5b86a55583d90021e9f33e8'
+
+ const aaveUSDCAddress = '0x8a88124522dbbf1e56352ba3de1d9f78c143751e'
+ const aaveUSDTAddress = '0x978206fae13faf5a8d293fb614326b237684b750'
+
+ const humanAmountsIn: HumanTokenAmountWithAddress[] = [
+ { humanAmount: '0.1', tokenAddress: usdcSepoliaAddress },
+ ]
+
+ it('allPoolTokens', async () => {
expect(
allPoolTokens(v3Pool)
.map(t => t.address)
.sort()
- ).toMatchInlineSnapshot([usdcSepoliaAddress, usdtSepoliaAddress])
-
- const humanAmountsIn: HumanTokenAmountWithAddress[] = [
- { humanAmount: '0.1', tokenAddress: usdcSepoliaAddress },
- ]
- const helpers = new LiquidityActionHelpers(v3Pool)
+ ).toEqual([
+ usdcUsdtSepoliaBptAddress,
+ wethAddress,
+ aaveUSDCAddress,
+ usdcSepoliaAddress,
+ aaveUSDTAddress,
+ usdtSepoliaAddress,
+ ])
+ })
+ it('toInputAmounts', async () => {
expect(helpers.toInputAmounts(humanAmountsIn)).toEqual([
{
address: usdcSepoliaAddress,
diff --git a/packages/lib/modules/pool/actions/LiquidityActionHelpers.spec.ts b/packages/lib/modules/pool/actions/LiquidityActionHelpers.spec.ts
index 4b5ab194d..8da48d979 100644
--- a/packages/lib/modules/pool/actions/LiquidityActionHelpers.spec.ts
+++ b/packages/lib/modules/pool/actions/LiquidityActionHelpers.spec.ts
@@ -64,7 +64,7 @@ it('returns poolState for non nested pools', () => {
it('returns NestedPoolState for nested pools', () => {
const helpers = new LiquidityActionHelpers(nestedPoolMock)
- const nestedPoolState = helpers.nestedPoolState
+ const nestedPoolState = helpers.nestedPoolStateV2
expect(nestedPoolState.pools).toHaveLength(2)
const firstPool = nestedPoolState.pools[0]
diff --git a/packages/lib/modules/pool/actions/LiquidityActionHelpers.ts b/packages/lib/modules/pool/actions/LiquidityActionHelpers.ts
index 6eb93b658..23f6ea395 100644
--- a/packages/lib/modules/pool/actions/LiquidityActionHelpers.ts
+++ b/packages/lib/modules/pool/actions/LiquidityActionHelpers.ts
@@ -14,9 +14,12 @@ import {
PoolGetPool,
PoolState,
PoolStateWithBalances,
+ PoolStateWithUnderlyings,
+ PoolTokenWithUnderlying,
Token,
TokenAmount,
mapPoolToNestedPoolStateV2,
+ mapPoolToNestedPoolStateV3,
mapPoolType,
} from '@balancer/sdk'
import BigNumber from 'bignumber.js'
@@ -61,12 +64,44 @@ export class LiquidityActionHelpers {
}
/* Used by default nested SDK handlers */
- public get nestedPoolState(): NestedPoolState {
+ public get nestedPoolStateV2(): NestedPoolState {
const result = mapPoolToNestedPoolStateV2(this.pool as PoolGetPool)
result.protocolVersion = 2
return result
}
+ /* Used by default nested SDK handlers */
+ public get nestedPoolStateV3(): NestedPoolState {
+ const result = mapPoolToNestedPoolStateV3(this.pool as PoolGetPool)
+ result.protocolVersion = 3
+ return result
+ }
+
+ /* Used by V3 boosted SDK handlers */
+ public get boostedPoolState(): PoolStateWithUnderlyings & { totalShares: HumanAmount } {
+ const poolTokensWithUnderlyings: PoolTokenWithUnderlying[] = this.pool.poolTokens.map(
+ (token, index) => ({
+ ...token,
+ address: token.address as Address,
+ underlyingToken: {
+ ...token.underlyingToken,
+ address: token.underlyingToken?.address as Address,
+ decimals: token.underlyingToken?.decimals as number,
+ index, //TODO: review that this index is always the expected one
+ },
+ })
+ )
+ const state: PoolStateWithUnderlyings & { totalShares: HumanAmount } = {
+ id: this.pool.id as Hex,
+ address: this.pool.address as Address,
+ protocolVersion: 3,
+ type: mapPoolType(this.pool.type),
+ tokens: poolTokensWithUnderlyings,
+ totalShares: this.pool.dynamicData.totalShares as HumanAmount,
+ }
+ return state
+ }
+
public get poolStateWithBalances(): PoolStateWithBalances {
return toPoolStateWithBalances(this.pool)
}
diff --git a/packages/lib/modules/pool/actions/add-liquidity/form/AddLiquidityForm.tsx b/packages/lib/modules/pool/actions/add-liquidity/form/AddLiquidityForm.tsx
index ef2b6ba5e..75b5746ce 100644
--- a/packages/lib/modules/pool/actions/add-liquidity/form/AddLiquidityForm.tsx
+++ b/packages/lib/modules/pool/actions/add-liquidity/form/AddLiquidityForm.tsx
@@ -55,6 +55,7 @@ import { ConnectWallet } from '@repo/lib/modules/web3/ConnectWallet'
import { BalAlert } from '@repo/lib/shared/components/alerts/BalAlert'
import { SafeAppAlert } from '@repo/lib/shared/components/alerts/SafeAppAlert'
import { useTokens } from '@repo/lib/modules/tokens/TokensProvider'
+import { isBoosted } from '../../../pool.helpers'
// small wrapper to prevent out of context error
export function AddLiquidityForm() {
@@ -188,7 +189,11 @@ function AddLiquidityMainForm() {
)}
- {!nestedAddLiquidityEnabled ? (
+ {/* //TODO:
+ Avoid proportional inputs to avoid error above until SDK calculateProportionalAmounts for boosted is implemented
+ https://github.com/balancer/b-sdk/issues/468
+ */}
+ {!nestedAddLiquidityEnabled && !isBoosted(pool) ? (
tokenSelectDisclosure.onOpen()}
diff --git a/packages/lib/modules/pool/actions/add-liquidity/handlers/BoostedUnbalancedAddLiquidityV3.handler.integration.spec.ts b/packages/lib/modules/pool/actions/add-liquidity/handlers/BoostedUnbalancedAddLiquidityV3.handler.integration.spec.ts
new file mode 100644
index 000000000..7770e1948
--- /dev/null
+++ b/packages/lib/modules/pool/actions/add-liquidity/handlers/BoostedUnbalancedAddLiquidityV3.handler.integration.spec.ts
@@ -0,0 +1,52 @@
+/* eslint-disable max-len */
+import { getNetworkConfig } from '@repo/lib/config/app.config'
+import { HumanTokenAmountWithAddress } from '@repo/lib/modules/tokens/token.types'
+import { GqlChain, GqlPoolElement } from '@repo/lib/shared/services/api/generated/graphql'
+import { defaultTestUserAccount } from '@repo/lib/test/anvil/anvil-setup'
+import { getPoolMock } from '../../../__mocks__/getPoolMock'
+import { BoostedUnbalancedAddLiquidityV3Handler } from './BoostedUnbalancedAddLiquidityV3.handler'
+import { selectAddLiquidityHandler } from './selectAddLiquidityHandler'
+
+// TODO: unskip this test when sepolia V3 pools are available in production api
+describe.skip('When adding unbalanced liquidity for a V3 BOOSTED pool', async () => {
+ // Sepolia
+ const poolId = '0x6dbdd7a36d900083a5b86a55583d90021e9f33e8' // Sepolia stataEthUSDC stataEthUSDT
+
+ const usdcAaveAddress = '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8' // Sepolia underlying usdcAave faucet address (temporary until we have the real one)
+ const usdtAaveAddress = '0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0' // Sepolia underlying usdcAave faucet address (temporary until we have the real one)
+ // const v3Pool = await getPoolMock(poolId, GqlChain.Sepolia)
+ const v3Pool = {} as GqlPoolElement
+
+ const handler = selectAddLiquidityHandler(v3Pool) as BoostedUnbalancedAddLiquidityV3Handler
+
+ const humanAmountsIn: HumanTokenAmountWithAddress[] = [
+ { humanAmount: '0.1', tokenAddress: usdcAaveAddress },
+ { humanAmount: '0.1', tokenAddress: usdtAaveAddress },
+ ]
+
+ it('calculates price impact', async () => {
+ const priceImpact = await handler.getPriceImpact(humanAmountsIn)
+ expect(priceImpact).toBe(0)
+ })
+
+ it('queries bptOut', async () => {
+ const result = await handler.simulate(humanAmountsIn)
+
+ expect(result.bptOut.amount).toBeGreaterThan(100000000000000n)
+ expect(result.bptOut.token.address).toBe(poolId)
+ })
+
+ it('builds Tx Config', async () => {
+ const queryOutput = await handler.simulate(humanAmountsIn)
+
+ const result = await handler.buildCallData({
+ humanAmountsIn,
+ account: defaultTestUserAccount,
+ slippagePercent: '0.2',
+ queryOutput,
+ })
+ const router = getNetworkConfig(GqlChain.Sepolia).contracts.balancer.compositeLiquidityRouter
+ expect(result.to).toBe(router)
+ expect(result.data).toBeDefined()
+ })
+})
diff --git a/packages/lib/modules/pool/actions/add-liquidity/handlers/BoostedUnbalancedAddLiquidityV3.handler.ts b/packages/lib/modules/pool/actions/add-liquidity/handlers/BoostedUnbalancedAddLiquidityV3.handler.ts
new file mode 100644
index 000000000..314bcb7f8
--- /dev/null
+++ b/packages/lib/modules/pool/actions/add-liquidity/handlers/BoostedUnbalancedAddLiquidityV3.handler.ts
@@ -0,0 +1,79 @@
+import {
+ AddLiquidityBoostedBuildCallInput,
+ AddLiquidityBoostedInput,
+ AddLiquidityBoostedV3,
+ Hex,
+ PriceImpact,
+ PriceImpactAmount,
+} from '@balancer/sdk'
+import { HumanTokenAmountWithAddress } from '@repo/lib/modules/tokens/token.types'
+import { TransactionConfig } from '@repo/lib/modules/web3/contracts/contract.types'
+import { SdkBuildAddLiquidityInput, SdkQueryAddLiquidityOutput } from '../add-liquidity.types'
+import { BaseUnbalancedAddLiquidityHandler } from './BaseUnbalancedAddLiquidity.handler'
+import { constructBaseBuildCallInput } from './add-liquidity.utils'
+import { areEmptyAmounts } from '../../LiquidityActionHelpers'
+
+export class BoostedUnbalancedAddLiquidityV3Handler extends BaseUnbalancedAddLiquidityHandler {
+ public async getPriceImpact(humanAmountsIn: HumanTokenAmountWithAddress[]): Promise {
+ if (areEmptyAmounts(humanAmountsIn)) {
+ // Avoid price impact calculation when there are no amounts in
+ return 0
+ }
+
+ const addLiquidityInput = this.constructSdkInput(humanAmountsIn)
+
+ const priceImpactABA: PriceImpactAmount = await PriceImpact.addLiquidityUnbalancedBoosted(
+ addLiquidityInput,
+ this.helpers.boostedPoolState
+ )
+
+ return priceImpactABA.decimal
+ }
+
+ public async simulate(
+ humanAmountsIn: HumanTokenAmountWithAddress[]
+ ): Promise {
+ const addLiquidity = new AddLiquidityBoostedV3()
+ const addLiquidityInput: AddLiquidityBoostedInput = this.constructSdkInput(humanAmountsIn)
+
+ const sdkQueryOutput = await addLiquidity.query(
+ addLiquidityInput,
+ this.helpers.boostedPoolState
+ )
+
+ return { bptOut: sdkQueryOutput.bptOut, to: sdkQueryOutput.to, sdkQueryOutput }
+ }
+
+ public async buildCallData({
+ humanAmountsIn,
+ slippagePercent,
+ queryOutput,
+ account,
+ permit2,
+ }: SdkBuildAddLiquidityInput): Promise {
+ const addLiquidity = new AddLiquidityBoostedV3()
+
+ const buildCallParams: AddLiquidityBoostedBuildCallInput = {
+ ...constructBaseBuildCallInput({
+ humanAmountsIn,
+ sdkQueryOutput: queryOutput.sdkQueryOutput,
+ slippagePercent: slippagePercent,
+ pool: this.helpers.pool,
+ }),
+ protocolVersion: 3,
+ userData: '0x' as Hex,
+ }
+
+ const { callData, to, value } = permit2
+ ? addLiquidity.buildCallWithPermit2(buildCallParams, permit2)
+ : addLiquidity.buildCall(buildCallParams)
+
+ return {
+ account,
+ chainId: this.helpers.chainId,
+ data: callData,
+ to,
+ value,
+ }
+ }
+}
diff --git a/packages/lib/modules/pool/actions/add-liquidity/handlers/NestedAddLiquidity.handler.ts b/packages/lib/modules/pool/actions/add-liquidity/handlers/NestedAddLiquidity.handler.ts
index 66dfabee2..28f81cc1a 100644
--- a/packages/lib/modules/pool/actions/add-liquidity/handlers/NestedAddLiquidity.handler.ts
+++ b/packages/lib/modules/pool/actions/add-liquidity/handlers/NestedAddLiquidity.handler.ts
@@ -35,7 +35,10 @@ export class NestedAddLiquidityHandler implements AddLiquidityHandler {
return 0
}
const input = this.constructSdkInput(humanAmountsIn)
- const priceImpactABA = await PriceImpact.addLiquidityNested(input, this.helpers.nestedPoolState)
+ const priceImpactABA = await PriceImpact.addLiquidityNested(
+ input,
+ this.helpers.nestedPoolStateV2
+ )
return priceImpactABA.decimal
}
@@ -47,7 +50,10 @@ export class NestedAddLiquidityHandler implements AddLiquidityHandler {
const addLiquidityInput = this.constructSdkInput(humanAmountsIn, userAddress)
- const sdkQueryOutput = await addLiquidity.query(addLiquidityInput, this.helpers.nestedPoolState)
+ const sdkQueryOutput = await addLiquidity.query(
+ addLiquidityInput,
+ this.helpers.nestedPoolStateV2
+ )
return { bptOut: sdkQueryOutput.bptOut, to: sdkQueryOutput.to, sdkQueryOutput }
}
diff --git a/packages/lib/modules/pool/actions/add-liquidity/handlers/selectAddLiquidityHandler.ts b/packages/lib/modules/pool/actions/add-liquidity/handlers/selectAddLiquidityHandler.ts
index 4143cba98..801a3c6a8 100644
--- a/packages/lib/modules/pool/actions/add-liquidity/handlers/selectAddLiquidityHandler.ts
+++ b/packages/lib/modules/pool/actions/add-liquidity/handlers/selectAddLiquidityHandler.ts
@@ -6,14 +6,17 @@ import { AddLiquidityHandler } from './AddLiquidity.handler'
import { NestedAddLiquidityHandler } from './NestedAddLiquidity.handler'
import { requiresProportionalInput, supportsNestedActions } from '../../LiquidityActionHelpers'
import { ProportionalAddLiquidityHandler } from './ProportionalAddLiquidity.handler'
-import { isV3Pool } from '../../../pool.helpers'
+import { isBoosted, isV3Pool } from '../../../pool.helpers'
import { ProportionalAddLiquidityHandlerV3 } from './ProportionalAddLiquidityV3.handler'
import { UnbalancedAddLiquidityV3Handler } from './UnbalancedAddLiquidityV3.handler'
+import { BoostedUnbalancedAddLiquidityV3Handler } from './BoostedUnbalancedAddLiquidityV3.handler'
export function selectAddLiquidityHandler(pool: Pool): AddLiquidityHandler {
// This is just an example to illustrate how edge-case handlers would receive different inputs but return a common contract
if (pool.id === 'TWAMM-example') return new TwammAddLiquidityHandler(getChainId(pool.chain))
+ if (isBoosted(pool)) return new BoostedUnbalancedAddLiquidityV3Handler(pool)
+
if (requiresProportionalInput(pool)) {
if (isV3Pool(pool)) {
return new ProportionalAddLiquidityHandlerV3(pool)
diff --git a/packages/lib/modules/pool/actions/remove-liquidity/RemoveLiquidityProvider.tsx b/packages/lib/modules/pool/actions/remove-liquidity/RemoveLiquidityProvider.tsx
index 9fabd0f66..3abbb229e 100644
--- a/packages/lib/modules/pool/actions/remove-liquidity/RemoveLiquidityProvider.tsx
+++ b/packages/lib/modules/pool/actions/remove-liquidity/RemoveLiquidityProvider.tsx
@@ -23,6 +23,7 @@ import { useTransactionSteps } from '@repo/lib/modules/transactions/transaction-
import { HumanTokenAmountWithAddress } from '@repo/lib/modules/tokens/token.types'
import { getUserWalletBalance } from '../../user-balance.helpers'
import { useModalWithPoolRedirect } from '../../useModalWithPoolRedirect'
+import { GqlToken } from '@repo/lib/shared/services/api/generated/graphql'
export type UseRemoveLiquidityResponse = ReturnType
export const RemoveLiquidityContext = createContext(null)
@@ -73,7 +74,7 @@ export function _useRemoveLiquidity(urlTxHash?: Hash) {
const isSingleToken = removalType === RemoveLiquidityType.SingleToken
const isProportional = removalType === RemoveLiquidityType.Proportional
- function tokensToShow() {
+ function tokensToShow(): GqlToken[] {
// Cow AMM pools don't support wethIsEth
if (isCowAmmPool(pool.type)) return tokens
@@ -83,13 +84,15 @@ export function _useRemoveLiquidity(urlTxHash?: Hash) {
// if wethIsEth we only show the native asset
if (includesWrappedNativeAsset && wethIsEth) {
// replace the wrapped native asset with the native asset
- return tokens.map(token => {
- if (token && isWrappedNativeAsset(token.address as Address, chain)) {
- return nativeAsset
- } else {
- return token
- }
- })
+ return tokens
+ .map(token => {
+ if (token && isWrappedNativeAsset(token.address as Address, chain)) {
+ return nativeAsset
+ } else {
+ return token
+ }
+ })
+ .filter((token): token is GqlToken => token !== undefined)
}
return tokens
diff --git a/packages/lib/modules/pool/actions/remove-liquidity/form/RemoveLiquidityForm.tsx b/packages/lib/modules/pool/actions/remove-liquidity/form/RemoveLiquidityForm.tsx
index e23cb885e..69955fa36 100644
--- a/packages/lib/modules/pool/actions/remove-liquidity/form/RemoveLiquidityForm.tsx
+++ b/packages/lib/modules/pool/actions/remove-liquidity/form/RemoveLiquidityForm.tsx
@@ -39,19 +39,27 @@ import { SafeAppAlert } from '@repo/lib/shared/components/alerts/SafeAppAlert'
import { useTokens } from '@repo/lib/modules/tokens/TokensProvider'
import { TooltipWithTouch } from '@repo/lib/shared/components/tooltips/TooltipWithTouch'
import { useUserSettings } from '@repo/lib/modules/user/settings/UserSettingsProvider'
-
-const TABS: ButtonGroupOption[] = [
- {
- value: 'proportional',
- label: 'Proportional',
- },
- {
- value: 'single',
- label: 'Single token',
- },
-] as const
+import { isBoosted } from '../../../pool.helpers'
export function RemoveLiquidityForm() {
+ const { pool } = usePool()
+
+ const TABS: ButtonGroupOption[] = [
+ {
+ value: 'proportional',
+ label: 'Proportional',
+ },
+ {
+ value: 'single',
+ label: 'Single token',
+ //Boosted pools do not support single token removes
+ disabled: isBoosted(pool),
+ },
+ ] as const
+ const [activeTab, setActiveTab] = useState(TABS[0])
+ const isProportionalTabSelected = activeTab.value === 'proportional'
+ const isSingleTabSelected = activeTab.value === 'single'
+
const {
transactionSteps,
tokens,
@@ -72,11 +80,9 @@ export function RemoveLiquidityForm() {
setHumanBptInPercent,
setNeedsToAcceptHighPI,
} = useRemoveLiquidity()
- const { pool } = usePool()
const { priceImpactColor, priceImpact, setPriceImpact } = usePriceImpact()
const { redirectToPoolPage } = usePoolRedirect(pool)
const nextBtn = useRef(null)
- const [activeTab, setActiveTab] = useState(TABS[0])
const { startTokenPricePolling } = useTokens()
const { slippage } = useUserSettings()
@@ -180,10 +186,10 @@ export function RemoveLiquidityForm() {
You can only remove up to 25% of a single asset from the pool in one transaction
)}
- {activeTab === TABS[0] && (
+ {isProportionalTabSelected && (
)}
- {activeTab === TABS[1] && (
+ {isSingleTabSelected && (
)}
diff --git a/packages/lib/modules/pool/actions/remove-liquidity/form/RemoveLiquidityProportional.tsx b/packages/lib/modules/pool/actions/remove-liquidity/form/RemoveLiquidityProportional.tsx
index 170729bee..7a964fcd2 100644
--- a/packages/lib/modules/pool/actions/remove-liquidity/form/RemoveLiquidityProportional.tsx
+++ b/packages/lib/modules/pool/actions/remove-liquidity/form/RemoveLiquidityProportional.tsx
@@ -10,7 +10,7 @@ import { isNativeAsset, isNativeOrWrappedNative } from '@repo/lib/modules/tokens
import { NativeAssetSelectModal } from '@repo/lib/modules/tokens/NativeAssetSelectModal'
import { shouldShowNativeWrappedSelector } from '../../LiquidityActionHelpers'
-type Props = { tokens: (GqlToken | undefined)[]; poolType: GqlPoolType }
+type Props = { tokens: GqlToken[]; poolType: GqlPoolType }
export function RemoveLiquidityProportional({ tokens, poolType }: Props) {
const { amountOutForToken, validTokens, setWethIsEth, simulationQuery, priceImpactQuery } =
useRemoveLiquidity()
diff --git a/packages/lib/modules/pool/actions/remove-liquidity/handlers/BaseProportionalRemoveLiquidity.handler.ts b/packages/lib/modules/pool/actions/remove-liquidity/handlers/BaseProportionalRemoveLiquidity.handler.ts
index b35fc8f46..046211963 100644
--- a/packages/lib/modules/pool/actions/remove-liquidity/handlers/BaseProportionalRemoveLiquidity.handler.ts
+++ b/packages/lib/modules/pool/actions/remove-liquidity/handlers/BaseProportionalRemoveLiquidity.handler.ts
@@ -25,6 +25,11 @@ export abstract class BaseProportionalRemoveLiquidityHandler implements RemoveLi
this.helpers = new LiquidityActionHelpers(pool)
}
+ public async getPriceImpact(): Promise {
+ // proportional remove liquidity does not have price impact
+ return 0
+ }
+
public async simulate({
humanBptIn: bptIn,
userAddress,
@@ -37,11 +42,6 @@ export abstract class BaseProportionalRemoveLiquidityHandler implements RemoveLi
return { amountsOut: sdkQueryOutput.amountsOut.filter(a => a.amount > 0n), sdkQueryOutput }
}
- public async getPriceImpact(): Promise {
- // proportional remove liquidity does not have price impact
- return 0
- }
-
public abstract buildCallData(inputs: BuildRemoveLiquidityInput): Promise
/**
diff --git a/packages/lib/modules/pool/actions/remove-liquidity/handlers/BoostedProportionalRemoveLiquidityV3.handler.integration.spec.ts b/packages/lib/modules/pool/actions/remove-liquidity/handlers/BoostedProportionalRemoveLiquidityV3.handler.integration.spec.ts
new file mode 100644
index 000000000..bff104db7
--- /dev/null
+++ b/packages/lib/modules/pool/actions/remove-liquidity/handlers/BoostedProportionalRemoveLiquidityV3.handler.integration.spec.ts
@@ -0,0 +1,67 @@
+import { sepoliaCompositeRouter } from '@repo/lib/debug-helpers'
+import { emptyAddress } from '@repo/lib/modules/web3/contracts/wagmi-helpers'
+import { defaultTestUserAccount } from '@repo/lib/test/anvil/anvil-setup'
+import { connectWithDefaultUser } from '@repo/lib/test/utils/wagmi/wagmi-connections'
+import { Pool } from '../../../PoolProvider'
+import { QueryRemoveLiquidityInput, RemoveLiquidityType } from '../remove-liquidity.types'
+import { BoostedProportionalRemoveLiquidityV3Handler } from './BoostedProportionalRemoveLiquidityV3.handler'
+import { selectRemoveLiquidityHandler } from './selectRemoveLiquidityHandler'
+
+function selectProportionalHandler(pool: Pool): BoostedProportionalRemoveLiquidityV3Handler {
+ return selectRemoveLiquidityHandler(
+ pool,
+ RemoveLiquidityType.Proportional
+ ) as BoostedProportionalRemoveLiquidityV3Handler
+}
+
+const defaultBuildInput = { account: defaultTestUserAccount, slippagePercent: '0.2' }
+
+await connectWithDefaultUser()
+
+// TODO: unskip this test when sepolia V3 pools are available in production api
+describe.skip('When proportionally removing liquidity for a BOOSTED v3 pool', async () => {
+ // Sepolia
+ // const poolId = '0x6dbdd7a36d900083a5b86a55583d90021e9f33e8' // Sepolia stataEthUSDC stataEthUSDT
+
+ const usdcAaveAddress = '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8' // Sepolia underlying usdcAave faucet address (temporary until we have the real one)
+ const usdtAaveAddress = '0xaa8e23fb1079ea71e0a56f48a2aa51851d8433d0' // Sepolia underlying usdcAave faucet address (temporary until we have the real one)
+
+ // const v3Pool = await getPoolMock(poolId, GqlChain.Sepolia)
+ const v3Pool = {} as unknown as Pool
+
+ const defaultQueryInput: QueryRemoveLiquidityInput = {
+ humanBptIn: '0.01',
+ tokenOut: emptyAddress, // We don't use in this scenario it but it is required to simplify TS interfaces
+ userAddress: defaultTestUserAccount,
+ }
+
+ test('queries amounts out', async () => {
+ const handler = selectProportionalHandler(v3Pool)
+
+ const result = await handler.simulate(defaultQueryInput)
+
+ expect(result.sdkQueryOutput.to).toBe(sepoliaCompositeRouter)
+
+ const [aUsdcTokenAmountOut, aUsdtTokenAmountOut] = result.amountsOut.sort()
+
+ expect(aUsdcTokenAmountOut.token.address).toBe(usdcAaveAddress)
+ expect(aUsdcTokenAmountOut.amount).toBeGreaterThan(0n)
+
+ expect(aUsdtTokenAmountOut.token.address).toBe(usdtAaveAddress)
+ expect(aUsdtTokenAmountOut.amount).toBeGreaterThan(0n)
+ })
+
+ test('builds Tx Config', async () => {
+ const handler = selectProportionalHandler(v3Pool)
+
+ const queryOutput = await handler.simulate(defaultQueryInput)
+
+ const result = await handler.buildCallData({
+ ...defaultBuildInput,
+ queryOutput,
+ })
+
+ expect(result.to).toBe(sepoliaCompositeRouter)
+ expect(result.data).toBeDefined()
+ })
+})
diff --git a/packages/lib/modules/pool/actions/remove-liquidity/handlers/BoostedProportionalRemoveLiquidityV3.handler.ts b/packages/lib/modules/pool/actions/remove-liquidity/handlers/BoostedProportionalRemoveLiquidityV3.handler.ts
new file mode 100644
index 000000000..9497db970
--- /dev/null
+++ b/packages/lib/modules/pool/actions/remove-liquidity/handlers/BoostedProportionalRemoveLiquidityV3.handler.ts
@@ -0,0 +1,59 @@
+import {
+ RemoveLiquidityBoostedBuildCallInput,
+ RemoveLiquidityBoostedV3,
+ RemoveLiquidityKind,
+ Slippage,
+} from '@balancer/sdk'
+import { TransactionConfig } from '@repo/lib/modules/web3/contracts/contract.types'
+import {
+ QueryRemoveLiquidityInput,
+ SdkBuildRemoveLiquidityInput,
+ SdkQueryRemoveLiquidityOutput,
+} from '../remove-liquidity.types'
+import { BaseProportionalRemoveLiquidityHandler } from './BaseProportionalRemoveLiquidity.handler'
+
+export class BoostedProportionalRemoveLiquidityV3Handler extends BaseProportionalRemoveLiquidityHandler {
+ public async simulate({
+ humanBptIn: bptIn,
+ userAddress,
+ }: QueryRemoveLiquidityInput): Promise {
+ const removeLiquidity = new RemoveLiquidityBoostedV3()
+ const removeLiquidityInput = this.constructSdkInput(bptIn, userAddress)
+
+ const sdkQueryOutput = await removeLiquidity.query(
+ removeLiquidityInput,
+ this.helpers.boostedPoolState
+ )
+
+ return { amountsOut: sdkQueryOutput.amountsOut.filter(a => a.amount > 0n), sdkQueryOutput }
+ }
+
+ public async buildCallData({
+ account,
+ slippagePercent,
+ queryOutput,
+ permit,
+ }: SdkBuildRemoveLiquidityInput): Promise {
+ const removeLiquidity = new RemoveLiquidityBoostedV3()
+
+ const v3BuildCallParams: RemoveLiquidityBoostedBuildCallInput = {
+ ...queryOutput.sdkQueryOutput,
+ slippage: Slippage.fromPercentage(`${Number(slippagePercent)}`),
+ protocolVersion: 3,
+ userData: '0x',
+ removeLiquidityKind: RemoveLiquidityKind.Proportional,
+ }
+
+ const { callData, to, value } = permit
+ ? removeLiquidity.buildCallWithPermit(v3BuildCallParams, permit)
+ : removeLiquidity.buildCall(v3BuildCallParams)
+
+ return {
+ account,
+ chainId: this.helpers.chainId,
+ data: callData,
+ to,
+ value,
+ }
+ }
+}
diff --git a/packages/lib/modules/pool/actions/remove-liquidity/handlers/NestedProportionalRemoveLiquidity.handler.ts b/packages/lib/modules/pool/actions/remove-liquidity/handlers/NestedProportionalRemoveLiquidity.handler.ts
index 116df6938..35a5433a5 100644
--- a/packages/lib/modules/pool/actions/remove-liquidity/handlers/NestedProportionalRemoveLiquidity.handler.ts
+++ b/packages/lib/modules/pool/actions/remove-liquidity/handlers/NestedProportionalRemoveLiquidity.handler.ts
@@ -42,7 +42,7 @@ export class NestedProportionalRemoveLiquidityHandler implements RemoveLiquidity
const sdkQueryOutput = await removeLiquidity.query(
removeLiquidityInput,
- this.helpers.nestedPoolState
+ this.helpers.nestedPoolStateV2
)
return { amountsOut: sdkQueryOutput.amountsOut, sdkQueryOutput }
diff --git a/packages/lib/modules/pool/actions/remove-liquidity/handlers/NestedSingleTokenRemoveLiquidity.handler.ts b/packages/lib/modules/pool/actions/remove-liquidity/handlers/NestedSingleTokenRemoveLiquidity.handler.ts
index 33062e008..1085223b2 100644
--- a/packages/lib/modules/pool/actions/remove-liquidity/handlers/NestedSingleTokenRemoveLiquidity.handler.ts
+++ b/packages/lib/modules/pool/actions/remove-liquidity/handlers/NestedSingleTokenRemoveLiquidity.handler.ts
@@ -45,7 +45,7 @@ export class NestedSingleTokenRemoveLiquidityHandler implements RemoveLiquidityH
const sdkQueryOutput = await removeLiquidity.query(
removeLiquidityInput,
- this.helpers.nestedPoolState
+ this.helpers.nestedPoolStateV2
)
return { amountsOut: sdkQueryOutput.amountsOut, sdkQueryOutput }
@@ -59,7 +59,7 @@ export class NestedSingleTokenRemoveLiquidityHandler implements RemoveLiquidityH
const priceImpactABA: PriceImpactAmount = await PriceImpact.removeLiquidityNested(
removeLiquidityInput,
- this.helpers.nestedPoolState
+ this.helpers.nestedPoolStateV2
)
return priceImpactABA.decimal
diff --git a/packages/lib/modules/pool/actions/remove-liquidity/handlers/ProportionalRemoveLiquidity.handler.integration.spec.ts b/packages/lib/modules/pool/actions/remove-liquidity/handlers/ProportionalRemoveLiquidity.handler.integration.spec.ts
index 91b4f235c..895341210 100644
--- a/packages/lib/modules/pool/actions/remove-liquidity/handlers/ProportionalRemoveLiquidity.handler.integration.spec.ts
+++ b/packages/lib/modules/pool/actions/remove-liquidity/handlers/ProportionalRemoveLiquidity.handler.integration.spec.ts
@@ -1,16 +1,16 @@
import networkConfig from '@repo/lib/config/networks/mainnet'
-import { balAddress, sepoliaRouter, wETHAddress } from '@repo/lib/debug-helpers'
+import { balAddress, wETHAddress } from '@repo/lib/debug-helpers'
+import { emptyAddress } from '@repo/lib/modules/web3/contracts/wagmi-helpers'
+import { defaultTestUserAccount } from '@repo/lib/test/anvil/anvil-setup'
import {
aBalWethPoolElementMock,
aPhantomStablePoolMock,
} from '@repo/lib/test/msw/builders/gqlPoolElement.builders'
-import { defaultTestUserAccount } from '@repo/lib/test/anvil/anvil-setup'
+import { connectWithDefaultUser } from '@repo/lib/test/utils/wagmi/wagmi-connections'
import { Pool } from '../../../PoolProvider'
import { QueryRemoveLiquidityInput, RemoveLiquidityType } from '../remove-liquidity.types'
-import { selectRemoveLiquidityHandler } from './selectRemoveLiquidityHandler'
import { ProportionalRemoveLiquidityHandler } from './ProportionalRemoveLiquidity.handler'
-import { emptyAddress } from '@repo/lib/modules/web3/contracts/wagmi-helpers'
-import { connectWithDefaultUser } from '@repo/lib/test/utils/wagmi/wagmi-connections'
+import { selectRemoveLiquidityHandler } from './selectRemoveLiquidityHandler'
// import { GqlChain } from '@repo/lib/shared/services/api/generated/graphql'
// import { getPoolMock } from '../../../__mocks__/getPoolMock'
@@ -82,55 +82,3 @@ describe('When removing liquidity from a V2 stable pool', () => {
expect(result.account).toBe(defaultTestUserAccount)
})
})
-
-// TODO: unskip this test when sepolia V3 pools are available in production api
-describe.skip('When proportionally removing liquidity for a weighted v3 pool', async () => {
- // Sepolia
- const balAddress = '0xb19382073c7a0addbb56ac6af1808fa49e377b75'
- const wethAddress = '0x7b79995e5f793a07bc00c21412e50ecae098e7f9'
- // const poolId = '0xec1b5ca86c83c7a85392063399e7d2170d502e00' // Sepolia B-50BAL-50WETH
- // const v3Pool = await getPoolMock(poolId, GqlChain.Sepolia)
-
- const v3Pool = {} as unknown as Pool
-
- const defaultQueryInput: QueryRemoveLiquidityInput = {
- humanBptIn: '0.01',
- tokenOut: emptyAddress, // We don't use in this scenario it but it is required to simplify TS interfaces
- userAddress: defaultTestUserAccount,
- }
-
- test('returns ZERO price impact', async () => {
- const handler = selectProportionalHandler(v3Pool)
-
- const priceImpact = await handler.getPriceImpact()
-
- expect(priceImpact).toBe(0)
- })
- test('queries amounts out', async () => {
- const handler = selectProportionalHandler(v3Pool)
-
- const result = await handler.simulate(defaultQueryInput)
-
- const [wEthTokenAmountOut, balTokenAmountOut] = result.amountsOut
-
- expect(balTokenAmountOut.token.address).toBe(balAddress)
- expect(balTokenAmountOut.amount).toBeGreaterThan(200000000000000n)
-
- expect(wEthTokenAmountOut.token.address).toBe(wethAddress)
- expect(wEthTokenAmountOut.amount).toBeGreaterThan(100000000000000n)
- })
-
- test('builds Tx Config', async () => {
- const handler = selectProportionalHandler(v3Pool)
-
- const queryOutput = await handler.simulate(defaultQueryInput)
-
- const result = await handler.buildCallData({
- ...defaultBuildInput,
- queryOutput,
- })
-
- expect(result.to).toBe(sepoliaRouter)
- expect(result.data).toBeDefined()
- })
-})
diff --git a/packages/lib/modules/pool/actions/remove-liquidity/handlers/ProportionalRemoveLiquidityV3.handler.integration.spec.ts b/packages/lib/modules/pool/actions/remove-liquidity/handlers/ProportionalRemoveLiquidityV3.handler.integration.spec.ts
new file mode 100644
index 000000000..1ae117a27
--- /dev/null
+++ b/packages/lib/modules/pool/actions/remove-liquidity/handlers/ProportionalRemoveLiquidityV3.handler.integration.spec.ts
@@ -0,0 +1,117 @@
+import { sepoliaRouter } from '@repo/lib/debug-helpers'
+import { emptyAddress } from '@repo/lib/modules/web3/contracts/wagmi-helpers'
+import { defaultTestUserAccount } from '@repo/lib/test/anvil/anvil-setup'
+import { connectWithDefaultUser } from '@repo/lib/test/utils/wagmi/wagmi-connections'
+import { Pool } from '../../../PoolProvider'
+import { QueryRemoveLiquidityInput, RemoveLiquidityType } from '../remove-liquidity.types'
+import { ProportionalRemoveLiquidityV3Handler } from './ProportionalRemoveLiquidityV3.handler'
+import { selectRemoveLiquidityHandler } from './selectRemoveLiquidityHandler'
+
+function selectProportionalHandler(pool: Pool): ProportionalRemoveLiquidityV3Handler {
+ return selectRemoveLiquidityHandler(
+ pool,
+ RemoveLiquidityType.Proportional
+ ) as ProportionalRemoveLiquidityV3Handler
+}
+
+const defaultBuildInput = { account: defaultTestUserAccount, slippagePercent: '0.2' }
+
+await connectWithDefaultUser()
+
+// TODO: unskip this test when sepolia V3 pools are available in production api
+describe.skip('When proportionally removing liquidity for a weighted v3 pool', async () => {
+ // Sepolia
+ const balAddress = '0xb19382073c7a0addbb56ac6af1808fa49e377b75'
+ const wethAddress = '0x7b79995e5f793a07bc00c21412e50ecae098e7f9'
+ // const poolId = '0xec1b5ca86c83c7a85392063399e7d2170d502e00' // Sepolia B-50BAL-50WETH
+ // const v3Pool = await getPoolMock(poolId, GqlChain.Sepolia)
+
+ const v3Pool = {} as unknown as Pool
+
+ const defaultQueryInput: QueryRemoveLiquidityInput = {
+ humanBptIn: '0.01',
+ tokenOut: emptyAddress, // We don't use in this scenario it but it is required to simplify TS interfaces
+ userAddress: defaultTestUserAccount,
+ }
+
+ test('returns ZERO price impact', async () => {
+ const handler = selectProportionalHandler(v3Pool)
+
+ const priceImpact = await handler.getPriceImpact()
+
+ expect(priceImpact).toBe(0)
+ })
+ test('queries amounts out', async () => {
+ const handler = selectProportionalHandler(v3Pool)
+
+ const result = await handler.simulate(defaultQueryInput)
+
+ const [wEthTokenAmountOut, balTokenAmountOut] = result.amountsOut
+
+ expect(balTokenAmountOut.token.address).toBe(balAddress)
+ expect(balTokenAmountOut.amount).toBeGreaterThan(200000000000000n)
+
+ expect(wEthTokenAmountOut.token.address).toBe(wethAddress)
+ expect(wEthTokenAmountOut.amount).toBeGreaterThan(100000000000000n)
+ })
+
+ test('builds Tx Config', async () => {
+ const handler = selectProportionalHandler(v3Pool)
+
+ const queryOutput = await handler.simulate(defaultQueryInput)
+
+ const result = await handler.buildCallData({
+ ...defaultBuildInput,
+ queryOutput,
+ })
+
+ expect(result.to).toBe(sepoliaRouter)
+ expect(result.data).toBeDefined()
+ })
+})
+
+// TODO: unskip this test when sepolia V3 pools are available in production api
+describe.skip(`When proportionally removing liquidity using NON BOOSTED remove for a BOOSTED v3 pool
+ (if the user wants to get wrapping tokens instead of underlying ones)`, async () => {
+ // Sepolia
+ // const poolId = '0x6dbdd7a36d900083a5b86a55583d90021e9f33e8' // Sepolia stataEthUSDC stataEthUSDT
+
+ const stataEthUSDC = '0x8a88124522dbbf1e56352ba3de1d9f78c143751e'
+ const stataEthUSDT = '0x978206fae13faf5a8d293fb614326b237684b750'
+ // const v3Pool = await getPoolMock(poolId, GqlChain.Sepolia)
+ const v3Pool = {} as unknown as Pool
+
+ const defaultQueryInput: QueryRemoveLiquidityInput = {
+ humanBptIn: '0.01',
+ tokenOut: emptyAddress, // We don't use in this scenario it but it is required to simplify TS interfaces
+ userAddress: defaultTestUserAccount,
+ }
+
+ test('queries amounts out', async () => {
+ const handler = selectProportionalHandler(v3Pool)
+
+ const result = await handler.simulate(defaultQueryInput)
+
+ const [aUsdcTokenAmountOut, aUsdtTokenAmountOut] = result.amountsOut
+
+ expect(aUsdtTokenAmountOut.token.address).toBe(stataEthUSDT)
+ expect(aUsdtTokenAmountOut.amount).toBeGreaterThan(0n)
+
+ expect(aUsdcTokenAmountOut.token.address).toBe(stataEthUSDC)
+ expect(aUsdcTokenAmountOut.amount).toBeGreaterThan(0n)
+ })
+
+ test('builds Tx Config', async () => {
+ const handler = selectProportionalHandler(v3Pool)
+
+ const queryOutput = await handler.simulate(defaultQueryInput)
+
+ const result = await handler.buildCallData({
+ ...defaultBuildInput,
+ queryOutput,
+ })
+
+ expect(result.to).toBe(sepoliaRouter)
+ expect(result.data).toBeDefined()
+ })
+})
diff --git a/packages/lib/modules/pool/actions/remove-liquidity/handlers/selectRemoveLiquidityHandler.ts b/packages/lib/modules/pool/actions/remove-liquidity/handlers/selectRemoveLiquidityHandler.ts
index 37ef7e95b..a6e6d652d 100644
--- a/packages/lib/modules/pool/actions/remove-liquidity/handlers/selectRemoveLiquidityHandler.ts
+++ b/packages/lib/modules/pool/actions/remove-liquidity/handlers/selectRemoveLiquidityHandler.ts
@@ -1,10 +1,11 @@
import { Pool } from '../../../PoolProvider'
-import { isV3Pool } from '../../../pool.helpers'
+import { isBoosted, isV3Pool } from '../../../pool.helpers'
import {
shouldUseRecoveryRemoveLiquidity,
supportsNestedActions,
} from '../../LiquidityActionHelpers'
import { RemoveLiquidityType } from '../remove-liquidity.types'
+import { BoostedProportionalRemoveLiquidityV3Handler } from './BoostedProportionalRemoveLiquidityV3.handler'
import { NestedProportionalRemoveLiquidityHandler } from './NestedProportionalRemoveLiquidity.handler'
import { NestedSingleTokenRemoveLiquidityHandler } from './NestedSingleTokenRemoveLiquidity.handler'
import { ProportionalRemoveLiquidityHandler } from './ProportionalRemoveLiquidity.handler'
@@ -29,6 +30,10 @@ export function selectRemoveLiquidityHandler(
return new RecoveryRemoveLiquidityHandler(pool)
}
+ if (isV3Pool(pool) && isBoosted(pool)) {
+ return new BoostedProportionalRemoveLiquidityV3Handler(pool)
+ }
+
if (supportsNestedActions(pool) && kind === RemoveLiquidityType.Proportional) {
return new NestedProportionalRemoveLiquidityHandler(pool)
}
diff --git a/packages/lib/modules/pool/pool.helpers.ts b/packages/lib/modules/pool/pool.helpers.ts
index aa1fe222f..43e12f8fa 100644
--- a/packages/lib/modules/pool/pool.helpers.ts
+++ b/packages/lib/modules/pool/pool.helpers.ts
@@ -4,6 +4,7 @@ import { getBlockExplorerAddressUrl } from '@repo/lib/shared/hooks/useBlockExplo
import {
GetPoolQuery,
GqlChain,
+ GqlNestedPool,
GqlPoolBase,
GqlPoolNestingType,
GqlPoolStakingGauge,
@@ -354,6 +355,10 @@ export function getPoolActionableTokens(pool: Pool, getToken: GetTokenFn): GqlTo
.filter((token): token is GqlToken => token !== undefined)
}
+ if (isBoosted(pool)) {
+ return getBoostedGqlTokens(pool, getToken)
+ }
+
// TODO add exception for composable pools where we can allow adding
// liquidity with nested tokens
if (supportsNestedActions(pool)) {
@@ -435,21 +440,35 @@ export function isPoolSwapAllowed(pool: Pool, token1: Address, token2: Address):
}
/*
- Returns all the top level tokens + children nested tokens + ERC4626 underlying tokens.
- That is, the tokens that we can use in the pool's actions (add/remove/swap)
+ Returns all the tokens in the structure of the given pool:
+ top level tokens + children nested tokens + ERC4626 underlying tokens.
*/
export function allPoolTokens(pool: Pool | GqlPoolBase): TokenCore[] {
- const underlyingTokens: TokenCore[] = pool.poolTokens.flatMap((token, index) =>
- token.isErc4626 ? ({ ...token.underlyingToken, index } as TokenCore) : []
- )
+ const extractUnderlyingTokens = (token: PoolToken): TokenCore[] => {
+ if (token.isErc4626 && token.underlyingToken) {
+ return [{ ...token.underlyingToken, index: token.index } as TokenCore]
+ }
+ return []
+ }
+
+ const extractNestedUnderlyingTokens = (nestedPool?: GqlNestedPool): TokenCore[] => {
+ if (!nestedPool) return []
+ return nestedPool.tokens.flatMap((nestedToken, index) =>
+ nestedToken.isErc4626 && nestedToken.underlyingToken
+ ? ([nestedToken, { ...nestedToken.underlyingToken, index }] as TokenCore[]) // Is index is not relevant in this case?
+ : [nestedToken as TokenCore]
+ )
+ }
+
+ const underlyingTokens: TokenCore[] = pool.poolTokens.flatMap(extractUnderlyingTokens)
const nestedParentTokens: PoolToken[] = pool.poolTokens.flatMap(token =>
token.nestedPool ? token : []
)
- const nestedChildrenTokens: PoolToken[] = pool.poolTokens
- .flatMap(token => (token.nestedPool ? token.nestedPool.tokens : []))
- .filter((token): token is PoolToken => token !== undefined)
+ const nestedChildrenTokens: TokenCore[] = pool.poolTokens.flatMap(token =>
+ token.nestedPool ? extractNestedUnderlyingTokens(token.nestedPool as GqlNestedPool) : []
+ )
const standardTopLevelTokens: PoolToken[] = pool.poolTokens.flatMap(token =>
!token.hasNestedPool && !token.isErc4626 ? token : []
@@ -457,7 +476,7 @@ export function allPoolTokens(pool: Pool | GqlPoolBase): TokenCore[] {
const allTokens = underlyingTokens.concat(
toTokenCores(nestedParentTokens),
- toTokenCores(nestedChildrenTokens),
+ nestedChildrenTokens,
toTokenCores(standardTopLevelTokens)
)
@@ -465,6 +484,18 @@ export function allPoolTokens(pool: Pool | GqlPoolBase): TokenCore[] {
return uniqBy(allTokens, 'address')
}
+// Returns top level standard tokens + Erc4626 underlying tokens
+export function getBoostedGqlTokens(pool: Pool, getToken: GetTokenFn): GqlToken[] {
+ const underlyingTokens = pool.poolTokens
+ .flatMap(token =>
+ token.isErc4626
+ ? [getToken(token?.underlyingToken?.address as Address, pool.chain)]
+ : toGqlTokens([token], getToken, pool.chain)
+ )
+ .filter((token): token is GqlToken => token !== undefined)
+ return underlyingTokens
+}
+
function toTokenCores(poolTokens: PoolToken[]): TokenCore[] {
return poolTokens.map(
t =>
diff --git a/packages/lib/modules/tokens/approvals/permit/signRemoveLiquidityPermit.tsx b/packages/lib/modules/tokens/approvals/permit/signRemoveLiquidityPermit.tsx
index fd8497712..37f59f1e2 100644
--- a/packages/lib/modules/tokens/approvals/permit/signRemoveLiquidityPermit.tsx
+++ b/packages/lib/modules/tokens/approvals/permit/signRemoveLiquidityPermit.tsx
@@ -8,6 +8,8 @@ import {
PublicWalletClient,
RemoveLiquidityQueryOutput,
} from '@balancer/sdk'
+import { isBoosted } from '@repo/lib/modules/pool/pool.helpers'
+import { Pool } from '@repo/lib/modules/pool/PoolProvider'
export interface PermitRemoveLiquidityInput {
account: Address
@@ -19,16 +21,18 @@ type Params = {
sdkClient?: PublicWalletClient
permitInput: PermitRemoveLiquidityInput
wethIsEth: boolean
+ pool: Pool
}
export async function signRemoveLiquidityPermit({
wethIsEth,
sdkClient,
permitInput,
+ pool,
}: Params): Promise {
if (!sdkClient) return undefined
try {
- const signature = await signPermit({ permitInput, wethIsEth, sdkClient })
+ const signature = await signPermit({ permitInput, wethIsEth, sdkClient, pool })
return signature
} catch (e: unknown) {
const error = ensureError(e)
@@ -39,13 +43,18 @@ export async function signRemoveLiquidityPermit({
}
}
-async function signPermit({ permitInput, wethIsEth, sdkClient }: Params): Promise {
+async function signPermit({ permitInput, wethIsEth, sdkClient, pool }: Params): Promise {
const baseInput = constructRemoveBaseBuildCallInput({
wethIsEth,
slippagePercent: permitInput.slippagePercent,
sdkQueryOutput: permitInput.sdkQueryOutput as RemoveLiquidityQueryOutput,
})
- const signature = await PermitHelper.signRemoveLiquidityApproval({
+
+ const signPermitFn = isBoosted(pool)
+ ? PermitHelper.signRemoveLiquidityBoostedApproval
+ : PermitHelper.signRemoveLiquidityApproval
+
+ const signature = await signPermitFn({
...baseInput,
/* eslint-disable @typescript-eslint/no-non-null-assertion */
client: sdkClient!,
diff --git a/packages/lib/modules/tokens/approvals/permit/useSignPermit.tsx b/packages/lib/modules/tokens/approvals/permit/useSignPermit.tsx
index 33bac55ce..c0302928a 100644
--- a/packages/lib/modules/tokens/approvals/permit/useSignPermit.tsx
+++ b/packages/lib/modules/tokens/approvals/permit/useSignPermit.tsx
@@ -57,6 +57,7 @@ export function useSignPermit({
sdkQueryOutput: queryOutput.sdkQueryOutput,
},
wethIsEth,
+ pool,
})
if (signature) {
diff --git a/packages/lib/modules/tokens/approvals/permit2/signPermit2Add.tsx b/packages/lib/modules/tokens/approvals/permit2/signPermit2Add.tsx
index 65fd229d9..4f1918c35 100644
--- a/packages/lib/modules/tokens/approvals/permit2/signPermit2Add.tsx
+++ b/packages/lib/modules/tokens/approvals/permit2/signPermit2Add.tsx
@@ -14,6 +14,7 @@ import { NoncesByTokenAddress } from './usePermit2Allowance'
import { constructBaseBuildCallInput } from '@repo/lib/modules/pool/actions/add-liquidity/handlers/add-liquidity.utils'
import { GqlChain } from '@repo/lib/shared/services/api/generated/graphql'
import { isWrappedNativeAsset } from '../../token.helpers'
+import { isBoosted } from '@repo/lib/modules/pool/pool.helpers'
type SignPermit2AddParams = {
sdkClient?: PublicWalletClient
@@ -27,6 +28,7 @@ type SignPermit2AddParams = {
}
export async function signPermit2Add(params: SignPermit2AddParams): Promise {
if (!params.nonces) throw new Error('Missing nonces in signPermitAdd')
+
try {
const signature = await sign(params)
return signature
@@ -66,7 +68,11 @@ async function sign({
amountsIn: sdkQueryOutput.amountsIn,
})
- const signature = await Permit2Helper.signAddLiquidityApproval({
+ const signFn = isBoosted(pool)
+ ? Permit2Helper.signAddLiquidityBoostedApproval
+ : Permit2Helper.signAddLiquidityApproval
+
+ const signature = await signFn({
...baseInput,
client: sdkClient,
owner: account,
diff --git a/packages/lib/modules/tokens/approvals/permit2/signPermit2Swap.tsx b/packages/lib/modules/tokens/approvals/permit2/signPermit2Swap.tsx
index 2427e80c7..88acb911b 100644
--- a/packages/lib/modules/tokens/approvals/permit2/signPermit2Swap.tsx
+++ b/packages/lib/modules/tokens/approvals/permit2/signPermit2Swap.tsx
@@ -54,10 +54,12 @@ async function sign({
const MaxAllowance = MaxUint159
const maximizedQueryOutput = { ...queryOutput }
- if (maximizedQueryOutput.swapKind === SwapKind.GivenIn)
- {maximizedQueryOutput.amountIn.amount = MaxAllowance}
- if (maximizedQueryOutput.swapKind === SwapKind.GivenOut)
- {maximizedQueryOutput.amountOut.amount = MaxAllowance}
+ if (maximizedQueryOutput.swapKind === SwapKind.GivenIn) {
+ maximizedQueryOutput.amountIn.amount = MaxAllowance
+ }
+ if (maximizedQueryOutput.swapKind === SwapKind.GivenOut) {
+ maximizedQueryOutput.amountOut.amount = MaxAllowance
+ }
const signature = await Permit2Helper.signSwapApproval({
client: sdkClient,
diff --git a/packages/lib/shared/services/api/pool.graphql b/packages/lib/shared/services/api/pool.graphql
index c175cc189..6b1ea97f0 100644
--- a/packages/lib/shared/services/api/pool.graphql
+++ b/packages/lib/shared/services/api/pool.graphql
@@ -228,6 +228,13 @@ fragment PoolTokens on GqlPoolTokenDetail {
index
address
decimals
+ isErc4626
+ underlyingToken {
+ address
+ decimals
+ name
+ symbol
+ }
}
}
isErc4626