diff --git a/.changeset/slow-geese-join.md b/.changeset/slow-geese-join.md new file mode 100644 index 00000000..f3da4504 --- /dev/null +++ b/.changeset/slow-geese-join.md @@ -0,0 +1,5 @@ +--- +"@balancer/sdk": minor +--- + +create gyro E-CLP on v3 diff --git a/package.json b/package.json index bc13dd55..ec8c38b2 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "typings": "dist/index.d.ts", - "files": [ - "dist/" - ], + "files": ["dist/"], "scripts": { "build": "tsup", "format": "biome format --write .", @@ -28,6 +26,7 @@ "example": "npx tsx ./examples/lib/executeExample.ts" }, "dependencies": { + "@balancer-labs/balancer-maths": "0.0.24", "decimal.js-light": "^2.5.1", "lodash.clonedeep": "^4.5.0", "viem": "^2.22.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52cb1baf..2642ccd8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@balancer-labs/balancer-maths': + specifier: 0.0.24 + version: 0.0.24 decimal.js-light: specifier: ^2.5.1 version: 2.5.1 @@ -97,6 +100,11 @@ packages: regenerator-runtime: 0.14.1 dev: true + /@balancer-labs/balancer-maths@0.0.24: + resolution: {integrity: sha512-M6iqcmLfR9CP1mw3R5w0FdtZkUk7M0vCJhloAfhE/xufbAZrCSoapFYlt/NMBKLDQoGNJ2FN5E4zpdLVsxhoow==} + engines: {node: '>=18.x'} + dev: false + /@biomejs/biome@1.5.2: resolution: {integrity: sha512-LhycxGQBQLmfv6M3e4tMfn/XKcUWyduDYOlCEBrHXJ2mMth2qzYt1JWypkWp+XmU/7Hl2dKvrP4mZ5W44+nWZw==} engines: {node: '>=14.*'} diff --git a/src/abi/gyroECLPPoolFactory.V3.ts b/src/abi/gyroECLPPoolFactory.V3.ts new file mode 100644 index 00000000..0f821105 --- /dev/null +++ b/src/abi/gyroECLPPoolFactory.V3.ts @@ -0,0 +1,339 @@ +export const gyroECLPPoolFactoryAbi_V3 = [ + { + inputs: [ + { internalType: 'contract IVault', name: 'vault', type: 'address' }, + { + internalType: 'uint32', + name: 'pauseWindowDuration', + type: 'uint32', + }, + { internalType: 'string', name: 'factoryVersion', type: 'string' }, + { internalType: 'string', name: 'poolVersion', type: 'string' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { inputs: [], name: 'Create2EmptyBytecode', type: 'error' }, + { inputs: [], name: 'Create2FailedDeployment', type: 'error' }, + { + inputs: [ + { internalType: 'uint256', name: 'balance', type: 'uint256' }, + { internalType: 'uint256', name: 'needed', type: 'uint256' }, + ], + name: 'Create2InsufficientBalance', + type: 'error', + }, + { inputs: [], name: 'Disabled', type: 'error' }, + { inputs: [], name: 'IndexOutOfBounds', type: 'error' }, + { inputs: [], name: 'PoolPauseWindowDurationOverflow', type: 'error' }, + { inputs: [], name: 'SenderNotAllowed', type: 'error' }, + { inputs: [], name: 'StandardPoolWithCreator', type: 'error' }, + { inputs: [], name: 'SupportsOnlyTwoTokens', type: 'error' }, + { anonymous: false, inputs: [], name: 'FactoryDisabled', type: 'event' }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'pool', + type: 'address', + }, + ], + name: 'PoolCreated', + type: 'event', + }, + { + inputs: [ + { internalType: 'string', name: 'name', type: 'string' }, + { internalType: 'string', name: 'symbol', type: 'string' }, + { + components: [ + { + internalType: 'contract IERC20', + name: 'token', + type: 'address', + }, + { + internalType: 'enum TokenType', + name: 'tokenType', + type: 'uint8', + }, + { + internalType: 'contract IRateProvider', + name: 'rateProvider', + type: 'address', + }, + { + internalType: 'bool', + name: 'paysYieldFees', + type: 'bool', + }, + ], + internalType: 'struct TokenConfig[]', + name: 'tokens', + type: 'tuple[]', + }, + { + components: [ + { internalType: 'int256', name: 'alpha', type: 'int256' }, + { internalType: 'int256', name: 'beta', type: 'int256' }, + { internalType: 'int256', name: 'c', type: 'int256' }, + { internalType: 'int256', name: 's', type: 'int256' }, + { internalType: 'int256', name: 'lambda', type: 'int256' }, + ], + internalType: 'struct IGyroECLPPool.EclpParams', + name: 'eclpParams', + type: 'tuple', + }, + { + components: [ + { + components: [ + { + internalType: 'int256', + name: 'x', + type: 'int256', + }, + { + internalType: 'int256', + name: 'y', + type: 'int256', + }, + ], + internalType: 'struct IGyroECLPPool.Vector2', + name: 'tauAlpha', + type: 'tuple', + }, + { + components: [ + { + internalType: 'int256', + name: 'x', + type: 'int256', + }, + { + internalType: 'int256', + name: 'y', + type: 'int256', + }, + ], + internalType: 'struct IGyroECLPPool.Vector2', + name: 'tauBeta', + type: 'tuple', + }, + { internalType: 'int256', name: 'u', type: 'int256' }, + { internalType: 'int256', name: 'v', type: 'int256' }, + { internalType: 'int256', name: 'w', type: 'int256' }, + { internalType: 'int256', name: 'z', type: 'int256' }, + { internalType: 'int256', name: 'dSq', type: 'int256' }, + ], + internalType: 'struct IGyroECLPPool.DerivedEclpParams', + name: 'derivedEclpParams', + type: 'tuple', + }, + { + components: [ + { + internalType: 'address', + name: 'pauseManager', + type: 'address', + }, + { + internalType: 'address', + name: 'swapFeeManager', + type: 'address', + }, + { + internalType: 'address', + name: 'poolCreator', + type: 'address', + }, + ], + internalType: 'struct PoolRoleAccounts', + name: 'roleAccounts', + type: 'tuple', + }, + { + internalType: 'uint256', + name: 'swapFeePercentage', + type: 'uint256', + }, + { + internalType: 'address', + name: 'poolHooksContract', + type: 'address', + }, + { internalType: 'bool', name: 'enableDonation', type: 'bool' }, + { + internalType: 'bool', + name: 'disableUnbalancedLiquidity', + type: 'bool', + }, + { internalType: 'bytes32', name: 'salt', type: 'bytes32' }, + ], + name: 'create', + outputs: [{ internalType: 'address', name: 'pool', type: 'address' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'disable', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes4', name: 'selector', type: 'bytes4' }], + name: 'getActionId', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getAuthorizer', + outputs: [ + { internalType: 'contract IAuthorizer', name: '', type: 'address' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getDefaultLiquidityManagement', + outputs: [ + { + components: [ + { + internalType: 'bool', + name: 'disableUnbalancedLiquidity', + type: 'bool', + }, + { + internalType: 'bool', + name: 'enableAddLiquidityCustom', + type: 'bool', + }, + { + internalType: 'bool', + name: 'enableRemoveLiquidityCustom', + type: 'bool', + }, + { + internalType: 'bool', + name: 'enableDonation', + type: 'bool', + }, + ], + internalType: 'struct LiquidityManagement', + name: 'liquidityManagement', + type: 'tuple', + }, + ], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [], + name: 'getDefaultPoolHooksContract', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes', name: 'constructorArgs', type: 'bytes' }, + { internalType: 'bytes32', name: 'salt', type: 'bytes32' }, + ], + name: 'getDeploymentAddress', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getNewPoolPauseWindowEndTime', + outputs: [{ internalType: 'uint32', name: '', type: 'uint32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getOriginalPauseWindowEndTime', + outputs: [{ internalType: 'uint32', name: '', type: 'uint32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getPauseWindowDuration', + outputs: [{ internalType: 'uint32', name: '', type: 'uint32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getPoolCount', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getPoolVersion', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getPools', + outputs: [{ internalType: 'address[]', name: '', type: 'address[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'start', type: 'uint256' }, + { internalType: 'uint256', name: 'count', type: 'uint256' }, + ], + name: 'getPoolsInRange', + outputs: [ + { internalType: 'address[]', name: 'pools', type: 'address[]' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getVault', + outputs: [ + { internalType: 'contract IVault', name: '', type: 'address' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'isDisabled', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'pool', type: 'address' }], + name: 'isPoolFromFactory', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'version', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, +] as const; diff --git a/src/abi/index.ts b/src/abi/index.ts index 30eecedd..b5be5c16 100644 --- a/src/abi/index.ts +++ b/src/abi/index.ts @@ -20,3 +20,4 @@ export * from './stablePoolFactory.V3'; export * from './balancerCompositeLiquidityRouterNested'; export * from './balancerCompositeLiquidityRouterBoosted'; export * from './stableSurgeFactory'; +export * from './gyroECLPPoolFactory.V3'; diff --git a/src/entities/createPool/createPoolV3/gyroECLP/createPoolGyroECLP.ts b/src/entities/createPool/createPoolV3/gyroECLP/createPoolGyroECLP.ts new file mode 100644 index 00000000..b3536f6e --- /dev/null +++ b/src/entities/createPool/createPoolV3/gyroECLP/createPoolGyroECLP.ts @@ -0,0 +1,87 @@ +import { getRandomBytes32 } from '@/entities/utils/getRandomBytes32'; +import { encodeFunctionData, zeroAddress } from 'viem'; +import { + CreatePoolBase, + CreatePoolBuildCallOutput, + PoolRoleAccounts, + CreatePoolV3BaseInput, + TokenConfig, +} from '../../types'; +import { gyroECLPPoolFactoryAbi_V3 } from '@/abi/gyroECLPPoolFactory.V3'; +import { GYROECLP_POOL_FACTORY_BALANCER_V3, sortByAddress } from '@/utils'; +import { Hex, PoolType } from '@/types'; + +export type EclpParams = { + alpha: bigint; + beta: bigint; + c: bigint; + s: bigint; + lambda: bigint; +}; + +export type Vector2 = { + x: bigint; + y: bigint; +}; + +export type DerivedEclpParams = { + tauAlpha: Vector2; + tauBeta: Vector2; + u: bigint; + v: bigint; + w: bigint; + z: bigint; + dSq: bigint; +}; + +export type CreatePoolGyroECLPInput = CreatePoolV3BaseInput & { + poolType: PoolType.GyroE; + tokens: TokenConfig[]; + eclpParams: EclpParams; + derivedEclpParams: DerivedEclpParams; +}; + +export class CreatePoolGyroECLP implements CreatePoolBase { + buildCall(input: CreatePoolGyroECLPInput): CreatePoolBuildCallOutput { + const callData = this.encodeCall(input); + return { + callData, + to: GYROECLP_POOL_FACTORY_BALANCER_V3[input.chainId], + }; + } + + private encodeCall(input: CreatePoolGyroECLPInput): Hex { + const sortedTokenConfigs = sortByAddress(input.tokens); + + const tokens = sortedTokenConfigs.map(({ address, ...rest }) => ({ + token: address, + ...rest, + })); + + const roleAccounts: PoolRoleAccounts = { + pauseManager: input.pauseManager, + swapFeeManager: input.swapFeeManager, + poolCreator: zeroAddress, + }; + + const args = [ + input.name || input.symbol, + input.symbol, + tokens, + input.eclpParams, + input.derivedEclpParams, + roleAccounts, + input.swapFeePercentage, + input.poolHooksContract, + input.enableDonation, + input.disableUnbalancedLiquidity, + input.salt || getRandomBytes32(), + ] as const; + + return encodeFunctionData({ + abi: gyroECLPPoolFactoryAbi_V3, + functionName: 'create', + args, + }); + } +} diff --git a/src/entities/createPool/createPoolV3/index.ts b/src/entities/createPool/createPoolV3/index.ts index 9d0e5543..dd273850 100644 --- a/src/entities/createPool/createPoolV3/index.ts +++ b/src/entities/createPool/createPoolV3/index.ts @@ -7,7 +7,7 @@ import { import { CreatePoolWeightedV3 } from './weighted/createPoolWeighted'; import { CreatePoolStableV3 } from './stable/createPoolStable'; import { CreatePoolStableSurge } from './stableSurge/createStableSurge'; - +import { CreatePoolGyroECLP } from './gyroECLP/createPoolGyroECLP'; export class CreatePoolV3 implements CreatePoolBase { private readonly createPoolTypes: Record = {}; @@ -16,6 +16,7 @@ export class CreatePoolV3 implements CreatePoolBase { [PoolType.Weighted]: new CreatePoolWeightedV3(), [PoolType.Stable]: new CreatePoolStableV3(), [PoolType.StableSurge]: new CreatePoolStableSurge(), + [PoolType.GyroE]: new CreatePoolGyroECLP(), }; } diff --git a/src/entities/createPool/createPoolV3/stable/createPoolStable.ts b/src/entities/createPool/createPoolV3/stable/createPoolStable.ts index 541b8321..2b731193 100644 --- a/src/entities/createPool/createPoolV3/stable/createPoolStable.ts +++ b/src/entities/createPool/createPoolV3/stable/createPoolStable.ts @@ -22,6 +22,11 @@ export class CreatePoolStableV3 implements CreatePoolBase { private encodeCall(input: CreatePoolV3StableInput): Hex { const sortedTokenConfigs = sortByAddress(input.tokens); + const tokens = sortedTokenConfigs.map(({ address, ...rest }) => ({ + token: address, + ...rest, + })); + const roleAccounts: PoolRoleAccounts = { pauseManager: input.pauseManager, swapFeeManager: input.swapFeeManager, @@ -31,14 +36,7 @@ export class CreatePoolStableV3 implements CreatePoolBase { const args = [ input.name || input.symbol, input.symbol, - sortedTokenConfigs.map( - ({ address, rateProvider, tokenType, paysYieldFees }) => ({ - token: address, - tokenType, - rateProvider, - paysYieldFees, - }), - ), + tokens, input.amplificationParameter, roleAccounts, input.swapFeePercentage, diff --git a/src/entities/createPool/createPoolV3/stableSurge/createStableSurge.ts b/src/entities/createPool/createPoolV3/stableSurge/createStableSurge.ts index 4db2a4d6..e4404945 100644 --- a/src/entities/createPool/createPoolV3/stableSurge/createStableSurge.ts +++ b/src/entities/createPool/createPoolV3/stableSurge/createStableSurge.ts @@ -22,6 +22,11 @@ export class CreatePoolStableSurge implements CreatePoolBase { private encodeCall(input: CreatePoolStableSurgeInput): Hex { const sortedTokenConfigs = sortByAddress(input.tokens); + const tokens = sortedTokenConfigs.map(({ address, ...rest }) => ({ + token: address, + ...rest, + })); + const roleAccounts: PoolRoleAccounts = { pauseManager: input.pauseManager, swapFeeManager: input.swapFeeManager, @@ -31,14 +36,7 @@ export class CreatePoolStableSurge implements CreatePoolBase { const args = [ input.name || input.symbol, input.symbol, - sortedTokenConfigs.map( - ({ address, rateProvider, tokenType, paysYieldFees }) => ({ - token: address, - tokenType, - rateProvider, - paysYieldFees, - }), - ), + tokens, input.amplificationParameter, roleAccounts, input.swapFeePercentage, diff --git a/src/entities/createPool/createPoolV3/weighted/createPoolWeighted.ts b/src/entities/createPool/createPoolV3/weighted/createPoolWeighted.ts index 97a152f0..73908109 100644 --- a/src/entities/createPool/createPoolV3/weighted/createPoolWeighted.ts +++ b/src/entities/createPool/createPoolV3/weighted/createPoolWeighted.ts @@ -4,7 +4,6 @@ import { CreatePoolBase, CreatePoolV3WeightedInput, CreatePoolBuildCallOutput, - TokenConfig, PoolRoleAccounts, } from '../../types'; import { weightedPoolFactoryAbi_V3 } from '@/abi/weightedPoolFactory.V3'; @@ -21,30 +20,16 @@ export class CreatePoolWeightedV3 implements CreatePoolBase { } private encodeCall(input: CreatePoolV3WeightedInput): Hex { - const sortedTokenParams = sortByAddress(input.tokens); + const sortedTokenConfigs = sortByAddress(input.tokens); - const [tokenConfigs, normalizedWeights] = sortedTokenParams.reduce( - ( - acc, - { - address: tokenAddress, - rateProvider, - weight, - tokenType, - paysYieldFees, - }, - ) => { - acc[0].push({ - token: tokenAddress, - tokenType, - rateProvider, - paysYieldFees: paysYieldFees ?? false, - }); - acc[1].push(weight); - return acc; - }, - [[], []] as [TokenConfig[], bigint[]], - ); + const normalizedWeights: bigint[] = []; + const tokens = sortedTokenConfigs.map(({ weight, ...rest }) => { + normalizedWeights.push(weight); + return { + token: rest.address, + ...rest, + }; + }); const roleAccounts: PoolRoleAccounts = { pauseManager: input.pauseManager, @@ -55,7 +40,7 @@ export class CreatePoolWeightedV3 implements CreatePoolBase { const args = [ input.name || input.symbol, input.symbol, - tokenConfigs, + tokens, normalizedWeights, roleAccounts, input.swapFeePercentage, diff --git a/src/entities/createPool/index.ts b/src/entities/createPool/index.ts index 6cda3e09..6d123e0d 100644 --- a/src/entities/createPool/index.ts +++ b/src/entities/createPool/index.ts @@ -9,6 +9,7 @@ import { CreatePoolV3 } from './createPoolV3'; export * from './types'; export * from './createPoolV3/stableSurge/createStableSurge'; +export * from './createPoolV3/gyroECLP/createPoolGyroECLP'; export class CreatePool implements CreatePoolBase { private readonly inputValidator: InputValidator; diff --git a/src/entities/createPool/types.ts b/src/entities/createPool/types.ts index eb7abbff..2259a512 100644 --- a/src/entities/createPool/types.ts +++ b/src/entities/createPool/types.ts @@ -1,5 +1,6 @@ import { PoolType, TokenType } from '@/types'; import { Address, Hex } from 'viem'; +import { CreatePoolGyroECLPInput } from './createPoolV3/gyroECLP/createPoolGyroECLP'; export interface CreatePoolBase { buildCall(input: CreatePoolInput): CreatePoolBuildCallOutput; @@ -52,12 +53,7 @@ export type CreatePoolV3BaseInput = CreatePoolBaseInput & { export type CreatePoolV3StableInput = CreatePoolV3BaseInput & { poolType: PoolType.Stable; amplificationParameter: bigint; // value between 1e3 and 5000e3 - tokens: { - address: Address; - rateProvider: Address; - tokenType: TokenType; - paysYieldFees: boolean; - }[]; + tokens: TokenConfig[]; }; export type CreatePoolStableSurgeInput = Omit< @@ -69,13 +65,7 @@ export type CreatePoolStableSurgeInput = Omit< export type CreatePoolV3WeightedInput = CreatePoolV3BaseInput & { poolType: PoolType.Weighted; - tokens: { - address: Address; - rateProvider: Address; - weight: bigint; - tokenType: TokenType; - paysYieldFees?: boolean; - }[]; + tokens: (TokenConfig & { weight: bigint })[]; }; export type CreatePoolInput = @@ -83,7 +73,8 @@ export type CreatePoolInput = | CreatePoolV2ComposableStableInput | CreatePoolV3WeightedInput | CreatePoolV3StableInput - | CreatePoolStableSurgeInput; + | CreatePoolStableSurgeInput + | CreatePoolGyroECLPInput; export type CreatePoolBuildCallOutput = { callData: Hex; @@ -115,7 +106,7 @@ export type CreatePoolV2ComposableStableArgs = [ ]; export type TokenConfig = { - token: Address; + address: Address; tokenType: TokenType; rateProvider: Address; paysYieldFees: boolean; diff --git a/src/entities/inputValidator/gyro/inputValidatorGyro.ts b/src/entities/inputValidator/gyro/inputValidatorGyro.ts index dacb3a5d..0067a9ca 100644 --- a/src/entities/inputValidator/gyro/inputValidatorGyro.ts +++ b/src/entities/inputValidator/gyro/inputValidatorGyro.ts @@ -1,6 +1,4 @@ -import { InitPoolInput } from '@/entities/initPool'; import { AddLiquidityInput, AddLiquidityKind } from '../../addLiquidity/types'; -import { CreatePoolInput } from '../../createPool/types'; import { RemoveLiquidityInput, RemoveLiquidityKind, @@ -15,11 +13,17 @@ import { addLiquidityProportionalOnlyError, removeLiquidityProportionalOnlyError, } from '@/utils'; +import { CreatePoolGyroECLPInput } from '@/entities/createPool'; +import { GyroECLPMath } from '@balancer-labs/balancer-maths'; export class InputValidatorGyro extends InputValidatorBase { - // biome-ignore lint/correctness/noUnusedVariables: - validateInitPool(initPoolInput: InitPoolInput, poolState: PoolState): void { - throw new Error('Method not implemented.'); + validateCreatePool(input: CreatePoolGyroECLPInput) { + super.validateCreatePool(input); + + const { eclpParams, derivedEclpParams } = input; + + GyroECLPMath.validateParams(eclpParams); + GyroECLPMath.validateDerivedParams(eclpParams, derivedEclpParams); } validateAddLiquidity( @@ -47,9 +51,4 @@ export class InputValidatorGyro extends InputValidatorBase { } validateTokensRemoveLiquidity(removeLiquidityInput, poolState); } - - validateCreatePool(input: CreatePoolInput): void { - console.log(input); - throw new Error('Method not implemented.'); - } } diff --git a/src/utils/constantsV3.ts b/src/utils/constantsV3.ts index b78302bf..562225a7 100644 --- a/src/utils/constantsV3.ts +++ b/src/utils/constantsV3.ts @@ -54,6 +54,12 @@ export const STABLE_SURGE_FACTORY: Record = { [ChainId.SEPOLIA]: '0xD516c344413B4282dF1E4082EAE6B1081F3b1932', }; +export const GYROECLP_POOL_FACTORY_BALANCER_V3: Record = { + [ChainId.ARBITRUM_ONE]: '0x268E2EE1413D768b6e2dc3F5a4ddc9Ae03d9AF42', + [ChainId.BASE]: '0x6B5dA774890Db7B7b96C6f44e6a4b0F657399E2e', + [ChainId.SEPOLIA]: '0x2255b6a03A6eDd0D6CC670864F297869063FE00F', +}; + // V3 export const BALANCER_ROUTER: Record = { [ChainId.SEPOLIA]: '0x0BF61f706105EA44694f2e92986bD01C39930280', diff --git a/test/lib/utils/createPoolHelper.ts b/test/lib/utils/createPoolHelper.ts index 774575cf..02f2e4b0 100644 --- a/test/lib/utils/createPoolHelper.ts +++ b/test/lib/utils/createPoolHelper.ts @@ -1,15 +1,16 @@ import { CreatePoolTxInput } from './types'; import { + CreatePool, Address, PoolType, weightedPoolFactoryV4Abi_V2, composableStableFactoryV6Abi_V2, weightedPoolFactoryAbi_V3, stablePoolFactoryAbi_V3, + gyroECLPPoolFactoryAbi_V3, + stableSurgeFactoryAbi, } from 'src'; import { findEventInReceiptLogs } from './findEventInReceiptLogs'; -import { CreatePool } from 'src'; -import { stableSurgeFactoryAbi } from '@/abi/stableSurgeFactory'; export async function doCreatePool( txInput: CreatePoolTxInput, @@ -28,6 +29,7 @@ export async function doCreatePool( [PoolType.Weighted]: weightedPoolFactoryAbi_V3, [PoolType.Stable]: stablePoolFactoryAbi_V3, [PoolType.StableSurge]: stableSurgeFactoryAbi, + [PoolType.GyroE]: gyroECLPPoolFactoryAbi_V3, }, }; diff --git a/test/v3/createPool/gyroECLP/gyroECLP.integration.test.ts b/test/v3/createPool/gyroECLP/gyroECLP.integration.test.ts new file mode 100644 index 00000000..482466a1 --- /dev/null +++ b/test/v3/createPool/gyroECLP/gyroECLP.integration.test.ts @@ -0,0 +1,198 @@ +// pnpm test -- v3/createPool/gyroECLP/gyroECLP.integration.test.ts +import { + Address, + createTestClient, + http, + publicActions, + walletActions, + zeroAddress, + parseUnits, + TestActions, +} from 'viem'; +import { + CHAINS, + ChainId, + PoolType, + TokenType, + CreatePoolGyroECLPInput, + InitPool, + Permit2Helper, + PERMIT2, + VAULT_V3, + vaultExtensionAbi_V3, + PublicWalletClient, + InitPoolDataProvider, +} from 'src'; +import { ANVIL_NETWORKS, startFork } from '../../../anvil/anvil-global-setup'; +import { + doCreatePool, + TOKENS, + assertInitPool, + setTokenBalances, + approveSpenderOnTokens, + sendTransactionGetBalances, +} from '../../../lib/utils'; + +const protocolVersion = 3; +const chainId = ChainId.SEPOLIA; +const poolType = PoolType.GyroE; +const BAL = TOKENS[chainId].BAL; +const DAI = TOKENS[chainId].DAI; + +describe('GyroECLP - create & init', () => { + let rpcUrl: string; + let client: PublicWalletClient & TestActions; + let testAddress: Address; + let createPoolInput: CreatePoolGyroECLPInput; + let poolAddress: Address; + + beforeAll(async () => { + ({ rpcUrl } = await startFork( + ANVIL_NETWORKS.SEPOLIA, + undefined, + 7747598n, + )); + client = createTestClient({ + mode: 'anvil', + chain: CHAINS[chainId], + transport: http(rpcUrl, { timeout: 120_000 }), + }) + .extend(publicActions) + .extend(walletActions); + testAddress = (await client.getAddresses())[0]; + + await setTokenBalances( + client, + testAddress, + [DAI.address, BAL.address], + [DAI.slot!, BAL.slot!], + [parseUnits('100', 18), parseUnits('100', 18)], + ); + + await approveSpenderOnTokens( + client, + testAddress, + [DAI.address, BAL.address], + PERMIT2[chainId], + ); + + createPoolInput = { + poolType, + symbol: '50BAL-50DAI', + tokens: [ + { + address: BAL.address, + rateProvider: zeroAddress, + tokenType: TokenType.STANDARD, + paysYieldFees: false, + }, + { + address: DAI.address, + rateProvider: zeroAddress, + tokenType: TokenType.STANDARD, + paysYieldFees: false, + }, + ], + swapFeePercentage: 10000000000000000n, + poolHooksContract: zeroAddress, + pauseManager: testAddress, + swapFeeManager: testAddress, + disableUnbalancedLiquidity: false, + chainId, + protocolVersion, + enableDonation: false, + eclpParams: { + alpha: 998502246630054917n, + beta: 1000200040008001600n, + c: 707106781186547524n, + s: 707106781186547524n, + lambda: 4000000000000000000000n, + }, + derivedEclpParams: { + tauAlpha: { + x: -94861212813096057289512505574275160547n, + y: 31644119574235279926451292677567331630n, + }, + tauBeta: { + x: 37142269533113549537591131345643981951n, + y: 92846388265400743995957747409218517601n, + }, + u: 66001741173104803338721745994955553010n, + v: 62245253919818011890633399060291020887n, + w: 30601134345582732000058913853921008022n, + z: -28859471639991253843240999485797747790n, + dSq: 99999999999999999886624093342106115200n, + }, + }; + + poolAddress = await doCreatePool({ + client, + testAddress, + createPoolInput, + }); + }, 120_000); + + test('pool should be created', async () => { + expect(poolAddress).to.not.be.undefined; + }); + + test('pool should be registered with Vault', async () => { + const isPoolRegistered = await client.readContract({ + address: VAULT_V3[chainId], + abi: vaultExtensionAbi_V3, + functionName: 'isPoolRegistered', + args: [poolAddress], + }); + expect(isPoolRegistered).to.be.true; + }); + + test('pool should init', async () => { + const initPoolInput = { + amountsIn: [ + { + address: BAL.address, + rawAmount: parseUnits('10', BAL.decimals), + decimals: BAL.decimals, + }, + { + address: DAI.address, + rawAmount: parseUnits('17', DAI.decimals), + decimals: DAI.decimals, + }, + ], + minBptAmountOut: 0n, + chainId, + }; + + const initPoolDataProvider = new InitPoolDataProvider(chainId, rpcUrl); + const poolState = await initPoolDataProvider.getInitPoolData( + poolAddress, + poolType, + protocolVersion, + ); + + const permit2 = await Permit2Helper.signInitPoolApproval({ + ...initPoolInput, + client, + owner: testAddress, + }); + + const initPool = new InitPool(); + const initPoolBuildOutput = initPool.buildCallWithPermit2( + initPoolInput, + poolState, + permit2, + ); + + const txOutput = await sendTransactionGetBalances( + [BAL.address, DAI.address], + client, + testAddress, + initPoolBuildOutput.to, + initPoolBuildOutput.callData, + initPoolBuildOutput.value, + ); + + assertInitPool(initPoolInput, { txOutput, initPoolBuildOutput }); + }, 120_000); +}); diff --git a/test/v3/createPool/gyroECLP/gyroECLP.validation.test.ts b/test/v3/createPool/gyroECLP/gyroECLP.validation.test.ts new file mode 100644 index 00000000..ddcd9eb4 --- /dev/null +++ b/test/v3/createPool/gyroECLP/gyroECLP.validation.test.ts @@ -0,0 +1,342 @@ +// pnpm test -- createPool/gyroECLP/gyroECLP.validation.test.ts +import { zeroAddress, parseUnits } from 'viem'; +import { + ChainId, + PoolType, + TokenType, + CreatePool, + CreatePoolGyroECLPInput, +} from 'src'; +import { TOKENS } from 'test/lib/utils/addresses'; + +import { GyroECLPMath } from '@balancer-labs/balancer-maths'; + +const { + _ONE, + _ONE_XP, + _DERIVED_TAU_NORM_ACCURACY_XP, + _DERIVED_DSQ_NORM_ACCURACY_XP, + _ROTATION_VECTOR_NORM_ACCURACY, + _MAX_STRETCH_FACTOR, + _MAX_INV_INVARIANT_DENOMINATOR_XP, +} = GyroECLPMath; + +const chainId = ChainId.SEPOLIA; +const BAL = TOKENS[chainId].BAL; +const DAI = TOKENS[chainId].DAI; + +describe('create GyroECLP pool input validations', () => { + const createPool = new CreatePool(); + let createPoolInput: CreatePoolGyroECLPInput; + + beforeAll(async () => { + // Start with a valid input and modify individual params to expect errors + createPoolInput = { + poolType: PoolType.GyroE, + symbol: '50BAL-50DAI', + tokens: [ + { + address: BAL.address, + rateProvider: zeroAddress, + tokenType: TokenType.STANDARD, + paysYieldFees: false, + }, + { + address: DAI.address, + rateProvider: zeroAddress, + tokenType: TokenType.STANDARD, + paysYieldFees: false, + }, + ], + swapFeePercentage: 10000000000000000n, + poolHooksContract: zeroAddress, + pauseManager: zeroAddress, + swapFeeManager: zeroAddress, + disableUnbalancedLiquidity: false, + chainId, + protocolVersion: 3, + enableDonation: false, + eclpParams: { + alpha: 998502246630054917n, + beta: 1000200040008001600n, + c: 707106781186547524n, + s: 707106781186547524n, + lambda: 4000000000000000000000n, + }, + derivedEclpParams: { + tauAlpha: { + x: -94861212813096057289512505574275160547n, + y: 31644119574235279926451292677567331630n, + }, + tauBeta: { + x: 37142269533113549537591131345643981951n, + y: 92846388265400743995957747409218517601n, + }, + u: 66001741173104803338721745994955553010n, + v: 62245253919818011890633399060291020887n, + w: 30601134345582732000058913853921008022n, + z: -28859471639991253843240999485797747790n, + dSq: 99999999999999999886624093342106115200n, + }, + }; + }); + + // Helper function to modify input parameters succinctly + const buildCallWithModifiedInput = ( + updates: Partial< + Omit + > & { + eclpParams?: Partial; + derivedEclpParams?: Partial< + Omit< + CreatePoolGyroECLPInput['derivedEclpParams'], + 'tauAlpha' | 'tauBeta' + > + > & { + tauAlpha?: Partial<{ x: bigint; y: bigint }>; + tauBeta?: Partial<{ x: bigint; y: bigint }>; + }; + } = {}, + ) => { + createPool.buildCall({ + ...createPoolInput, + ...updates, + eclpParams: { + ...createPoolInput.eclpParams, + ...updates.eclpParams, + }, + derivedEclpParams: { + ...createPoolInput.derivedEclpParams, + ...updates.derivedEclpParams, + tauAlpha: { + ...createPoolInput.derivedEclpParams.tauAlpha, + ...updates.derivedEclpParams?.tauAlpha, + }, + tauBeta: { + ...createPoolInput.derivedEclpParams.tauBeta, + ...updates.derivedEclpParams?.tauBeta, + }, + }, + }); + }; + + test('Duplicate token addresses, expects error', async () => { + const tokens: CreatePoolGyroECLPInput['tokens'] = [ + { + address: BAL.address, + rateProvider: zeroAddress, + tokenType: TokenType.STANDARD, + paysYieldFees: false, + }, + { + address: BAL.address, + rateProvider: zeroAddress, + tokenType: TokenType.STANDARD, + paysYieldFees: false, + }, + { + address: DAI.address, + rateProvider: zeroAddress, + tokenType: TokenType.STANDARD, + paysYieldFees: false, + }, + ]; + expect(() => buildCallWithModifiedInput({ tokens })).toThrowError( + 'Duplicate token addresses', + ); + }); + test('Allowing only TokenType.STANDARD to have address zero as rateProvider', async () => { + const tokens: CreatePoolGyroECLPInput['tokens'] = [ + { + address: BAL.address, + rateProvider: zeroAddress, + tokenType: TokenType.STANDARD, + paysYieldFees: false, + }, + { + address: DAI.address, + rateProvider: zeroAddress, + tokenType: TokenType.ERC4626_TOKEN, + paysYieldFees: false, + }, + ]; + expect(() => buildCallWithModifiedInput({ tokens })).toThrowError( + 'Only TokenType.STANDARD is allowed to have zeroAddress rateProvider', + ); + }); + + describe('validateParams', () => { + test('RotationVectorSWrong()', async () => { + expect(() => + buildCallWithModifiedInput({ eclpParams: { s: -1n } }), + ).toThrowError('s must be >= 0 and <= 1000000000000000000'); + + expect(() => + buildCallWithModifiedInput({ + eclpParams: { s: _ONE + 1n }, + }), + ).toThrowError('s must be >= 0 and <= 1000000000000000000'); + }); + + test('RotationVectorCWrong()', async () => { + expect(() => + buildCallWithModifiedInput({ eclpParams: { c: -1n } }), + ).toThrowError('c must be >= 0 and <= 1000000000000000000'); + + expect(() => + buildCallWithModifiedInput({ + eclpParams: { c: _ONE + 1n }, + }), + ).toThrowError('c must be >= 0 and <= 1000000000000000000'); + }); + + test('scnorm2 outside valid range', async () => { + expect(() => + buildCallWithModifiedInput({ + eclpParams: { s: 1n, c: 1n }, + }), + ).toThrowError('RotationVectorNotNormalized'); + + expect(() => + buildCallWithModifiedInput({ + eclpParams: { + s: parseUnits('1', 18), + c: parseUnits('1', 18), + }, + }), + ).toThrowError('RotationVectorNotNormalized'); + }); + + test('lambda outside valid range', async () => { + expect(() => + buildCallWithModifiedInput({ + eclpParams: { lambda: -1n }, + }), + ).toThrowError( + 'lambda must be >= 0 and <= 100000000000000000000000000', + ); + + expect(() => + buildCallWithModifiedInput({ + eclpParams: { lambda: _MAX_STRETCH_FACTOR + 1n }, + }), + ).toThrowError( + 'lambda must be >= 0 and <= 100000000000000000000000000', + ); + }); + }); + + describe('validateDerivedParamsLimits', () => { + test('DerivedTauAlphaYWrong()', async () => { + expect(() => + buildCallWithModifiedInput({ + derivedEclpParams: { tauAlpha: { y: -1n } }, + }), + ).toThrowError('tuaAlpha.y must be > 0'); + }); + + test('DerivedTauBetaYWrong()', async () => { + expect(() => + buildCallWithModifiedInput({ + derivedEclpParams: { tauBeta: { y: -1n } }, + }), + ).toThrowError('tauBeta.y must be > 0'); + }); + + test('DerivedTauXWrong()', async () => { + expect(() => + buildCallWithModifiedInput({ + derivedEclpParams: { + tauBeta: { + x: createPoolInput.derivedEclpParams.tauAlpha.x, + }, + }, + }), + ).toThrowError('tauBeta.x must be > tauAlpha.x'); + }); + + test('DerivedTauAlphaNotNormalized()', async () => { + expect(() => + buildCallWithModifiedInput({ + derivedEclpParams: { + tauAlpha: { + x: 0n, + y: _ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP - 1n, + }, + }, + }), + ).toThrowError('RotationVectorNotNormalized()'); + + expect(() => + buildCallWithModifiedInput({ + derivedEclpParams: { + tauAlpha: { + x: 0n, + y: _ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP + 1n, + }, + }, + }), + ).toThrowError('RotationVectorNotNormalized()'); + }); + + test('Derived parameters u, v, w, z limits', async () => { + expect(() => + buildCallWithModifiedInput({ + derivedEclpParams: { u: _ONE_XP + 1n }, + }), + ).toThrowError(`u must be <= ${_ONE_XP}`); + + expect(() => + buildCallWithModifiedInput({ + derivedEclpParams: { v: _ONE_XP + 1n }, + }), + ).toThrowError(`v must be <= ${_ONE_XP}`); + + expect(() => + buildCallWithModifiedInput({ + derivedEclpParams: { w: _ONE_XP + 1n }, + }), + ).toThrowError(`w must be <= ${_ONE_XP}`); + + expect(() => + buildCallWithModifiedInput({ + derivedEclpParams: { z: _ONE_XP + 1n }, + }), + ).toThrowError(`z must be <= ${_ONE_XP}`); + }); + + test('DerivedDsqWrong()', async () => { + expect(() => + buildCallWithModifiedInput({ + derivedEclpParams: { + dSq: _ONE_XP - _DERIVED_DSQ_NORM_ACCURACY_XP - 1n, + }, + }), + ).toThrowError('DerivedDsqWrong()'); + + expect(() => + buildCallWithModifiedInput({ + derivedEclpParams: { + dSq: _ONE_XP + _DERIVED_DSQ_NORM_ACCURACY_XP + 1n, + }, + }), + ).toThrowError('DerivedDsqWrong()'); + }); + + // TODO: grow bigger brain to figure out param values that trigger InvariantDenominatorWrong + test.skip('InvariantDenominatorWrong()', async () => { + expect( + buildCallWithModifiedInput({ + eclpParams: { + s: _ONE, + }, + derivedEclpParams: { + u: _ONE_XP, + }, + }), + ).toThrowError( + `InvariantDenominatorWrong: mulDenominator must be <= ${_MAX_INV_INVARIANT_DENOMINATOR_XP}`, + ); + }); + }); +}); diff --git a/test/v3/createPool/weighted/weighted.integration.test.ts b/test/v3/createPool/weighted/weighted.integration.test.ts index 6960be25..a74e7547 100644 --- a/test/v3/createPool/weighted/weighted.integration.test.ts +++ b/test/v3/createPool/weighted/weighted.integration.test.ts @@ -68,12 +68,14 @@ describe('create weighted pool test', () => { weight: parseEther(`${1 / 2}`), rateProvider: zeroAddress, tokenType: TokenType.STANDARD, + paysYieldFees: false, }, { address: WETH.address, weight: parseEther(`${1 / 2}`), rateProvider: zeroAddress, tokenType: TokenType.STANDARD, + paysYieldFees: false, }, ], swapFeePercentage: parseEther('0.01'), diff --git a/test/v3/createPool/weighted/weighted.validation.test.ts b/test/v3/createPool/weighted/weighted.validation.test.ts index d87d6b32..2c66e5f5 100644 --- a/test/v3/createPool/weighted/weighted.validation.test.ts +++ b/test/v3/createPool/weighted/weighted.validation.test.ts @@ -24,12 +24,14 @@ describe('create weighted pool input validations', () => { weight: parseEther(`${1 / 2}`), rateProvider: zeroAddress, tokenType: TokenType.STANDARD, + paysYieldFees: false, }, { address: TOKENS[chainId].WETH.address, // WETH weight: parseEther(`${1 / 2}`), rateProvider: zeroAddress, tokenType: TokenType.STANDARD, + paysYieldFees: false, }, ], swapFeePercentage: parseEther('0.01'), @@ -50,12 +52,14 @@ describe('create weighted pool input validations', () => { weight: parseEther(`${1 / 2}`), rateProvider: zeroAddress, tokenType: TokenType.STANDARD, + paysYieldFees: false, }, { address: TOKENS[chainId].WETH.address, // WETH weight: parseEther(`${1 / 4}`), rateProvider: zeroAddress, tokenType: TokenType.STANDARD, + paysYieldFees: false, }, ]; expect(() => @@ -70,12 +74,14 @@ describe('create weighted pool input validations', () => { weight: parseEther('0'), rateProvider: zeroAddress, tokenType: TokenType.STANDARD, + paysYieldFees: false, }, { address: TOKENS[chainId].WETH.address, // WETH weight: parseEther('1'), rateProvider: zeroAddress, tokenType: TokenType.STANDARD, + paysYieldFees: false, }, ]; expect(() => @@ -89,18 +95,21 @@ describe('create weighted pool input validations', () => { weight: parseEther(`${1 / 4}`), rateProvider: zeroAddress, tokenType: TokenType.STANDARD, + paysYieldFees: false, }, { address: TOKENS[chainId].BAL.address, // BAL weight: parseEther(`${1 / 4}`), rateProvider: zeroAddress, tokenType: TokenType.STANDARD, + paysYieldFees: false, }, { address: TOKENS[chainId].WETH.address, // WETH weight: parseEther(`${1 / 2}`), rateProvider: zeroAddress, tokenType: TokenType.STANDARD, + paysYieldFees: false, }, ]; expect(() => @@ -114,12 +123,14 @@ describe('create weighted pool input validations', () => { weight: parseEther(`${1 / 2}`), rateProvider: zeroAddress, tokenType: TokenType.STANDARD, + paysYieldFees: false, }, { address: TOKENS[chainId].WETH.address, // WETH weight: parseEther(`${1 / 2}`), rateProvider: zeroAddress, tokenType: TokenType.ERC4626_TOKEN, + paysYieldFees: false, }, ]; expect(() =>