From a5d46aa67c3aa8b3e0e4ebe6af4b8d4c99d41394 Mon Sep 17 00:00:00 2001 From: mkflow27 Date: Wed, 21 Aug 2024 09:15:03 +0200 Subject: [PATCH 1/2] docs: update test instruction --- modules/sor/balancer-sor.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/sor/balancer-sor.test.ts b/modules/sor/balancer-sor.test.ts index 7901eea68..2f0406d7d 100644 --- a/modules/sor/balancer-sor.test.ts +++ b/modules/sor/balancer-sor.test.ts @@ -4,7 +4,7 @@ import { poolService } from '../pool/pool.service'; import { GqlSorSwapType } from '../../schema'; -// npx jest --testPathPattern=modules/beethoven/balancer-sor.test.ts +// npx jest --testPathPattern=modules/sor/balancer-sor.test.ts describe('SmartOrderRouter', () => { test('swap with mixed decimals', async () => { const tokens = await tokenService.getTokens(); From be1cf510f5ede0c545c48b40a58a96801a691578 Mon Sep 17 00:00:00 2001 From: mkflow27 Date: Wed, 4 Sep 2024 11:37:04 +0200 Subject: [PATCH 2/2] feat: add hooks to sor (tests only) --- modules/sor/balancer-sor.integration.test.ts | 132 ++++++++++++++++++ modules/sor/sorV2/lib/poolsV2/basePool.ts | 3 +- .../lib/poolsV3/stable/stablePool.test.ts | 101 +++++++++++--- .../sorV2/lib/poolsV3/stable/stablePool.ts | 44 +++++- .../lib/poolsV3/weighted/weightedPool.test.ts | 98 +++++++++++-- .../lib/poolsV3/weighted/weightedPool.ts | 45 +++++- modules/sor/sorV2/lib/static.ts | 7 +- modules/sor/sorV2/sorPathService.ts | 6 + prisma/prisma-types.ts | 8 ++ test/factories/index.ts | 2 + test/factories/prismaHook.factory.ts | 30 ++++ .../prismaHookDynamicData.factory.ts | 12 ++ 12 files changed, 446 insertions(+), 42 deletions(-) create mode 100644 test/factories/prismaHook.factory.ts create mode 100644 test/factories/prismaHookDynamicData.factory.ts diff --git a/modules/sor/balancer-sor.integration.test.ts b/modules/sor/balancer-sor.integration.test.ts index aa383be13..c158a7c2d 100644 --- a/modules/sor/balancer-sor.integration.test.ts +++ b/modules/sor/balancer-sor.integration.test.ts @@ -13,6 +13,8 @@ import { prismaPoolFactory, prismaPoolTokenDynamicDataFactory, prismaPoolTokenFactory, + hookDataFactory, + hookFactory } from '../../test/factories'; import { createTestClient, formatEther, Hex, http, parseEther, TestClient } from 'viem'; import { sepolia } from 'viem/chains'; @@ -102,6 +104,7 @@ describe('Balancer SOR Integration Tests', () => { amountIn, [prismaWeightedPool], protocolVersion, + [], )) as PathWithAmount[]; // build SDK swap from SOR paths @@ -177,6 +180,7 @@ describe('Balancer SOR Integration Tests', () => { amountIn, [prismaStablePool], protocolVersion, + [], )) as PathWithAmount[]; const swapPaths: Path[] = paths.map((path) => ({ @@ -287,6 +291,7 @@ describe('Balancer SOR Integration Tests', () => { amountIn, [nestedPool, weightedPool], protocolVersion, + [], )) as PathWithAmount[]; const swapPaths: Path[] = paths.map((path) => ({ @@ -338,6 +343,7 @@ describe('Balancer SOR Integration Tests', () => { amountIn, [nestedPool, weightedPool], protocolVersion, + [], )) as PathWithAmount[]; const swapPaths: Path[] = paths.map((path) => ({ @@ -416,6 +422,7 @@ describe('Balancer SOR Integration Tests', () => { amountIn, [prismaStablePool], protocolVersion, + [], )) as PathWithAmount[]; const swapPaths: Path[] = paths.map((path) => ({ @@ -448,6 +455,131 @@ describe('Balancer SOR Integration Tests', () => { }); }); + describe('Stable Pool Path with hooks', async () => { + + beforeAll(async() => { + // setup mock pool data + const poolAddress = '0x302b75a27e5e157f93c679dd7a25fdfcdbc1473c'; + const stataUSDC = prismaPoolTokenFactory.build({ + address: '0x8a88124522dbbf1e56352ba3de1d9f78c143751e', + token: { decimals: 6 }, + dynamicData: prismaPoolTokenDynamicDataFactory.build({ + balance: '500', + priceRate: '1.046992819427282715', + }), + }); + const stataDAI = prismaPoolTokenFactory.build({ + address: '0xde46e43f46ff74a23a65ebb0580cbe3dfe684a17', + token: { decimals: 18 }, + dynamicData: prismaPoolTokenDynamicDataFactory.build({ + balance: '500', + priceRate: '1.101882285912091736', + }), + }); + const prismaStablePool = prismaPoolFactory.stable('1000').build({ + address: poolAddress, + tokens: [stataUSDC, stataDAI], + dynamicData: prismaPoolDynamicDataFactory.build({ + totalShares: '1054.451151293881721519', + swapFee: '0.01', + }), + }); + + + // mock api data for hooks. + const dynamicData = hookDataFactory.build({ + // Add any specific dynamic data parameters here + addLiquidityFeePercentage: '0.01', + removeLiquidityFeePercentage: '0.01', + swapFeePercentage: '0.01' + }); + + + // Create the Hook instance + const prismaHook1 = hookFactory.build({ + dynamicData: dynamicData, + enableHookAdjustedAmounts: true, + poolsIds: [poolAddress, '0x102b75a27e5e157f93c679dd7a25fdfcdbc1473c'], + shouldCallAfterAddLiquidity: true, + shouldCallAfterInitialize: true, + shouldCallAfterRemoveLiquidity: true, + shouldCallAfterSwap: true, + shouldCallBeforeAddLiquidity: true, + shouldCallBeforeInitialize: true, + shouldCallBeforeRemoveLiquidity: true, + shouldCallBeforeSwap: true, + shouldCallComputeDynamicSwapFee: true, + }); + + // Create the Hook instance + const prismaHook2 = hookFactory.build({ + dynamicData: dynamicData, + enableHookAdjustedAmounts: true, + poolsIds: ['0x102b75a27e5e157f93c679dd6a25fdfcdbc1473f', '0x102b75a17e5e157f93c679dd7a25fdfcdbc1473c'], + shouldCallAfterAddLiquidity: true, + shouldCallAfterInitialize: true, + shouldCallAfterRemoveLiquidity: true, + shouldCallAfterSwap: true, + shouldCallBeforeAddLiquidity: true, + shouldCallBeforeInitialize: true, + shouldCallBeforeRemoveLiquidity: true, + shouldCallBeforeSwap: true, + shouldCallComputeDynamicSwapFee: true, + }); + + + + + // get SOR paths + const tIn = new Token( + parseFloat(chainToIdMap[stataUSDC.token.chain]), + stataUSDC.address as Address, + stataUSDC.token.decimals, + ); + const tOut = new Token( + parseFloat(chainToIdMap[stataDAI.token.chain]), + stataDAI.address as Address, + stataDAI.token.decimals, + ); + const amountIn = BigInt(1000e6); + paths = (await sorGetPathsWithPools( + tIn, + tOut, + SwapKind.GivenIn, + amountIn, + [prismaStablePool], + protocolVersion, + [prismaHook1, prismaHook2], + )) as PathWithAmount[]; + + const swapPaths: Path[] = paths.map((path) => ({ + protocolVersion, + inputAmountRaw: path.inputAmount.amount, + outputAmountRaw: path.outputAmount.amount, + tokens: path.tokens.map((token) => ({ + address: token.address, + decimals: token.decimals, + })), + pools: path.pools.map((pool) => pool.id), + })); + + // build SDK swap from SOR paths + sdkSwap = new Swap({ + chainId: parseFloat(chainToIdMap['SEPOLIA']), + paths: swapPaths, + swapKind: SwapKind.GivenIn, + }); + }) + + test('SOR quote should match swap query', async () => { + const returnAmountSOR = getOutputAmount(paths); + const queryOutput = await sdkSwap.query(rpcUrl); + const returnAmountQuery = (queryOutput as ExactInQueryOutput).expectedAmountOut; + expect(returnAmountQuery.amount).toEqual(returnAmountSOR.amount); + }); + }); + + afterAll(async () => { await stopAnvilForks(); }); diff --git a/modules/sor/sorV2/lib/poolsV2/basePool.ts b/modules/sor/sorV2/lib/poolsV2/basePool.ts index c8ae23ace..24986bd08 100644 --- a/modules/sor/sorV2/lib/poolsV2/basePool.ts +++ b/modules/sor/sorV2/lib/poolsV2/basePool.ts @@ -1,4 +1,4 @@ -import { BufferState, PoolState } from '@balancer-labs/balancer-maths'; +import { BufferState, PoolState, HookState } from '@balancer-labs/balancer-maths'; import { PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; import { Hex } from 'viem'; import { BasePoolToken } from './basePoolToken'; @@ -23,4 +23,5 @@ export interface BasePool { export interface BasePoolV3 extends BasePool { tokens: (BasePoolToken | Erc4626PoolToken)[]; getPoolState(): PoolState | BufferState; + getHookState(): HookState | undefined; } diff --git a/modules/sor/sorV2/lib/poolsV3/stable/stablePool.test.ts b/modules/sor/sorV2/lib/poolsV3/stable/stablePool.test.ts index ac3137277..795c760c3 100644 --- a/modules/sor/sorV2/lib/poolsV3/stable/stablePool.test.ts +++ b/modules/sor/sorV2/lib/poolsV3/stable/stablePool.test.ts @@ -2,7 +2,7 @@ import { parseEther, parseUnits } from 'viem'; -import { PrismaPoolWithDynamic } from '../../../../../../prisma/prisma-types'; +import { PrismaPoolWithDynamic, PrismaHookWithDynamic } from '../../../../../../prisma/prisma-types'; import { WAD } from '../../utils/math'; import { StablePool } from './stablePool'; @@ -13,7 +13,10 @@ import { prismaPoolFactory, prismaPoolTokenDynamicDataFactory, prismaPoolTokenFactory, + hookFactory, + hookDataFactory, } from '../../../../../../test/factories'; +import { stable } from '../../../../../pool/pool-data'; describe('SOR V3 Stable Pool Tests', () => { let amp: string; @@ -27,7 +30,39 @@ describe('SOR V3 Stable Pool Tests', () => { let tokenRates: string[]; let totalShares: string; - beforeAll(() => { + test('Get Pool State', () => { + setupStablePool(false); + const poolState = { + poolType: 'Stable', + swapFee: parseEther(swapFee), + balancesLiveScaled18: tokenBalances.map((b) => parseEther(b)), + tokenRates: tokenRates.map((r) => parseEther(r)), + totalSupply: parseEther(totalShares), + amp: parseUnits(amp, 3), + tokens: tokenAddresses, + scalingFactors, + aggregateSwapFee: 0n + }; + expect(poolState).toEqual(stablePool.getPoolState()); + }); + + test('Get hook State hook attached', () => { + // true means that the stable pool has a hook attached in this test + setupStablePool(true); + const hookState = { + tokens: tokenAddresses, + removeLiquidityHookFeePercentage: BigInt(1e16) //'0.01' % + } + expect(hookState).toEqual(stablePool.getHookState()); + }) + + test('Get hook State no hook attached', () => { + // false means that the stable pool has no hook attached in this test + setupStablePool(false); + expect(stablePool.getHookState()).toBeUndefined(); + }) + + const setupStablePool = (hooks: boolean) => { swapFee = '0.01'; tokenBalances = ['169', '144']; tokenDecimals = [6, 18]; @@ -62,20 +97,52 @@ describe('SOR V3 Stable Pool Tests', () => { tokens: [poolToken1, poolToken2], dynamicData: prismaPoolDynamicDataFactory.build({ swapFee, totalShares }), }); - stablePool = StablePool.fromPrismaPool(stablePrismaPool); - }); + if (!hooks) { + stablePool = StablePool.fromPrismaPool(stablePrismaPool, []); + } else { - test('Get Pool State', () => { - const poolState = { - poolType: 'Stable', - swapFee: parseEther(swapFee), - balancesLiveScaled18: tokenBalances.map((b) => parseEther(b)), - tokenRates: tokenRates.map((r) => parseEther(r)), - totalSupply: parseEther(totalShares), - amp: parseUnits(amp, 3), - tokens: tokenAddresses, - scalingFactors, - }; - expect(poolState).toEqual(stablePool.getPoolState()); - }); + // create hooks here due to needing to pass stable pool address + // The stable pool has a hook attached in this test + const dynamicData = hookDataFactory.build({ + // Add any specific dynamic data parameters here + addLiquidityFeePercentage: '0.01', + removeLiquidityFeePercentage: '0.01', + swapFeePercentage: '0.01' + }); + + // Create the Hook instance + const prismaHook1 = hookFactory.build({ + dynamicData: dynamicData, + enableHookAdjustedAmounts: true, + poolsIds: [stablePrismaPool.address, '0x102b75a27e5e157f93c679dd7a25fdfcdbc1473c'], + shouldCallAfterAddLiquidity: true, + shouldCallAfterInitialize: true, + shouldCallAfterRemoveLiquidity: true, + shouldCallAfterSwap: true, + shouldCallBeforeAddLiquidity: true, + shouldCallBeforeInitialize: true, + shouldCallBeforeRemoveLiquidity: true, + shouldCallBeforeSwap: true, + shouldCallComputeDynamicSwapFee: true, + }); + + // Create the Hook instance + const prismaHook2 = hookFactory.build({ + dynamicData: dynamicData, + enableHookAdjustedAmounts: true, + poolsIds: ['0x102b75a27e5e157f93c679dd6a25fdfcdbc1473f', '0x102b75a17e5e157f93c679dd7a25fdfcdbc1473c'], + shouldCallAfterAddLiquidity: true, + shouldCallAfterInitialize: true, + shouldCallAfterRemoveLiquidity: true, + shouldCallAfterSwap: true, + shouldCallBeforeAddLiquidity: true, + shouldCallBeforeInitialize: true, + shouldCallBeforeRemoveLiquidity: true, + shouldCallBeforeSwap: true, + shouldCallComputeDynamicSwapFee: true, + }); + + stablePool = StablePool.fromPrismaPool(stablePrismaPool, [prismaHook1, prismaHook2]); + } + } }); diff --git a/modules/sor/sorV2/lib/poolsV3/stable/stablePool.ts b/modules/sor/sorV2/lib/poolsV3/stable/stablePool.ts index 644dcf221..f21fa0957 100644 --- a/modules/sor/sorV2/lib/poolsV3/stable/stablePool.ts +++ b/modules/sor/sorV2/lib/poolsV3/stable/stablePool.ts @@ -1,10 +1,10 @@ import { Address, Hex, parseEther, parseUnits } from 'viem'; import { MAX_UINT256, PoolType, SwapKind, Token, TokenAmount } from '@balancer/sdk'; -import { AddKind, RemoveKind, StableState, Vault } from '@balancer-labs/balancer-maths'; +import { AddKind, RemoveKind, StableState, Vault, HookState } from '@balancer-labs/balancer-maths'; import { Chain } from '@prisma/client'; -import { PrismaPoolWithDynamic } from '../../../../../../prisma/prisma-types'; +import { PrismaPoolWithDynamic, PrismaHookWithDynamic } from '../../../../../../prisma/prisma-types'; import { chainToIdMap } from '../../../../../network/network-config'; import { StableData } from '../../../../../pool/subgraph-mapper'; import { TokenPairData } from '../../../../../sources/contracts/fetch-tokenpair-data'; @@ -27,13 +27,14 @@ export class StablePool implements BasePoolV3 { public totalShares: bigint; public tokens: StablePoolToken[]; + public readonly hook: HookState | undefined; private readonly tokenMap: Map; private vault: Vault; private poolState: StableState; - static fromPrismaPool(pool: PrismaPoolWithDynamic): StablePool { + static fromPrismaPool(pool: PrismaPoolWithDynamic, hooks?: PrismaHookWithDynamic[]): StablePool { const poolTokens: StablePoolToken[] = []; if (!pool.dynamicData) throw new Error('Stable pool has no dynamic data'); @@ -75,6 +76,11 @@ export class StablePool implements BasePoolV3 { const totalShares = parseEther(pool.dynamicData.totalShares); const amp = parseUnits((pool.typeData as StableData).amp, 3); + // Get the hook for the pool + var hook = hooks?.find(hook => hook.poolsIds.includes(pool.id)); + // transform + hook = transformPrismaHookToHookState(hook); + return new StablePool( pool.id as Hex, pool.address, @@ -84,7 +90,22 @@ export class StablePool implements BasePoolV3 { poolTokens, totalShares, pool.dynamicData.tokenPairsData as TokenPairData[], + hook, ); + + function transformPrismaHookToHookState(prismaHook?: PrismaHookWithDynamic): HookState | undefined { + if (!prismaHook) { + return undefined; + } + // TODO: return the specific hook type state. Right now the HookState is an alias + const feePercentageString = prismaHook.dynamicData.removeLiquidityFeePercentage; + const feePercentageNumber = parseFloat(feePercentageString); + const feePercentageBigInt = BigInt(Math.round(feePercentageNumber * 10 ** 18)); + return { + tokens: poolTokens.map(token => token.token.address), + removeLiquidityHookFeePercentage: feePercentageBigInt + }; + } } constructor( @@ -96,6 +117,7 @@ export class StablePool implements BasePoolV3 { tokens: StablePoolToken[], totalShares: bigint, tokenPairs: TokenPairData[], + hook: PrismaHookWithDynamic | undefined = undefined, ) { this.chain = chain; this.id = id; @@ -107,6 +129,7 @@ export class StablePool implements BasePoolV3 { this.tokens = tokens.sort((a, b) => a.index - b.index); this.tokenMap = new Map(this.tokens.map((token) => [token.token.address, token])); this.tokenPairs = tokenPairs; + this.hook = hook; // add BPT to tokenMap, so we can handle add/remove liquidity operations const bpt = new Token(tokens[0].token.chainId, this.id, 18, 'BPT', 'BPT'); @@ -165,6 +188,7 @@ export class StablePool implements BasePoolV3 { kind: RemoveKind.SINGLE_TOKEN_EXACT_IN, }, this.poolState, + this.hook ); calculatedAmount = amountsOutRaw[tOut.index]; } else if (tOut.token.isSameAddress(this.id)) { @@ -177,6 +201,7 @@ export class StablePool implements BasePoolV3 { kind: AddKind.UNBALANCED, }, this.poolState, + this.hook ); calculatedAmount = bptAmountOutRaw; } else { @@ -189,6 +214,7 @@ export class StablePool implements BasePoolV3 { swapKind: SwapKind.GivenIn, }, this.poolState, + this.hook ); } return TokenAmount.fromRawAmount(tOut.token, calculatedAmount); @@ -267,6 +293,18 @@ export class StablePool implements BasePoolV3 { }; } + public getHookState(): HookState | undefined { + if (this.hook === undefined) { + return undefined; + } + + // returned hook state will depend on hook type eventually + return { + tokens: this.tokens.map((t) => t.token.address), + removeLiquidityHookFeePercentage: this.hook.removeLiquidityHookFeePercentage, + }; + } + public getPoolTokens(tokenIn: Token, tokenOut: Token): { tIn: StablePoolToken; tOut: StablePoolToken } { const tIn = this.tokenMap.get(tokenIn.wrapped); const tOut = this.tokenMap.get(tokenOut.wrapped); diff --git a/modules/sor/sorV2/lib/poolsV3/weighted/weightedPool.test.ts b/modules/sor/sorV2/lib/poolsV3/weighted/weightedPool.test.ts index db79f7e3c..497824011 100644 --- a/modules/sor/sorV2/lib/poolsV3/weighted/weightedPool.test.ts +++ b/modules/sor/sorV2/lib/poolsV3/weighted/weightedPool.test.ts @@ -13,6 +13,8 @@ import { prismaPoolFactory, prismaPoolTokenDynamicDataFactory, prismaPoolTokenFactory, + hookFactory, + hookDataFactory } from '../../../../../../test/factories'; describe('SOR V3 Weighted Pool Tests', () => { @@ -27,6 +29,40 @@ describe('SOR V3 Weighted Pool Tests', () => { let weightedPrismaPool: PrismaPoolWithDynamic; beforeAll(() => { + + }); + + test('Get Pool State', () => { + setupWeightedPool(false); + const poolState = { + poolType: 'Weighted', + swapFee: parseEther(swapFee), + balancesLiveScaled18: tokenBalances.map((b) => parseEther(b)), + tokenRates: Array(tokenBalances.length).fill(WAD), + totalSupply: parseEther(totalShares), + weights: tokenWeights.map((w) => parseEther(w)), + tokens: tokenAddresses, + scalingFactors, + aggregateSwapFee: 0n + }; + expect(poolState).toEqual(weightedPool.getPoolState()); + }); + test('get hook State hook attached', () => { + // true means that the weighted pool has a hook attached in this test + setupWeightedPool(true); + const hookState = { + tokens: tokenAddresses, + removeLiquidityHookFeePercentage: BigInt(1e16), //'0.01' % + } + expect(hookState).toEqual(weightedPool.getHookState()); + }); + test('get hook State no hook attached', () => { + // false means that the weighted pool has no hook attached in this test + setupWeightedPool(false); + expect(weightedPool.getHookState()).toBeUndefined(); + }); + + const setupWeightedPool = (hasHooks: boolean) => { swapFee = '0.01'; tokenBalances = ['169', '144']; tokenDecimals = [6, 18]; @@ -57,20 +93,52 @@ describe('SOR V3 Weighted Pool Tests', () => { tokens: [poolToken1, poolToken2], dynamicData: prismaPoolDynamicDataFactory.build({ swapFee, totalShares }), }); - weightedPool = WeightedPoolV3.fromPrismaPool(weightedPrismaPool); - }); + if (!hasHooks) { + weightedPool = WeightedPoolV3.fromPrismaPool(weightedPrismaPool, []); + } else { - test('Get Pool State', () => { - const poolState = { - poolType: 'Weighted', - swapFee: parseEther(swapFee), - balancesLiveScaled18: tokenBalances.map((b) => parseEther(b)), - tokenRates: Array(tokenBalances.length).fill(WAD), - totalSupply: parseEther(totalShares), - weights: tokenWeights.map((w) => parseEther(w)), - tokens: tokenAddresses, - scalingFactors, - }; - expect(poolState).toEqual(weightedPool.getPoolState()); - }); + // create hooks here due to needing to pass stable pool address + // The stable pool has a hook attached in this test + const dynamicData = hookDataFactory.build({ + // Add any specific dynamic data parameters here + addLiquidityFeePercentage: '0.01', + removeLiquidityFeePercentage: '0.01', + swapFeePercentage: '0.01' + }); + + // Create the Hook instance + const prismaHook1 = hookFactory.build({ + dynamicData: dynamicData, + enableHookAdjustedAmounts: true, + poolsIds: [weightedPrismaPool.address, '0x102b75a27e5e157f93c679dd7a25fdfcdbc1473c'], + shouldCallAfterAddLiquidity: true, + shouldCallAfterInitialize: true, + shouldCallAfterRemoveLiquidity: true, + shouldCallAfterSwap: true, + shouldCallBeforeAddLiquidity: true, + shouldCallBeforeInitialize: true, + shouldCallBeforeRemoveLiquidity: true, + shouldCallBeforeSwap: true, + shouldCallComputeDynamicSwapFee: true, + }); + + // Create the Hook instance + const prismaHook2 = hookFactory.build({ + dynamicData: dynamicData, + enableHookAdjustedAmounts: true, + poolsIds: ['0x102b75a27e5e157f93c679dd6a25fdfcdbc1473f', '0x102b75a17e5e157f93c679dd7a25fdfcdbc1473c'], + shouldCallAfterAddLiquidity: true, + shouldCallAfterInitialize: true, + shouldCallAfterRemoveLiquidity: true, + shouldCallAfterSwap: true, + shouldCallBeforeAddLiquidity: true, + shouldCallBeforeInitialize: true, + shouldCallBeforeRemoveLiquidity: true, + shouldCallBeforeSwap: true, + shouldCallComputeDynamicSwapFee: true, + }); + + weightedPool = WeightedPoolV3.fromPrismaPool(weightedPrismaPool, [prismaHook1, prismaHook2]); + } + } }); diff --git a/modules/sor/sorV2/lib/poolsV3/weighted/weightedPool.ts b/modules/sor/sorV2/lib/poolsV3/weighted/weightedPool.ts index 1d44c8515..72a9fcaea 100644 --- a/modules/sor/sorV2/lib/poolsV3/weighted/weightedPool.ts +++ b/modules/sor/sorV2/lib/poolsV3/weighted/weightedPool.ts @@ -1,9 +1,9 @@ import { Address, Hex, parseEther } from 'viem'; import { MAX_UINT256, SwapKind, Token, TokenAmount, WAD } from '@balancer/sdk'; -import { AddKind, RemoveKind, Vault, Weighted, WeightedState } from '@balancer-labs/balancer-maths'; +import { AddKind, RemoveKind, Vault, Weighted, WeightedState, HookState } from '@balancer-labs/balancer-maths'; import { Chain } from '@prisma/client'; -import { PrismaPoolWithDynamic } from '../../../../../../prisma/prisma-types'; +import { PrismaHookWithDynamic, PrismaPoolWithDynamic } from '../../../../../../prisma/prisma-types'; import { GqlPoolType } from '../../../../../../schema'; import { TokenPairData } from '../../../../../sources/contracts/fetch-tokenpair-data'; import { chainToIdMap } from '../../../../../network/network-config'; @@ -26,13 +26,14 @@ export class WeightedPoolV3 implements BasePoolV3 { public readonly tokenPairs: TokenPairData[]; public readonly MAX_IN_RATIO = 300000000000000000n; // 0.3 public readonly MAX_OUT_RATIO = 300000000000000000n; // 0.3 + public readonly hook: HookState | undefined; private readonly tokenMap: Map; private vault: Vault; private poolState: WeightedState; - static fromPrismaPool(pool: PrismaPoolWithDynamic): WeightedPoolV3 { + static fromPrismaPool(pool: PrismaPoolWithDynamic, hooks?: PrismaHookWithDynamic[]): WeightedPoolV3 { const poolTokens: WeightedPoolToken[] = []; if (!pool.dynamicData) { @@ -77,6 +78,12 @@ export class WeightedPoolV3 implements BasePoolV3 { } } + // write the logic to transform the PrismaHooks into a Hook object + var hook = hooks?.find(hook => hook.poolsIds.includes(pool.id)); + // find the first hook that matches the poolsIds of + hook = transformPrismaHookToHookState(hook); + + return new WeightedPoolV3( pool.id as Hex, pool.address, @@ -86,7 +93,22 @@ export class WeightedPoolV3 implements BasePoolV3 { parseEther(pool.dynamicData.totalShares), poolTokens, pool.dynamicData.tokenPairsData as TokenPairData[], + hook ); + + function transformPrismaHookToHookState(prismaHook?: PrismaHookWithDynamic): HookState | undefined { + if (!prismaHook) { + return undefined; + } + // TODO: return the specific hook type state. Right now the HookState is an alias + const feePercentageString = prismaHook.dynamicData.removeLiquidityFeePercentage; + const feePercentageNumber = parseFloat(feePercentageString); + const feePercentageBigInt = BigInt(Math.round(feePercentageNumber * 10 ** 18)); + return { + tokens: poolTokens.map(token => token.token.address), + removeLiquidityHookFeePercentage: feePercentageBigInt + }; + } } constructor( @@ -98,6 +120,7 @@ export class WeightedPoolV3 implements BasePoolV3 { totalShares: bigint, tokens: WeightedPoolToken[], tokenPairs: TokenPairData[], + hook: HookState | undefined = undefined, ) { this.chain = chain; this.id = id; @@ -108,6 +131,7 @@ export class WeightedPoolV3 implements BasePoolV3 { this.tokens = tokens; this.tokenMap = new Map(tokens.map((token) => [token.token.address, token])); this.tokenPairs = tokenPairs; + this.hook = hook // add BPT to tokenMap, so we can handle add/remove liquidity operations const bpt = new Token(tokens[0].token.chainId, this.id, 18, 'BPT', 'BPT'); @@ -181,6 +205,7 @@ export class WeightedPoolV3 implements BasePoolV3 { kind: RemoveKind.SINGLE_TOKEN_EXACT_IN, }, this.poolState, + this.hook, ); calculatedAmount = amountsOutRaw[tOut.index]; } else if (tOut.token.isSameAddress(this.id)) { @@ -193,6 +218,7 @@ export class WeightedPoolV3 implements BasePoolV3 { kind: AddKind.UNBALANCED, }, this.poolState, + this.hook ); calculatedAmount = bptAmountOutRaw; } else { @@ -205,6 +231,7 @@ export class WeightedPoolV3 implements BasePoolV3 { swapKind: SwapKind.GivenIn, }, this.poolState, + this.hook, ); } return TokenAmount.fromRawAmount(tOut.token, calculatedAmount); @@ -268,6 +295,18 @@ export class WeightedPoolV3 implements BasePoolV3 { }; } + public getHookState(): HookState | undefined { + if (this.hook === undefined) { + return undefined; + } + + // returned hook state will depend on hook type eventually + return { + tokens: this.tokens.map((t) => t.token.address), + removeLiquidityHookFeePercentage: this.hook.removeLiquidityHookFeePercentage, + }; + } + // Helper methods public getPoolTokens(tokenIn: Token, tokenOut: Token): { tIn: WeightedPoolToken; tOut: WeightedPoolToken } { diff --git a/modules/sor/sorV2/lib/static.ts b/modules/sor/sorV2/lib/static.ts index ce5823867..905482d8b 100644 --- a/modules/sor/sorV2/lib/static.ts +++ b/modules/sor/sorV2/lib/static.ts @@ -1,5 +1,5 @@ import { Router } from './router'; -import { PrismaPoolWithDynamic } from '../../../../prisma/prisma-types'; +import { PrismaPoolWithDynamic, PrismaHookWithDynamic } from '../../../../prisma/prisma-types'; import { checkInputs } from './utils/helpers'; import { ComposableStablePool, FxPool, Gyro2Pool, Gyro3Pool, GyroEPool, MetaStablePool, WeightedPool } from './poolsV2'; import { SwapKind, Token } from '@balancer/sdk'; @@ -15,6 +15,7 @@ export async function sorGetPathsWithPools( swapAmountEvm: bigint, prismaPools: PrismaPoolWithDynamic[], protocolVersion: number, + hooks: PrismaHookWithDynamic[], swapOptions?: Omit, ): Promise { const checkedSwapAmount = checkInputs(tokenIn, tokenOut, swapKind, swapAmountEvm); @@ -28,7 +29,7 @@ export async function sorGetPathsWithPools( if (prismaPool.protocolVersion === 2) { basePools.push(WeightedPool.fromPrismaPool(prismaPool)); } else { - basePools.push(WeightedPoolV3.fromPrismaPool(prismaPool)); + basePools.push(WeightedPoolV3.fromPrismaPool(prismaPool, hooks)); } } break; @@ -39,7 +40,7 @@ export async function sorGetPathsWithPools( case 'STABLE': { if (prismaPool.protocolVersion === 3) { - basePools.push(StablePool.fromPrismaPool(prismaPool)); + basePools.push(StablePool.fromPrismaPool(prismaPool, hooks)); } } break; diff --git a/modules/sor/sorV2/sorPathService.ts b/modules/sor/sorV2/sorPathService.ts index a057d6632..d06b7f096 100644 --- a/modules/sor/sorV2/sorPathService.ts +++ b/modules/sor/sorV2/sorPathService.ts @@ -53,6 +53,7 @@ class SorPathService implements SwapService { try { const poolsFromDb = await this.getBasePoolsFromDb(chain, protocolVersion); + // TODO pools are known, fetch all hooks for known pools const tIn = await getToken(tokenIn as Address, chain); const tOut = await getToken(tokenOut as Address, chain); const swapKind = this.mapSwapTypeToSwapKind(swapType); @@ -145,6 +146,7 @@ class SorPathService implements SwapService { ): Promise { try { const poolsFromDb = await this.getBasePoolsFromDb(chain, protocolVersion); + // TODO Pools are known, fetch all hooks for the given pools const tIn = await getToken(tokenIn as Address, chain); const tOut = await getToken(tokenOut as Address, chain); const swapKind = this.mapSwapTypeToSwapKind(swapType); @@ -490,6 +492,10 @@ class SorPathService implements SwapService { return pools; } + private async getBaseHooksFromDb(chain: Chain): Promise { + + } + private mapRoutes(paths: PathWithAmount[], pools: GqlPoolMinimal[]): GqlSorSwapRoute[] { const isBatchSwap = paths.length > 1 || paths[0].pools.length > 1; diff --git a/prisma/prisma-types.ts b/prisma/prisma-types.ts index 9e4d963e2..9898ce5f0 100644 --- a/prisma/prisma-types.ts +++ b/prisma/prisma-types.ts @@ -349,3 +349,11 @@ export const prismaPoolWithDynamic = Prisma.validator()({ }); export type PrismaPoolWithDynamic = Prisma.PrismaPoolGetPayload; + +export const prismaHookWithDynamic = Prisma.validator()({ + include: { + dynamicData: true, + }, +}); + +export type PrismaHookWithDynamic = Prisma.PrismaHookGetPayload; diff --git a/test/factories/index.ts b/test/factories/index.ts index 8324c7740..e2d2348ef 100644 --- a/test/factories/index.ts +++ b/test/factories/index.ts @@ -3,4 +3,6 @@ export * from './pool_token.factory'; export * from './prismaPool.factory'; export * from './prismaPoolDynamicData.factory'; export * from './prismaToken.factory'; +export * from './prismaHookDynamicData.factory'; +export * from './prismaHook.factory'; export * as SOR from './sor'; diff --git a/test/factories/prismaHook.factory.ts b/test/factories/prismaHook.factory.ts new file mode 100644 index 000000000..0b970ebeb --- /dev/null +++ b/test/factories/prismaHook.factory.ts @@ -0,0 +1,30 @@ +import { Factory } from 'fishery'; +import { createRandomAddress } from '../utils'; +import { Chain } from '@prisma/client'; +import { Hook } from '../../schema'; // Adjust the path based on your project structure +import { hookDataFactory } from './prismaHookDynamicData.factory'; + +class PrismaHookFactory extends Factory { + +} + +export const hookFactory = PrismaHookFactory.define(({ params }) => { + const hookAddress = params?.address ?? createRandomAddress(); + + return { + address: hookAddress, + chain: params?.chain || Chain.SEPOLIA, + dynamicData: params?.dynamicData ?? hookDataFactory.build(), + enableHookAdjustedAmounts: params?.enableHookAdjustedAmounts ?? false, + poolsIds: params?.poolsIds ?? [], + shouldCallAfterAddLiquidity: params?.shouldCallAfterAddLiquidity ?? false, + shouldCallAfterInitialize: params?.shouldCallAfterInitialize ?? false, + shouldCallAfterRemoveLiquidity: params?.shouldCallAfterRemoveLiquidity ?? false, + shouldCallAfterSwap: params?.shouldCallAfterSwap ?? false, + shouldCallBeforeAddLiquidity: params?.shouldCallBeforeAddLiquidity ?? false, + shouldCallBeforeInitialize: params?.shouldCallBeforeInitialize ?? false, + shouldCallBeforeRemoveLiquidity: params?.shouldCallBeforeRemoveLiquidity ?? false, + shouldCallBeforeSwap: params?.shouldCallBeforeSwap ?? false, + shouldCallComputeDynamicSwapFee: params?.shouldCallComputeDynamicSwapFee ?? false, + }; +}); diff --git a/test/factories/prismaHookDynamicData.factory.ts b/test/factories/prismaHookDynamicData.factory.ts new file mode 100644 index 000000000..8a4eb486e --- /dev/null +++ b/test/factories/prismaHookDynamicData.factory.ts @@ -0,0 +1,12 @@ +import { Factory } from 'fishery'; +import { HookData } from '../../schema'; + +export class HookDataFactory extends Factory {} + +export const hookDataFactory = HookDataFactory.define(({ params }) => { + return { + addLiquidityFeePercentage: params?.addLiquidityFeePercentage ?? '0.01', + removeLiquidityFeePercentage: params?.removeLiquidityFeePercentage ?? '0.01', + swapFeePercentage: params?.swapFeePercentage ?? '0.01', + }; +});