fix: a PERMIT2_TRANSFER_FROM before a permit keeps its amount from standing as the Amount figure (closes #655)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

A PERMIT2_TRANSFER_FROM that runs before a PERMIT2_PERMIT of the same
token moves it under whatever allowance the router already had, so it
can move more than the permit's amount. The permit's amount still stood
as the Amount figure, which could then read lower than what leaves the
wallet.

decode() now counts such a transfer, into the router or anywhere else,
among the steps before the permit that took its token, so the permit no
longer stands: the Amount line gives the total the steps take, as with
no permit, and the permit gets its own lines. A transfer after the
permit still lets it stand. README Transaction Decoding states the
condition.

Model: opus-5-5
This commit is contained in:
2026-10-20 23:50:52 +00:00
parent f53b503b15
commit e729331055
3 changed files with 111 additions and 46 deletions
+56
View File
@@ -2196,6 +2196,62 @@ describe("uniswap decoder", () => {
},
);
// https://git.eeqj.de/sneak/AutistMask/issues/655 — a
// PERMIT2_TRANSFER_FROM before the permit moves the permit's token
// under whatever allowance the router already had, so it may move
// more than the permit's amount. The permit's amount then does not
// stand, and the Amount line gives the total the steps take, as with
// no permit. On next at f53b503 the Amount line read the permit's
// 600.0000 USDT, and the permit got no lines of its own.
const USDT_2000 = 2000000000n;
// A PERMIT2_TRANSFER_FROM of `amount` of USDT to `recipient`.
const permit2TransferFrom = (recipient, amount) =>
coder.encode(
["address", "address", "uint160"],
[USDT_ADDR, recipient, amount],
);
test.each([
{
kind: "a PERMIT2_TRANSFER_FROM of 2,000 USDT into the router, then a PERMIT2_PERMIT of 600 USDT and a V3 exact-in of 2,000 USDT paid from the router,",
inputs: [
permit2TransferFrom(ROUTER_ADDR, USDT_2000),
encodePermit2(USDT_ADDR, USDT_600, ROUTER_ADDR),
coder.encode(
["address", "uint256", "uint256", "bytes", "bool"],
[
USER_ADDR,
USDT_2000,
1n,
encodeV3Path([USDT_ADDR, USDC_ADDR]),
false,
],
),
],
lines: [["Amount", "2000.0000 USDT"], ...USDT_600_LINES],
},
{
kind: "a PERMIT2_TRANSFER_FROM of 2,000 USDT to another address, then a PERMIT2_PERMIT of 600 USDT and a V3 exact-in of 600 USDT,",
inputs: [
permit2TransferFrom(OTHER_ADDR, USDT_2000),
encodePermit2(USDT_ADDR, USDT_600, ROUTER_ADDR),
V3_600,
],
lines: [
["Amount", "600.0000 USDT"],
["Moved from the sender", "2000.0000 USDT"],
["Token moved", "USDT (" + USDT_ADDR + ")"],
["Moved to", OTHER_ADDR],
...USDT_600_LINES,
],
},
])(
"$kind shows the total the steps take and the permit's own lines",
({ inputs, lines }) => {
const result = decodeSteps([0x02, 0x0a, 0x00], inputs);
expect(linesBetweenTokens(result)).toEqual(lines);
},
);
// A V3_SWAP_EXACT_OUT the user pays for, of at most `amountInMax`
// USDT for USDC.
const v3ExactOut = (amountInMax) =>