fix: a swap's unknown first take shows beside a PERMIT2_TRANSFER_FROM, only when the user pays it (closes #634)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

decode() now records whether the user pays the V4 step that sets the
input side any of its input token, and decides once, after the loop,
whether that step counts as a take from the sender. A stated amount
counts whoever pays it; with none stated, the step counts only when the
user pays it. Paid from the router, it gets no Moved from the sender
lines, with a PERMIT2_PERMIT of another token or without. When it counts
and states no amount, its Unknown (could not be decoded) line also shows
beside a PERMIT2_TRANSFER_FROM of its token, whose figure is on that
move's own lines or, into the router, on its Paid to line. README
Transaction Decoding says the same.

Model: opus-5-5
This commit was merged in pull request #639.
This commit is contained in:
2026-10-20 12:37:01 +02:00
parent 657dc14f04
commit 070ad82338
3 changed files with 182 additions and 13 deletions
+147
View File
@@ -2318,6 +2318,24 @@ describe("uniswap decoder", () => {
encodeV4TakeAll(WETH_ADDR, 0n),
],
);
// The same step with its SETTLE of OPEN_DELTA paid from the router.
const V4_EXACT_OUT_600_FROM_ROUTER = encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_OUT_SINGLE_ID,
V4_SETTLE_ID,
V4_TAKE_ALL_ID,
]),
[
encodeV4ExactOutSingle(
USDT_ADDR,
WETH_ADDR,
ONE_WETH,
USDT_600,
),
settle(USDT_ADDR, 0n, false),
encodeV4TakeAll(WETH_ADDR, 0n),
],
);
test.each([
{
kind: "a V4 exact-out step with a SETTLE of OPEN_DELTA by the user",
@@ -2325,6 +2343,13 @@ describe("uniswap decoder", () => {
first: V4_EXACT_OUT_600,
lines: [...UNKNOWN_USDT, ...UP_TO_400_USDT],
},
// Paid from the router, the step takes nothing from the wallet.
{
kind: "a V4 exact-out step with a SETTLE of OPEN_DELTA from the router",
commands: [0x10, 0x09],
first: V4_EXACT_OUT_600_FROM_ROUTER,
lines: UP_TO_400_USDT,
},
// Settled by the user, the step's amount would be what that
// SETTLE pays, which the decoder cannot state here.
{
@@ -2370,6 +2395,36 @@ describe("uniswap decoder", () => {
},
);
// https://git.eeqj.de/sneak/AutistMask/issues/634 — after a
// PERMIT2_PERMIT of another token, the V4 exact-out step paid from
// the router still takes nothing from the wallet, so it gets no lines
// of its own. On next it got lines saying its amount is not known.
const ONE_DAI_PERMIT = encodePermit2(DAI_ADDR, 10n ** 18n, ROUTER_ADDR);
test.each([
{
kind: "a PERMIT2_PERMIT of 1 DAI, then a V4 exact-out step of at most 600 USDT paid from the router",
commands: [0x0a, 0x10],
inputs: [ONE_DAI_PERMIT, V4_EXACT_OUT_600_FROM_ROUTER],
lines: [["Amount", "1.0000 DAI"]],
},
{
kind: "a PERMIT2_PERMIT of 1 DAI, a V4 exact-out step of at most 600 USDT paid from the router, then a V2 exact-out of at most 400 USDT the user pays for",
commands: [0x0a, 0x10, 0x09],
inputs: [
ONE_DAI_PERMIT,
V4_EXACT_OUT_600_FROM_ROUTER,
V2_EXACT_OUT_400,
],
lines: [["Amount", "Up to 1.0000 DAI"], ...UP_TO_400_USDT],
},
])(
"$kind, gives the V4 step no lines of its own",
({ commands, inputs, lines }) => {
const result = decodeSteps(commands, inputs);
expect(linesBetweenTokens(result)).toEqual(lines);
},
);
// The V4 exact-out route above the other way round, where the later
// take's unknown amount leaves no figure at all, and other ways the
// step that sets the input side states no amount.
@@ -2423,6 +2478,98 @@ describe("uniswap decoder", () => {
},
);
// https://git.eeqj.de/sneak/AutistMask/issues/634 — a
// PERMIT2_TRANSFER_FROM of the token the V4 exact-out step spends,
// before or after it, shows its own figure, so the step's amount is
// shown as not known beside it, and the 400 USDT cannot read as all
// that leaves. On next only the 400 USDT was shown. Into the router,
// the 400 USDT is on its Paid to line.
const transfer = (token, recipient, amount) =>
coder.encode(
["address", "address", "uint160"],
[token, recipient, amount],
);
const MOVED_400_USDT = [
["Moved from the sender", "400.0000 USDT"],
["Token moved", "USDT (" + USDT_ADDR + ")"],
["Moved to", OTHER_ADDR],
];
test.each([
{
kind: "a V4 exact-out step of at most 600 USDT, then a PERMIT2_TRANSFER_FROM of 400 USDT to another address",
commands: [0x10, 0x02],
inputs: [
V4_EXACT_OUT_600,
transfer(USDT_ADDR, OTHER_ADDR, USDT_400),
],
lines: [...UNKNOWN_USDT, ...MOVED_400_USDT],
},
{
kind: "a PERMIT2_TRANSFER_FROM of 400 USDT to another address, then a V4 exact-out step of at most 600 USDT",
commands: [0x02, 0x10],
inputs: [
transfer(USDT_ADDR, OTHER_ADDR, USDT_400),
V4_EXACT_OUT_600,
],
lines: [...UNKNOWN_USDT, ...MOVED_400_USDT],
},
{
kind: "a V4 exact-out step of at most 600 USDT, then a PERMIT2_TRANSFER_FROM of 400 USDT into the router",
commands: [0x10, 0x02],
inputs: [
V4_EXACT_OUT_600,
transfer(USDT_ADDR, ROUTER_ADDR, USDT_400),
],
lines: UNKNOWN_USDT,
},
{
kind: "a PERMIT2_TRANSFER_FROM of 400 USDT into the router, then a V4 exact-out step of at most 600 USDT",
commands: [0x02, 0x10],
inputs: [
transfer(USDT_ADDR, ROUTER_ADDR, USDT_400),
V4_EXACT_OUT_600,
],
lines: UNKNOWN_USDT,
},
])(
"$kind, paid by the user, shows that the step's amount is not known",
({ commands, inputs, lines }) => {
const result = decodeSteps(commands, inputs);
expect(linesBetweenTokens(result)).toEqual(lines);
},
);
// Paid from the router, the V4 exact-out step takes nothing from the
// wallet, so the moves give all that leaves.
test("a PERMIT2_TRANSFER_FROM of 400 USDT into the router, one of 10 USDT to another address, then a V4 exact-out step paid from the router, shows only the moves", () => {
const result = decodeSteps(
[0x02, 0x02, 0x10],
[
transfer(USDT_ADDR, ROUTER_ADDR, USDT_400),
transfer(USDT_ADDR, OTHER_ADDR, 10000000n),
V4_EXACT_OUT_600_FROM_ROUTER,
],
);
expect(linesBetweenTokens(result)).toEqual([
["Moved from the sender", "10.0000 USDT"],
["Token moved", "USDT (" + USDT_ADDR + ")"],
["Moved to", OTHER_ADDR],
]);
});
// A move of another token says nothing of how much USDT leaves.
test("a V4 exact-out step of at most 600 USDT, then a PERMIT2_TRANSFER_FROM of 400 DAI, shows only the DAI moved", () => {
const result = decodeSteps(
[0x10, 0x02],
[V4_EXACT_OUT_600, transfer(DAI_ADDR, OTHER_ADDR, DAI_400)],
);
expect(linesBetweenTokens(result)).toEqual([
["Moved from the sender", "400.0000 DAI"],
["Token moved", "DAI (" + DAI_ADDR + ")"],
["Moved to", OTHER_ADDR],
]);
});
// A V2 exact-out step makes every amount the swap steps spend a maximum,
// the total included.
test("a split with a V2 exact-out step the user pays for shows the total as a maximum", () => {