fix: the approval screen names the address a swap's output is paid to (closes #594)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s

The swap decoder read no recipient, so a swap paying its output to another
address still showed Min. received. It now collects the address that every
swap step, SWEEP, WRAP_ETH, UNWRAP_WETH, V4 TAKE and TAKE_ALL pays, across the
whole transaction, leaving out payments to the router itself. The minimum
reads Min. received only when some step pays the sender, none pays another
address, and no step that may pay an address went unread: a recipient that
does not decode, or a command or V4 action the decoder does not read.
Otherwise it reads Min. paid out, with a Paid to line for each other address
and one saying when who is paid is unknown. The approval and transaction
detail screens pass the sender in.

Model: opus-5-5
This commit is contained in:
2026-10-19 06:58:59 +00:00
committed by sneak
parent e6fe9b2593
commit ab86707a77
11 changed files with 1068 additions and 98 deletions
+128 -49
View File
@@ -332,7 +332,13 @@ describe("uniswap decoder", () => {
});
test("decodes first-ever AutistMask swap (PERMIT2_PERMIT + V4_SWAP)", () => {
const result = uniswap.decode(FIRST_SWAP_CALLDATA, ROUTER_ADDR, "0x1");
const result = uniswap.decode(
FIRST_SWAP_CALLDATA,
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(result).not.toBeNull();
expect(result.name).toBe("Swap USDT \u2192 ETH");
@@ -356,8 +362,10 @@ describe("uniswap decoder", () => {
expect(result.details.find((d) => d.label === "Amount").value).toBe(
"Unlimited",
);
// This fixture's TAKE parameters are one byte long, so who it pays
// cannot be read, and the minimum is not said to be received.
expect(
result.details.find((d) => d.label === "Min. received").value,
result.details.find((d) => d.label === "Min. paid out").value,
).toBe("0.0002 ETH");
// Real mainnet calldata is in the deployed shape, so its V4 step is
@@ -373,6 +381,8 @@ describe("uniswap decoder", () => {
v4SwapCalldata(id, deployed),
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(detail(result, "Steps").value).toBe("V4 Swap");
},
@@ -389,6 +399,8 @@ describe("uniswap decoder", () => {
v4SwapCalldata(id, newer),
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(detail(result, "Amount")).toBeUndefined();
expect(detail(result, "Min. received")).toBeUndefined();
@@ -419,7 +431,13 @@ describe("uniswap decoder", () => {
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(
data,
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(detail(result, "Steps").value).toBe(
"V4 Swap (could not be decoded)",
);
@@ -440,7 +458,13 @@ describe("uniswap decoder", () => {
[encodeV4Swap(new Uint8Array([id]), [newer])],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(
data,
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(detail(result, "Steps").value).toBe(
"V4 Swap (could not be decoded)",
);
@@ -470,7 +494,13 @@ describe("uniswap decoder", () => {
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(
data,
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(result.name).toBe("Uniswap Swap");
expect(detail(result, "Amount").value).toBe("2.0000 USDT");
expect(detail(result, "Token Out").value).toBe(
@@ -498,7 +528,13 @@ describe("uniswap decoder", () => {
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(
data,
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(result.name).toBe("Uniswap Swap");
expect(detail(result, "Token In").value).toBe(
"Unknown (could not be decoded)",
@@ -520,7 +556,7 @@ describe("uniswap decoder", () => {
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(detail(result, "Token Out").value).toBe(
"Unknown (could not be decoded)",
);
@@ -540,7 +576,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
expect(result.name).toBe("Swap USDT \u2192 WETH");
@@ -567,7 +603,7 @@ describe("uniswap decoder", () => {
1767225600n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result.name).toBe("Swap USDC → WETH");
expect(detail(result, "Token In").address).toBe(USDC_ADDR);
expect(detail(result, "Token Out").address).toBe(WETH_ADDR);
@@ -599,7 +635,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result.name).toBe("Swap ETH → USDC");
const amount = detail(result, "Amount");
@@ -628,7 +664,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result.name).toBe("Swap USDC → ETH");
expect(detail(result, "Amount").value).toBe("Up to 1500.0000 USDC");
expect(detail(result, "Token Out").value).toBe("ETH");
@@ -671,7 +707,13 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(
data,
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(detail(result, "Token Out").address).toBe(tokenOut);
expect(detail(result, "Min. received").value).toBe(minReceived);
},
@@ -692,7 +734,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result.name).toBe("Swap USDT → USDC");
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
expect(detail(result, "Min. received").value).toBe("1.9000 USDC");
@@ -725,7 +767,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(detail(result, "Token Out").value).toBe("ETH");
expect(detail(result, "Min. received").value).toBe("0.4000 ETH");
});
@@ -744,7 +786,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(detail(result, "Token Out").value).toBe("ETH");
expect(detail(result, "Min. received").value).toBe("0.0005 ETH");
});
@@ -769,7 +811,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(detail(result, "Token Out").value).toBe(
"Unknown (not named in the calldata)",
);
@@ -792,7 +834,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
expect(result.name).toBe("Swap USDT \u2192 WETH");
});
@@ -804,7 +846,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
const tokenIn = result.details.find((d) => d.label === "Token In");
@@ -827,12 +869,13 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
// UNWRAP_WETH means output is native ETH
expect(result.name).toBe("Swap USDT \u2192 ETH");
});
// No step pays anyone, so the minimum is not said to be received.
test("decodes BALANCE_CHECK_ERC20 for min output", () => {
const data = buildExecute(
solidityPacked(["uint8", "uint8"], [0x0b, 0x0e]),
@@ -843,10 +886,10 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
const minOut = result.details.find((d) => d.label === "Min. received");
const minOut = result.details.find((d) => d.label === "Min. paid out");
expect(minOut).toBeDefined();
expect(minOut.value).toContain("2.0000");
expect(minOut.value).toContain("USDT");
@@ -863,7 +906,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
// The V4 swap action's parameters are empty, so that step is said to
@@ -943,7 +986,13 @@ describe("uniswap decoder", () => {
[subPlan],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(
data,
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(result.name).toBe("Uniswap Swap");
expect(detail(result, "Steps").value).toBe(
"Execute Sub-Plan (its steps are not shown)",
@@ -1031,7 +1080,13 @@ describe("uniswap decoder", () => {
"$beside an EXECUTE_SUB_PLAN step does not name either side",
({ commands, inputs, steps }) => {
const data = buildExecute(commands, inputs, 9999999999n);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(
data,
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(detail(result, "Steps").value).toBe(steps);
expect(result.name).toBe("Uniswap Swap");
expect(detail(result, "Token In").value).toBe(
@@ -1046,7 +1101,13 @@ describe("uniswap decoder", () => {
);
test("shows the deadline as a UTC date and time", () => {
const result = uniswap.decode(FIRST_SWAP_CALLDATA, ROUTER_ADDR, "0x1");
const result = uniswap.decode(
FIRST_SWAP_CALLDATA,
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(detail(result, "Deadline").value).toBe("2026-02-27 08:25:51");
});
@@ -1064,7 +1125,7 @@ describe("uniswap decoder", () => {
2n ** 256n - 1n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
expect(result.name).toBe("Swap USDT \u2192 WETH");
expect(detail(result, "Deadline").value).toBe(
@@ -1079,7 +1140,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
const amount = result.details.find((d) => d.label === "Amount");
@@ -1154,7 +1215,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
// Before decodeV4Swap fix: name would be "Swap USDT → ETH" or "Uniswap Swap"
// After fix: correctly identifies both tokens from V4 sub-actions
@@ -1188,7 +1249,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
// The amount and the token it is counted in come from the same hop.
@@ -1228,7 +1289,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toBe(
@@ -1273,7 +1334,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toContain("USDC");
@@ -1302,7 +1363,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toContain("USDC");
@@ -1339,7 +1400,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
expect(detail(result, "Token In").value).toContain("WETH");
@@ -1356,9 +1417,12 @@ describe("uniswap decoder", () => {
// are written. Each is tried with three endings: SETTLE_ALL and TAKE_ALL,
// SETTLE and TAKE, and CLOSE_CURRENCY for each currency, which leaves the
// sides to the swap actions alone.
// `minLabel` is the minimum's line: with no TAKE or TAKE_ALL, no step
// pays the user, so it is not said to be received.
const V4_STEP_ENDINGS = [
{
ending: "SETTLE_ALL and TAKE_ALL",
minLabel: "Min. received",
ids: [V4_SETTLE_ALL_ID, V4_TAKE_ALL_ID],
params: [
encodeV4SettleAll(USDT_ADDR, 1000000000n),
@@ -1367,11 +1431,13 @@ describe("uniswap decoder", () => {
},
{
ending: "SETTLE and TAKE",
minLabel: "Min. received",
ids: [V4_SETTLE_ID, V4_TAKE_ID],
params: [encodeV4Settle(USDT_ADDR), encodeV4Take(USDC_ADDR)],
},
{
ending: "CLOSE_CURRENCY",
minLabel: "Min. paid out",
ids: [
V4_CLOSE_CURRENCY_ID,
V4_CLOSE_CURRENCY_ID,
@@ -1520,7 +1586,7 @@ describe("uniswap decoder", () => {
),
)(
"a V4 step of $step, then $ending, reads Swap USDT → USDC",
({ swaps, ids, params, amount, min }) => {
({ swaps, ids, params, amount, min, minLabel }) => {
const data = buildExecute(
"0x10",
[
@@ -1532,12 +1598,18 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(
data,
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(result.name).toBe("Swap USDT → USDC");
expect(detail(result, "Token In").address).toBe(USDT_ADDR);
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
expect(detail(result, "Amount")?.value).toBe(amount);
expect(detail(result, "Min. received")?.value).toBe(min);
expect(detail(result, minLabel)?.value).toBe(min);
},
);
@@ -1584,7 +1656,13 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(
data,
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(detail(result, "Steps").value).toBe(
"V4 Swap (could not be decoded)",
);
@@ -1643,7 +1721,7 @@ describe("uniswap decoder", () => {
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result.name).toBe("Uniswap Swap");
expect(detail(result, "Token In").address).toBe(
USDT_ADDR.toLowerCase(),
@@ -1699,7 +1777,7 @@ describe("uniswap decoder", () => {
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result.name).toBe("Uniswap Swap");
expect(detail(result, "Token In").value).toBe(
"Unknown (not named in the calldata)",
@@ -1722,7 +1800,7 @@ describe("uniswap decoder", () => {
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(detail(result, "Token Out").value).toBe(
"Unknown (not named in the calldata)",
);
@@ -1756,7 +1834,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result.name).toBe("Swap USDT → USDC");
expect(detail(result, "Token In").address).toBe(USDT_ADDR);
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
@@ -1780,7 +1858,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
expect(detail(result, "Token In").value).toContain("USDT");
@@ -1811,7 +1889,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toContain("WETH");
@@ -1832,7 +1910,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toContain("USDC");
@@ -1840,7 +1918,8 @@ describe("uniswap decoder", () => {
});
// With no minimum stated before it, a zero balance check is what sets the
// output side, and it guarantees nothing.
// output side, and it guarantees nothing. No step pays anyone, so the
// minimum is not said to be received.
test("a zero balance check with no earlier minimum states no minimum", () => {
const data = buildExecute(
solidityPacked(["uint8", "uint8"], [0x0b, 0x0e]),
@@ -1851,11 +1930,11 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toContain("USDT");
expect(detail(result, "Min. received").value).toBe(
expect(detail(result, "Min. paid out").value).toBe(
"None (no minimum guaranteed)",
);
});
@@ -1871,7 +1950,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
expect(result.name).toBe("Uniswap Swap");
@@ -1891,7 +1970,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result).not.toBeNull();
expect(result.name).toBe("Uniswap Swap");