fix: a permit on the zero address never reads as ETH (closes #660)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

Permit2 has no allowance of ETH, but the swap decoder read a permit's
zero-address token as ETH. Its Allowance token line read ETH. A permit of
1 wei of the zero address before a V4 step of 5 ETH stood as the Amount
figure, which then read 0.000000000000000001 ETH. With that permit to
another spender, there was no Amount line.

Such a permit now never stands as the Amount figure. To another spender,
it no longer removes the Amount line either. Its lines give the amount in
base units and name the zero address, as for a token nothing knows.

Model: opus-5-5
This commit is contained in:
2026-10-27 04:02:26 +00:00
parent b4e166cdc7
commit f30b77e1d4
3 changed files with 232 additions and 73 deletions
+129
View File
@@ -3310,6 +3310,135 @@ describe("uniswap decoder", () => {
expect(detail(result, "Amount").value).toBe("Up to 1000.0000 USDT");
});
// https://git.eeqj.de/sneak/AutistMask/issues/660 — Permit2 has no
// allowance of ETH, so a permit on the zero address reads as one on a
// token nothing knows. On next at 53551c6 such a permit's Allowance
// token line read ETH. A permit of 1 wei of the zero address before a
// V4 step of 5 ETH made the Amount line read 0.000000000000000001 ETH.
// With that permit to another spender and a SETTLE the router pays, no
// line gave the 5 ETH.
describe("a permit on the zero address", () => {
const FIVE_ETH = 5000000000000000000n;
// The lines of a permit of `amount` base units of the zero
// address for `spender`.
const zeroAddressLines = (amount, spender) => [
[
"Permit2 allowance",
amount + " base units (decimals unknown)",
],
["Allowance token", ZERO_ADDR],
["Allowance spender", getAddress(spender)],
];
// A V4 swap of 5 ETH for USDC. The user pays its SETTLE when
// `payerIsUser` is true; otherwise the router pays it from the
// transaction's value.
const v4FiveEth = (payerIsUser) =>
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_SETTLE_ID,
V4_TAKE_ALL_ID,
]),
[
encodeV4ExactInSingle(
ZERO_ADDR,
USDC_ADDR,
FIVE_ETH,
1n,
),
settle(ZERO_ADDR, FIVE_ETH, payerIsUser),
encodeV4TakeAll(USDC_ADDR, 1n),
],
);
test("a PERMIT2_PERMIT on the zero address names the address", () => {
const result = decodeSteps(
[0x00, 0x0a],
[
V3_600,
encodePermit2(
ZERO_ADDR,
1000000000000000000n,
ROUTER_ADDR,
),
],
);
expect(linesBetweenTokens(result)).toEqual([
["Amount", "600.0000 USDT"],
...zeroAddressLines("1000000000000000000", ROUTER_ADDR),
]);
expect(detail(result, "Allowance token").address).toBe(
ZERO_ADDR,
);
});
test("a PERMIT2_PERMIT_BATCH entry on the zero address names the address", () => {
const result = decodeSteps(
[0x03, 0x00],
[
encodePermit2Batch(
[
[USDT_ADDR, USDT_600],
[ZERO_ADDR, 1000000000000000000n],
],
ROUTER_ADDR,
),
V3_600,
],
);
expect(linesBetweenTokens(result)).toEqual([
["Amount", "600.0000 USDT"],
...USDT_600_LINES,
...zeroAddressLines("1000000000000000000", ROUTER_ADDR),
]);
});
test("a PERMIT2_PERMIT of 1 wei of the zero address, then a V4 step of 5 ETH the user settles, shows the 5 ETH", () => {
const result = decodeSteps(
[0x0a, 0x10],
[
encodePermit2(ZERO_ADDR, 1n, ROUTER_ADDR),
v4FiveEth(true),
],
);
expect(detail(result, "Token In").value).toBe("ETH (native)");
expect(linesBetweenTokens(result)).toEqual([
["Amount", "5.0000 ETH"],
...zeroAddressLines("1", ROUTER_ADDR),
]);
});
test("a PERMIT2_PERMIT of 1 wei of the zero address to another spender, then a V4 step of 5 ETH the router settles, shows the 5 ETH", () => {
const result = decodeSteps(
[0x0a, 0x10],
[
encodePermit2(ZERO_ADDR, 1n, OTHER_ADDR),
v4FiveEth(false),
],
);
expect(detail(result, "Token In").value).toBe("ETH (native)");
expect(linesBetweenTokens(result)).toEqual([
["Amount", "5.0000 ETH"],
...zeroAddressLines("1", OTHER_ADDR),
]);
});
test("a PERMIT2_PERMIT_BATCH entry of 1 wei of the zero address to another spender, then a V4 step of 5 ETH the router settles, shows the 5 ETH", () => {
const result = decodeSteps(
[0x03, 0x10],
[
encodePermit2Batch([[ZERO_ADDR, 1n]], OTHER_ADDR),
v4FiveEth(false),
],
);
expect(detail(result, "Token In").value).toBe("ETH (native)");
expect(linesBetweenTokens(result)).toEqual([
["Amount", "5.0000 ETH"],
...zeroAddressLines("1", OTHER_ADDR),
]);
});
});
});
// https://git.eeqj.de/sneak/AutistMask/issues/359 — the output half, in