diff --git a/ts/packages/defi-protocols/src/dex/balancer_v3.test.ts b/ts/packages/defi-protocols/src/dex/balancer_v3.test.ts index 9eb05bd..b82bfbf 100644 --- a/ts/packages/defi-protocols/src/dex/balancer_v3.test.ts +++ b/ts/packages/defi-protocols/src/dex/balancer_v3.test.ts @@ -10,13 +10,19 @@ const LOW = ("0x" + "01".repeat(20)) as Address; const HIGH = ("0x" + "ff".repeat(20)) as Address; const RECIPIENT = ("0x" + "be".repeat(20)) as Address; +const ROUTER = ("0x" + "01".repeat(20)) as Address; +const POOL = ("0x" + "ab".repeat(20)) as Address; const ENTRY: ProtocolEntry = { name: "test_balancer_v3", slug: "test-balancer-v3", category: ProtocolCategory.Dex, interface: "balancer_v3", chain: "test", - contracts: { router: ("0x" + "01".repeat(20)) as Address }, + contracts: { router: ROUTER, pool: POOL }, +}; +const ENTRY_NO_POOL: ProtocolEntry = { + ...ENTRY, + contracts: { router: ROUTER }, }; const swapAbi = parseAbi([ @@ -38,7 +44,22 @@ describe("BalancerV3Adapter slippage protection (SSOT 7.3)", () => { ).rejects.toThrow(/amount_out_min/); }); - it("buildSwap forwards amount_out_min override verbatim", async () => { + it("buildSwap refuses to ship without a registered pool address", async () => { + const adapter = new BalancerV3Adapter(ENTRY_NO_POOL); + await expect( + adapter.buildSwap({ + protocol: ENTRY_NO_POOL.slug, + token_in: LOW, + token_out: HIGH, + amount_in: 1_000_000n, + slippage: { bps: 50 }, + recipient: RECIPIENT, + amount_out_min: 1n, + }), + ).rejects.toThrow(/pool address/); + }); + + it("buildSwap forwards amount_out_min override verbatim and uses registered pool", async () => { const adapter = new BalancerV3Adapter(ENTRY); const tx = await adapter.buildSwap({ protocol: ENTRY.slug, @@ -51,7 +72,9 @@ describe("BalancerV3Adapter slippage protection (SSOT 7.3)", () => { }); const decoded = decodeFunctionData({ abi: swapAbi, data: tx.data as Hex }); // args order: pool, tokenIn, tokenOut, exactAmountIn, minAmountOut, ... - expect((decoded.args as readonly unknown[])[4]).toBe(99_999n); - expect((decoded.args as readonly unknown[])[4]).not.toBe(0n); + const args = decoded.args as readonly unknown[]; + expect(String(args[0]).toLowerCase()).toBe(POOL.toLowerCase()); + expect(args[4]).toBe(99_999n); + expect(args[4]).not.toBe(0n); }); }); diff --git a/ts/packages/defi-protocols/src/dex/balancer_v3.ts b/ts/packages/defi-protocols/src/dex/balancer_v3.ts index 029b355..bb121ab 100644 --- a/ts/packages/defi-protocols/src/dex/balancer_v3.ts +++ b/ts/packages/defi-protocols/src/dex/balancer_v3.ts @@ -1,4 +1,4 @@ -import { encodeFunctionData, parseAbi, zeroAddress } from "viem"; +import { encodeFunctionData, parseAbi } from "viem"; import { DefiError } from "@hypurrquant/defi-core"; import type { @@ -19,6 +19,7 @@ const abi = parseAbi([ export class BalancerV3Adapter implements IDex { private readonly protocolName: string; private readonly router: `0x${string}`; + private readonly pool: `0x${string}` | undefined; constructor(entry: ProtocolEntry, _rpcUrl?: string) { this.protocolName = entry.name; @@ -27,6 +28,7 @@ export class BalancerV3Adapter implements IDex { throw new DefiError("CONTRACT_ERROR", "Missing 'router' contract"); } this.router = router; + this.pool = entry.contracts?.["pool"] as `0x${string}` | undefined; } name(): string { @@ -43,16 +45,22 @@ export class BalancerV3Adapter implements IDex { `floor off-chain (e.g. via the Vault's static-call simulation) and pass it explicitly.`, ); } + if (!this.pool) { + throw DefiError.invalidParam( + `[${this.protocolName}] buildSwap requires a pool address. Register the pool under ` + + `[protocol.contracts] as \`pool = "0x..."\` in the protocol's TOML config. ` + + `Multi-pool routing is intentionally not implemented in this adapter — for that, ` + + `quote against the Balancer V3 BatchRouter off-chain and route via a different surface.`, + ); + } const minAmountOut = params.amount_out_min; const deadline = BigInt(params.deadline ?? 18446744073709551615n); - // Balancer V3 requires a pool address. For now use a simplified single-pool swap. - // In production, the pool would be resolved from the registry or an on-chain query. const data = encodeFunctionData({ abi, functionName: "swapSingleTokenExactIn", args: [ - zeroAddress, // TODO: resolve pool from registry + this.pool, params.token_in, params.token_out, params.amount_in, @@ -64,7 +72,7 @@ export class BalancerV3Adapter implements IDex { }); return { - description: `[${this.protocolName}] Swap ${params.amount_in} via Balancer V3`, + description: `[${this.protocolName}] Swap ${params.amount_in} via Balancer V3 pool ${this.pool}`, to: this.router, data, value: 0n,