Skip to content

Commit 31d069c

Browse files
committed
test(slippage): regression tests for thena_cl/algebra_v3/balancer_v3
Pin the SSOT 7.3 invariants for the three remaining adapters in the F1 fix. Mirrors the pattern from uniswap_v3.test.ts: - thena_cl.test.ts (4 tests) - buildSwap rejects without amount_out_min (no quoter wired). - buildSwap forwards amount_out_min override verbatim. - buildRemoveLiquidity rejects without amount_{a,b}_min. - buildRemoveLiquidity sorts overrides onto amount{0,1}Min. - balancer_v3.test.ts (2 tests) - buildSwap rejects without amount_out_min. - buildSwap forwards amount_out_min override verbatim. - algebra_v3.test.ts (4 tests) - buildSwap honors amount_out_min override (skips quote path). - buildAddLiquidity defaults to 50 bps slippage on both axes (uses NEST-style v2 mint via pool_deployer flag). - buildRemoveLiquidity rejects without amount_{a,b}_min. - buildRemoveLiquidity sorts overrides onto amount{0,1}Min. Total monorepo test count: 78 -> 95 (+17 across the F1 PR — uniswap_v3 contributed 7 in an earlier commit; this one adds 10 more). Verified: 95/95 tests pass, lint clean across 3 packages.
1 parent 0d86ecd commit 31d069c

3 files changed

Lines changed: 289 additions & 0 deletions

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Slippage-protection regression tests for AlgebraV3Adapter (SSOT 7.3).
2+
//
3+
// The buildSwap auto-quote path is exercised against a live RPC in
4+
// integration tests; here we cover the explicit override path and the
5+
// LP-side guards that don't require an RPC.
6+
import { ProtocolCategory } from "@hypurrquant/defi-core";
7+
import type { ProtocolEntry } from "@hypurrquant/defi-core";
8+
import { decodeFunctionData, parseAbi, type Address, type Hex } from "viem";
9+
import { describe, expect, it } from "vitest";
10+
11+
import { AlgebraV3Adapter } from "./algebra_v3.js";
12+
13+
const LOW = ("0x" + "01".repeat(20)) as Address;
14+
const HIGH = ("0x" + "ff".repeat(20)) as Address;
15+
const RECIPIENT = ("0x" + "be".repeat(20)) as Address;
16+
17+
const ENTRY: ProtocolEntry = {
18+
name: "test_algebra_v3",
19+
slug: "test-algebra-v3",
20+
category: ProtocolCategory.Dex,
21+
interface: "algebra_v3",
22+
chain: "test",
23+
contracts: {
24+
router: ("0x" + "01".repeat(20)) as Address,
25+
quoter: ("0x" + "02".repeat(20)) as Address,
26+
position_manager: ("0x" + "03".repeat(20)) as Address,
27+
factory: ("0x" + "04".repeat(20)) as Address,
28+
},
29+
};
30+
31+
const swapAbi = parseAbi([
32+
"function exactInputSingle((address tokenIn, address tokenOut, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 limitSqrtPrice) params) returns (uint256)",
33+
]);
34+
35+
const v2MintAbi = parseAbi([
36+
"function mint((address token0, address token1, int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired, uint256 amount0Min, uint256 amount1Min, address recipient, uint256 deadline) params) returns (uint256, uint128, uint256, uint256)",
37+
]);
38+
39+
const decreaseAbi = parseAbi([
40+
"function decreaseLiquidity((uint256 tokenId, uint128 liquidity, uint256 amount0Min, uint256 amount1Min, uint256 deadline) params) returns (uint256, uint256)",
41+
]);
42+
43+
const multicallAbi = parseAbi(["function multicall(bytes[] data) returns (bytes[])"]);
44+
45+
interface SwapTuple { amountOutMinimum: bigint }
46+
interface MintTuple { amount0Min: bigint; amount1Min: bigint }
47+
interface DecreaseTuple { amount0Min: bigint; amount1Min: bigint }
48+
49+
describe("AlgebraV3Adapter slippage protection (SSOT 7.3)", () => {
50+
it("buildSwap honors amount_out_min override (skips the quote path)", async () => {
51+
const adapter = new AlgebraV3Adapter(ENTRY);
52+
const tx = await adapter.buildSwap({
53+
protocol: ENTRY.slug,
54+
token_in: LOW,
55+
token_out: HIGH,
56+
amount_in: 1_000_000n,
57+
slippage: { bps: 50 },
58+
recipient: RECIPIENT,
59+
amount_out_min: 12_345n,
60+
});
61+
const decoded = decodeFunctionData({ abi: swapAbi, data: tx.data as Hex });
62+
const params = decoded.args[0] as SwapTuple;
63+
expect(params.amountOutMinimum).toBe(12_345n);
64+
expect(params.amountOutMinimum).not.toBe(0n);
65+
});
66+
67+
it("buildAddLiquidity defaults to 50 bps slippage on both axes", async () => {
68+
// useSingleQuoter is gated on `pool_deployer` (NEST-style Algebra
69+
// V2). Add it to land in the v2 mint branch whose ABI matches
70+
// v2MintAbi below. The Integral branch (KittenSwap) ships a
71+
// `deployer` field and is exercised separately by integration tests.
72+
const v2Entry: ProtocolEntry = {
73+
...ENTRY,
74+
contracts: {
75+
...(ENTRY.contracts as Record<string, Address>),
76+
pool_deployer: ("0x" + "05".repeat(20)) as Address,
77+
},
78+
};
79+
const adapter = new AlgebraV3Adapter(v2Entry);
80+
const tx = await adapter.buildAddLiquidity({
81+
protocol: v2Entry.slug,
82+
token_a: LOW,
83+
token_b: HIGH,
84+
amount_a: 1_000_000n,
85+
amount_b: 2_000_000n,
86+
recipient: RECIPIENT,
87+
});
88+
const decoded = decodeFunctionData({ abi: v2MintAbi, data: tx.data as Hex });
89+
const params = decoded.args[0] as MintTuple;
90+
// 50 bps = 0.5% → minimum = desired * 9950 / 10000.
91+
expect(params.amount0Min).toBe(995_000n);
92+
expect(params.amount1Min).toBe(1_990_000n);
93+
});
94+
95+
it("buildRemoveLiquidity refuses to ship without amount_{a,b}_min", async () => {
96+
const adapter = new AlgebraV3Adapter(ENTRY);
97+
await expect(
98+
adapter.buildRemoveLiquidity({
99+
protocol: ENTRY.slug,
100+
token_a: LOW,
101+
token_b: HIGH,
102+
liquidity: 100n,
103+
recipient: RECIPIENT,
104+
token_id: 1n,
105+
}),
106+
).rejects.toThrow(/amount_a_min and amount_b_min/);
107+
});
108+
109+
it("buildRemoveLiquidity sorts amount_{a,b}_min onto amount{0,1}Min", async () => {
110+
const adapter = new AlgebraV3Adapter(ENTRY);
111+
const tx = await adapter.buildRemoveLiquidity({
112+
protocol: ENTRY.slug,
113+
token_a: LOW,
114+
token_b: HIGH,
115+
liquidity: 100n,
116+
recipient: RECIPIENT,
117+
token_id: 7n,
118+
amount_a_min: 11n,
119+
amount_b_min: 22n,
120+
});
121+
const outer = decodeFunctionData({ abi: multicallAbi, data: tx.data as Hex });
122+
const decreaseData = (outer.args[0] as readonly Hex[])[0];
123+
const decoded = decodeFunctionData({ abi: decreaseAbi, data: decreaseData });
124+
const params = decoded.args[0] as DecreaseTuple;
125+
expect(params.amount0Min).toBe(11n);
126+
expect(params.amount1Min).toBe(22n);
127+
});
128+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Slippage-protection regression test for BalancerV3Adapter (SSOT 7.3).
2+
import { ProtocolCategory } from "@hypurrquant/defi-core";
3+
import type { ProtocolEntry } from "@hypurrquant/defi-core";
4+
import { decodeFunctionData, parseAbi, type Address, type Hex } from "viem";
5+
import { describe, expect, it } from "vitest";
6+
7+
import { BalancerV3Adapter } from "./balancer_v3.js";
8+
9+
const LOW = ("0x" + "01".repeat(20)) as Address;
10+
const HIGH = ("0x" + "ff".repeat(20)) as Address;
11+
const RECIPIENT = ("0x" + "be".repeat(20)) as Address;
12+
13+
const ENTRY: ProtocolEntry = {
14+
name: "test_balancer_v3",
15+
slug: "test-balancer-v3",
16+
category: ProtocolCategory.Dex,
17+
interface: "balancer_v3",
18+
chain: "test",
19+
contracts: { router: ("0x" + "01".repeat(20)) as Address },
20+
};
21+
22+
const swapAbi = parseAbi([
23+
"function swapSingleTokenExactIn(address pool, address tokenIn, address tokenOut, uint256 exactAmountIn, uint256 minAmountOut, uint256 deadline, bool wethIsEth, bytes userData) returns (uint256)",
24+
]);
25+
26+
describe("BalancerV3Adapter slippage protection (SSOT 7.3)", () => {
27+
it("buildSwap refuses to ship without amount_out_min", async () => {
28+
const adapter = new BalancerV3Adapter(ENTRY);
29+
await expect(
30+
adapter.buildSwap({
31+
protocol: ENTRY.slug,
32+
token_in: LOW,
33+
token_out: HIGH,
34+
amount_in: 1_000_000n,
35+
slippage: { bps: 50 },
36+
recipient: RECIPIENT,
37+
}),
38+
).rejects.toThrow(/amount_out_min/);
39+
});
40+
41+
it("buildSwap forwards amount_out_min override verbatim", async () => {
42+
const adapter = new BalancerV3Adapter(ENTRY);
43+
const tx = await adapter.buildSwap({
44+
protocol: ENTRY.slug,
45+
token_in: LOW,
46+
token_out: HIGH,
47+
amount_in: 1_000_000n,
48+
slippage: { bps: 50 },
49+
recipient: RECIPIENT,
50+
amount_out_min: 99_999n,
51+
});
52+
const decoded = decodeFunctionData({ abi: swapAbi, data: tx.data as Hex });
53+
// args order: pool, tokenIn, tokenOut, exactAmountIn, minAmountOut, ...
54+
expect((decoded.args as readonly unknown[])[4]).toBe(99_999n);
55+
expect((decoded.args as readonly unknown[])[4]).not.toBe(0n);
56+
});
57+
});
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Slippage-protection regression tests for ThenaCLAdapter (SSOT 7.3).
2+
import { ProtocolCategory } from "@hypurrquant/defi-core";
3+
import type { ProtocolEntry } from "@hypurrquant/defi-core";
4+
import { decodeFunctionData, parseAbi, type Address, type Hex } from "viem";
5+
import { describe, expect, it } from "vitest";
6+
7+
import { ThenaCLAdapter } from "./thena_cl.js";
8+
9+
const LOW = ("0x" + "01".repeat(20)) as Address;
10+
const HIGH = ("0x" + "ff".repeat(20)) as Address;
11+
const RECIPIENT = ("0x" + "be".repeat(20)) as Address;
12+
13+
const ENTRY: ProtocolEntry = {
14+
name: "test_thena_cl",
15+
slug: "test-thena-cl",
16+
category: ProtocolCategory.Dex,
17+
interface: "thena_cl",
18+
chain: "test",
19+
contracts: {
20+
router: ("0x" + "01".repeat(20)) as Address,
21+
position_manager: ("0x" + "03".repeat(20)) as Address,
22+
pool_factory: ("0x" + "04".repeat(20)) as Address,
23+
},
24+
};
25+
26+
const swapAbi = parseAbi([
27+
"function exactInputSingle((address tokenIn, address tokenOut, int24 tickSpacing, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) returns (uint256)",
28+
]);
29+
30+
const decreaseAbi = parseAbi([
31+
"function decreaseLiquidity((uint256 tokenId, uint128 liquidity, uint256 amount0Min, uint256 amount1Min, uint256 deadline) params) returns (uint256, uint256)",
32+
]);
33+
34+
const multicallAbi = parseAbi(["function multicall(bytes[] data) returns (bytes[])"]);
35+
36+
interface SwapTuple { amountOutMinimum: bigint; amountIn: bigint }
37+
interface DecreaseTuple { amount0Min: bigint; amount1Min: bigint }
38+
39+
describe("ThenaCLAdapter slippage protection (SSOT 7.3)", () => {
40+
it("buildSwap refuses to ship without amount_out_min (no quoter wired)", async () => {
41+
const adapter = new ThenaCLAdapter(ENTRY);
42+
await expect(
43+
adapter.buildSwap({
44+
protocol: ENTRY.slug,
45+
token_in: LOW,
46+
token_out: HIGH,
47+
amount_in: 1_000_000n,
48+
slippage: { bps: 50 },
49+
recipient: RECIPIENT,
50+
}),
51+
).rejects.toThrow(/amount_out_min/);
52+
});
53+
54+
it("buildSwap forwards amount_out_min override verbatim", async () => {
55+
const adapter = new ThenaCLAdapter(ENTRY);
56+
const tx = await adapter.buildSwap({
57+
protocol: ENTRY.slug,
58+
token_in: LOW,
59+
token_out: HIGH,
60+
amount_in: 1_000_000n,
61+
slippage: { bps: 50 },
62+
recipient: RECIPIENT,
63+
amount_out_min: 12_345n,
64+
});
65+
const decoded = decodeFunctionData({ abi: swapAbi, data: tx.data as Hex });
66+
const params = decoded.args[0] as SwapTuple;
67+
expect(params.amountOutMinimum).toBe(12_345n);
68+
expect(params.amountOutMinimum).not.toBe(0n);
69+
});
70+
71+
it("buildRemoveLiquidity refuses to ship without amount_{a,b}_min", async () => {
72+
const adapter = new ThenaCLAdapter(ENTRY);
73+
await expect(
74+
adapter.buildRemoveLiquidity({
75+
protocol: ENTRY.slug,
76+
token_a: LOW,
77+
token_b: HIGH,
78+
liquidity: 100n,
79+
recipient: RECIPIENT,
80+
token_id: 1n,
81+
}),
82+
).rejects.toThrow(/amount_a_min and amount_b_min/);
83+
});
84+
85+
it("buildRemoveLiquidity sorts amount_{a,b}_min onto amount{0,1}Min", async () => {
86+
const adapter = new ThenaCLAdapter(ENTRY);
87+
const tx = await adapter.buildRemoveLiquidity({
88+
protocol: ENTRY.slug,
89+
token_a: LOW,
90+
token_b: HIGH,
91+
liquidity: 100n,
92+
recipient: RECIPIENT,
93+
token_id: 7n,
94+
amount_a_min: 11n,
95+
amount_b_min: 22n,
96+
});
97+
const outer = decodeFunctionData({ abi: multicallAbi, data: tx.data as Hex });
98+
const decreaseData = (outer.args[0] as readonly Hex[])[0];
99+
const decoded = decodeFunctionData({ abi: decreaseAbi, data: decreaseData });
100+
const params = decoded.args[0] as DecreaseTuple;
101+
expect(params.amount0Min).toBe(11n);
102+
expect(params.amount1Min).toBe(22n);
103+
});
104+
});

0 commit comments

Comments
 (0)