Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

SOR - Update StableSurge hook support #1582

Merged
merged 8 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/modern-shrimps-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'backend': patch
---

SOR - Update stable surge hook support
Binary file modified bun.lockb
Binary file not shown.
12 changes: 7 additions & 5 deletions modules/sor/sor-debug.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,14 @@ describe('sor debugging', () => {

it('sor v3', async () => {
const useProtocolVersion = 3;
const chain = Chain.MAINNET;
const chain = Chain.SEPOLIA;

const chainId = Object.keys(chainIdToChain).find((key) => chainIdToChain[key] === chain) as string;
initRequestScopedContext();
setRequestScopedContextValue('chainId', chainId);
// only do once before starting to debug
// await PoolController().reloadPoolsV3(chain);
// await PoolController().syncHookData(chain);
// await TokenController().syncErc4626Tokens(chain);
// await TokenController().syncErc4626UnwrapRates(chain);

Expand All @@ -56,12 +57,13 @@ describe('sor debugging', () => {

const swaps = await sorService.getSorSwapPaths({
chain,
tokenIn: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
tokenOut: '0x7204b7dbf9412567835633b6f00c3edc3a8d6330', // csUSDC
tokenIn: '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8', // USDCaave
tokenOut: '0xaa8e23fb1079ea71e0a56f48a2aa51851d8433d0', // USDTaave
swapType: 'EXACT_IN',
swapAmount: '1000',
swapAmount: '10000',
useProtocolVersion,
// poolIds: ['0x10a04efba5b880e169920fd4348527c64fb29d4d'], // boosted
considerPoolsWithHooks: true,
// poolIds: ['0x9b677c72a1160e1e03fe542bfd2b0f373fa94a8c'], // boosted
});

console.log(swaps.returnAmount);
Expand Down
7 changes: 1 addition & 6 deletions modules/sor/sorV2/lib/poolsV3/stable/stablePool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,6 @@ export class StablePoolV3 implements BasePoolV3 {
//transform
const hookState = getHookState(pool);

// typeguard
if (!isLiquidityManagement(pool.liquidityManagement)) {
throw new Error('LiquidityManagement must be of type LiquidityManagement and cannot be null');
}

return new StablePoolV3(
pool.id as Hex,
pool.address,
Expand All @@ -109,7 +104,7 @@ export class StablePoolV3 implements BasePoolV3 {
poolTokens,
totalShares,
pool.dynamicData.tokenPairsData as TokenPairData[],
pool.liquidityManagement,
pool.liquidityManagement as unknown as LiquidityManagement,
hookState,
);
}
Expand Down
7 changes: 1 addition & 6 deletions modules/sor/sorV2/lib/poolsV3/weighted/weightedPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,6 @@ export class WeightedPoolV3 implements BasePoolV3 {
//transform
const hookState = getHookState(pool);

// typeguard
if (!isLiquidityManagement(pool.liquidityManagement)) {
throw new Error('LiquidityManagement must be of type LiquidityManagement and cannot be null');
}

return new WeightedPoolV3(
pool.id as Hex,
pool.address,
Expand All @@ -106,7 +101,7 @@ export class WeightedPoolV3 implements BasePoolV3 {
parseEther(pool.dynamicData.totalShares),
poolTokens,
pool.dynamicData.tokenPairsData as TokenPairData[],
pool.liquidityManagement,
pool.liquidityManagement as unknown as LiquidityManagement,
hookState,
);
}
Expand Down
9 changes: 8 additions & 1 deletion modules/sor/sorV2/lib/static.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Router } from './router';
import { PrismaPoolAndHookWithDynamic } from '../../../../prisma/prisma-types';
import { checkInputs } from './utils/helpers';
import { checkInputs, isLiquidityManagement } from './utils/helpers';
import {
ComposableStablePool,
FxPool,
Expand Down Expand Up @@ -32,6 +32,13 @@ export async function sorGetPathsWithPools(
const basePools: BasePool[] = [];

for (const prismaPool of prismaPools) {
// typeguard
if (prismaPool.protocolVersion === 3) {
if (!isLiquidityManagement(prismaPool.liquidityManagement)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving the typeguard here seems to be more elegant as it just does not consider the pool for route building and simply continues.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the main problem was that it was throwing an error if LiquidityManagement wasn't properly set.
This means that the whole SOR will break with a single corrupted pool in the DB.
Moving here allows us to simply skip that pool and later investigate why it doesn't have that attribute properly set

console.log('LiquidityManagement incorrect for pool', prismaPool.id);
continue;
}
}
switch (prismaPool.type) {
case 'WEIGHTED':
/// LBPs can be handled like weighted pools
Expand Down
76 changes: 45 additions & 31 deletions modules/sor/sorV2/lib/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { HookState } from '@balancer-labs/balancer-maths';
import { LiquidityManagement } from '../../../types';

import { parseEther, parseUnits } from 'viem';
import { PrismaPoolAndHookWithDynamic } from '../../../../../prisma/prisma-types';
import { HookData } from '../../../../sources/transformers';

export function checkInputs(
tokenIn: Token,
Expand Down Expand Up @@ -77,41 +79,53 @@ export function getOutputAmount(paths: PathWithAmount[]): TokenAmount {
return amounts.reduce((a, b) => a.add(b));
}

export function getHookState(pool: any): HookState | undefined {
if (pool.hook === undefined || pool.hook === null) {
export function getHookState(pool: PrismaPoolAndHookWithDynamic): HookState | undefined {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great change. I recall running into build issues when trying it. But it seems like back then I was not aware of the existing HookData type, which would have allowed me to assign the pool the PrismaPoolAndHookWithDynamic type (but still stick to using the name). The recently added GqlHookType by franz is a great addition.

if (!pool.hook) {
return undefined;
}

if (pool.hook.name === 'ExitFee') {
// api for this hook is an Object with removeLiquidityFeePercentage key & fee as string
const dynamicData = pool.hook.dynamicData as { removeLiquidityFeePercentage: string };

return {
tokens: pool.tokens.map((token: { address: string }) => token.address),
// ExitFeeHook will always have dynamicData as part of the API response
removeLiquidityHookFeePercentage: parseEther(dynamicData.removeLiquidityFeePercentage),
hookType: pool.hook.name,
};
}

if (pool.hook.name === 'DirectionalFee') {
// this hook does not require a hook state to be passed
return {
hookType: pool.hook.name,
} as HookState;
const hookData = pool.hook as HookData;

switch (hookData.type) {
case 'EXIT_FEE': {
// api for this hook is an Object with removeLiquidityFeePercentage key & fee as string
const dynamicData = hookData.dynamicData as { removeLiquidityFeePercentage: string };

return {
tokens: pool.tokens.map((token: { address: string }) => token.address),
// ExitFeeHook will always have dynamicData as part of the API response
removeLiquidityHookFeePercentage: parseEther(dynamicData.removeLiquidityFeePercentage),
hookType: 'ExitFee',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about hard coding the hookType back then as well. What would be the advantage of hardcoding it here compared to forwarding the hookData.name?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYI, hook.name is being deprecated in the favor of hookType

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this new hookType that is returned here for the Balancer maths repo?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's how Balancer Maths knows which hook is being used. This is the mapper I said I'd add to decouple hook type from API vs hook type from balancer maths.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answering @mkflow27 - the advantage of rewriting the hook type here instead of forwarding is that we decouple how they are declared in the api vs in Balancer Maths. If we simply forwarded what was specified in the api, we add the overhead of having to sync and reach consensus on how each hook type is handled in both codebases.
Since we always need to add code to support new hook types on the SOR, adding another attribute is trivial.

};
}
case 'DIRECTIONAL_FEE': {
// this hook does not require a hook state to be passed
return {
hookType: 'DirectionalFee',
} as HookState;
}
case 'STABLE_SURGE': {
const typeData = pool.typeData as { amp: string };
const dynamicData = hookData.dynamicData as {
surgeThresholdPercentage: string;
maxSurgeFeePercentage: string;
};
return {
// amp onchain precision is 1000. Api returns 200 means onchain value is 200000
amp: parseUnits(typeData.amp, 3),
// 18 decimal precision.
surgeThresholdPercentage: parseEther(dynamicData.surgeThresholdPercentage),
maxSurgeFeePercentage: parseEther(dynamicData.maxSurgeFeePercentage),
hookType: 'StableSurge',
};
}
default:
if (hookData.type) {
console.warn(`pool ${pool.id} with hook type ${hookData.type} not implemented`);
}

return undefined;
}

if (pool.hook.name === 'StableSurge') {
return {
// amp onchain precision is 1000. Api returns 200 means onchain value is 200000
amp: parseUnits(pool.typeData.amp, 3),
// 18 decimal precision.
surgeThresholdPercentage: parseEther(pool.hook.dynamicData.surgeThresholdPercentage),
hookType: pool.hook.name,
};
}

throw new Error(`${pool.hook.name} hook not implemented`);
}

export function isLiquidityManagement(value: any): value is LiquidityManagement {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@aws-sdk/client-cloudwatch": "^3.734.0",
"@aws-sdk/client-secrets-manager": "^3.734.0",
"@aws-sdk/client-sqs": "^3.734.0",
"@balancer-labs/balancer-maths": "^0.0.20",
"@balancer-labs/balancer-maths": "^0.0.21",
"@balancer/sdk": "^1.4.1",
"@ethersproject/address": "^5.7.0",
"@ethersproject/bignumber": "^5.7.0",
Expand Down