fix: a V4 swap action not in the deployed shape is shown as not decoded (closes #358)
The four V4 swap actions are read only as the parameter structs deployed on mainnet and Sepolia. ethers decoded the newer v4-periphery structs, which add minHopPriceX36, as the deployed ones without an error, and the newer multi-hop exact-in struct showed the offset of its hop-price array as the swap amount. A decode now counts only when encoding it again gives back the same bytes. When a swap action fails that, the Steps line lists its step as "V4 Swap (could not be decoded)", so the screen no longer looks complete without it. The README records the pinned shape. Model: opus-5-5
This commit is contained in:
+143
-3
@@ -163,6 +163,110 @@ function encodeV4ExactInSingle(currency0, currency1, amountIn, amountOutMin) {
|
||||
);
|
||||
}
|
||||
|
||||
// Each V4 swap action's parameters in two shapes: the struct V4Router has
|
||||
// deployed on mainnet and Sepolia, and the one v4-periphery `main` has since
|
||||
// moved to, which adds a `minHopPriceX36` field. Each swaps WETH for USDC.
|
||||
const POOL_KEY = "tuple(address,address,uint24,int24,address)";
|
||||
const PATH_KEY = "tuple(address,uint24,int24,address,bytes)";
|
||||
const WETH_USDC_POOL = [WETH_ADDR, USDC_ADDR, 100, 1, ZERO_ADDR];
|
||||
const ONE_WETH = 1000000000000000000n;
|
||||
const V4_SWAP_ACTION_SHAPES = [
|
||||
{
|
||||
action: "SWAP_EXACT_IN_SINGLE",
|
||||
id: 0x06,
|
||||
deployed: coder.encode(
|
||||
[`tuple(${POOL_KEY},bool,uint128,uint128,bytes)`],
|
||||
[[WETH_USDC_POOL, true, ONE_WETH, 990000n, "0x"]],
|
||||
),
|
||||
newer: coder.encode(
|
||||
[`tuple(${POOL_KEY},bool,uint128,uint128,uint256,bytes)`],
|
||||
[[WETH_USDC_POOL, true, ONE_WETH, 990000n, 0n, "0x"]],
|
||||
),
|
||||
},
|
||||
{
|
||||
action: "SWAP_EXACT_IN",
|
||||
id: 0x07,
|
||||
deployed: coder.encode(
|
||||
[`tuple(address,${PATH_KEY}[],uint128,uint128)`],
|
||||
[
|
||||
[
|
||||
WETH_ADDR,
|
||||
[[USDC_ADDR, 100, 1, ZERO_ADDR, "0x"]],
|
||||
ONE_WETH,
|
||||
990000n,
|
||||
],
|
||||
],
|
||||
),
|
||||
newer: coder.encode(
|
||||
[`tuple(address,${PATH_KEY}[],uint256[],uint128,uint128)`],
|
||||
[
|
||||
[
|
||||
WETH_ADDR,
|
||||
[[USDC_ADDR, 100, 1, ZERO_ADDR, "0x"]],
|
||||
[],
|
||||
ONE_WETH,
|
||||
990000n,
|
||||
],
|
||||
],
|
||||
),
|
||||
},
|
||||
{
|
||||
action: "SWAP_EXACT_OUT_SINGLE",
|
||||
id: 0x08,
|
||||
deployed: coder.encode(
|
||||
[`tuple(${POOL_KEY},bool,uint128,uint128,bytes)`],
|
||||
[[WETH_USDC_POOL, true, 990000n, ONE_WETH, "0x"]],
|
||||
),
|
||||
newer: coder.encode(
|
||||
[`tuple(${POOL_KEY},bool,uint128,uint128,uint256,bytes)`],
|
||||
[[WETH_USDC_POOL, true, 990000n, ONE_WETH, 0n, "0x"]],
|
||||
),
|
||||
},
|
||||
{
|
||||
action: "SWAP_EXACT_OUT",
|
||||
id: 0x09,
|
||||
deployed: coder.encode(
|
||||
[`tuple(address,${PATH_KEY}[],uint128,uint128)`],
|
||||
[
|
||||
[
|
||||
USDC_ADDR,
|
||||
[[WETH_ADDR, 100, 1, ZERO_ADDR, "0x"]],
|
||||
990000n,
|
||||
ONE_WETH,
|
||||
],
|
||||
],
|
||||
),
|
||||
newer: coder.encode(
|
||||
[`tuple(address,${PATH_KEY}[],uint256[],uint128,uint128)`],
|
||||
[
|
||||
[
|
||||
USDC_ADDR,
|
||||
[[WETH_ADDR, 100, 1, ZERO_ADDR, "0x"]],
|
||||
[],
|
||||
990000n,
|
||||
ONE_WETH,
|
||||
],
|
||||
],
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// Helper: execute() with one V4_SWAP step holding the swap action, then
|
||||
// SETTLE of WETH and TAKE of USDC.
|
||||
function v4SwapCalldata(actionId, swapParams) {
|
||||
return buildExecute(
|
||||
"0x10",
|
||||
[
|
||||
encodeV4Swap(new Uint8Array([actionId, V4_SETTLE_ID, V4_TAKE_ID]), [
|
||||
swapParams,
|
||||
encodeV4Settle(WETH_ADDR),
|
||||
encodeV4Take(USDC_ADDR),
|
||||
]),
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
}
|
||||
|
||||
function detail(result, label) {
|
||||
return result.details.find((d) => d.label === label);
|
||||
}
|
||||
@@ -203,11 +307,45 @@ describe("uniswap decoder", () => {
|
||||
result.details.find((d) => d.label === "Min. received").value,
|
||||
).toBe("0.0002 ETH");
|
||||
|
||||
// Real mainnet calldata is in the deployed shape, so its V4 step is
|
||||
// not said to be undecoded.
|
||||
const steps = result.details.find((d) => d.label === "Steps");
|
||||
expect(steps.value).toContain("Permit2 Permit");
|
||||
expect(steps.value).toContain("V4 Swap");
|
||||
expect(steps.value).toBe("Permit2 Permit → V4 Swap");
|
||||
});
|
||||
|
||||
test.each(V4_SWAP_ACTION_SHAPES)(
|
||||
"a V4 $action in the deployed shape is read",
|
||||
({ id, deployed }) => {
|
||||
const result = uniswap.decode(
|
||||
v4SwapCalldata(id, deployed),
|
||||
ROUTER_ADDR,
|
||||
);
|
||||
expect(detail(result, "Steps").value).toBe("V4 Swap");
|
||||
},
|
||||
);
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/358 — before it, all four
|
||||
// newer structs decoded as the deployed ones without an error, and the
|
||||
// newer SWAP_EXACT_IN showed the offset of its hop-price array as Amount
|
||||
// ("0.0000000000000004 WETH") and its amountIn as Min. received.
|
||||
test.each(V4_SWAP_ACTION_SHAPES)(
|
||||
"a V4 $action in the newer periphery shape is shown as not decoded",
|
||||
({ id, newer }) => {
|
||||
const result = uniswap.decode(
|
||||
v4SwapCalldata(id, newer),
|
||||
ROUTER_ADDR,
|
||||
);
|
||||
expect(detail(result, "Amount")).toBeUndefined();
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
expect(detail(result, "Steps").value).toBe(
|
||||
"V4 Swap (could not be decoded)",
|
||||
);
|
||||
// SETTLE and TAKE keep their own shape and still name both sides.
|
||||
expect(detail(result, "Token In").address).toBe(WETH_ADDR);
|
||||
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
|
||||
},
|
||||
);
|
||||
|
||||
test("decodes V2_SWAP_EXACT_IN with known tokens", () => {
|
||||
const data = buildExecute(
|
||||
"0x08", // V2_SWAP_EXACT_IN
|
||||
@@ -548,9 +686,11 @@ describe("uniswap decoder", () => {
|
||||
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
// The V4 swap action's parameters are empty, so that step is said to
|
||||
// be undecoded.
|
||||
const steps = result.details.find((d) => d.label === "Steps");
|
||||
expect(steps.value).toBe(
|
||||
"Permit2 Permit \u2192 V4 Swap \u2192 Unwrap WETH",
|
||||
"Permit2 Permit \u2192 V4 Swap (could not be decoded) \u2192 Unwrap WETH",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user