1 Commits
Author SHA1 Message Date
sneak 603496019c fix: a swap's unknown first take shows beside a PERMIT2_TRANSFER_FROM, only when the user pays it (closes #634)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
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
2026-10-20 10:03:46 +00:00
3 changed files with 50 additions and 12 deletions
+9 -6
View File
@@ -3185,7 +3185,8 @@ Currently supported:
step whose input it cannot read, in whole or in part, and a step it fails on.
In any other case the line gives the total, as with no permit. When the step
that sets the input side spends a token other than the permit's, or one the
decoder does not know, its amount gets lines of its own and the total has no
decoder does not know, its amount gets lines of its own, unless it states no
amount and the user pays it none of its input token, and the total has no
figure to start from: unless the permit's amount stands there is no `Amount`
line, and each take of the permit's token gets lines of its own. Each take the
total does not count gives three lines after the `Amount` line (after
@@ -3197,11 +3198,13 @@ Currently supported:
it some of its input token, and there is no `Amount` line, that step gets them
too whenever another take's lines give a figure, or a `PERMIT2_TRANSFER_FROM`
of its token gives one, on the lines below or, into the router, on its
`Paid to` line, so that figure cannot read as all that leaves. Paid from the
router, that step takes nothing from the user's wallet and never gets them.
They come in step order, the step that sets the input side first when it has
them: `Moved from the sender`, its amount, worded as the `Amount` line words
one, or `Unknown (could not be decoded)` when the decoder does not know it;
`Paid to` line, so that figure cannot read as all that leaves. When it states
no amount and the user pays it none of its input token, as when the router
pays it, that step takes none of that token from the user's wallet and never
gets them, with a permit or without. They come in step order, the step that
sets the input side first when it has them: `Moved from the sender`, its
amount, worded as the `Amount` line words one, or
`Unknown (could not be decoded)` when the decoder does not know it;
`Token moved`, giving the token's address as the `Token In` line does; and
`Moved to`, which reads `The swap step that spends it`. A token the decoder
does not know gives the `Moved from the sender` line alone. A step may also
+11 -6
View File
@@ -1849,17 +1849,22 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// (see spentUnread()), or a token it does not know.
const spentFromSender = [];
let total = null;
// The take of the step that set the input side, when the user pays
// that step, the total starts from it and it stated no amount (see
// below), or null. Paid from the router, the step takes nothing from
// the sender.
// Whether the step that set the input side counts as a take from the
// sender, which may get lines of its own. The amount it stated counts
// whoever pays it. Stating none, it counts only when the user pays it
// some of its input token: paid from the router, it takes nothing
// from the sender.
const hasFirstTake =
inputEstablished && (present(inputAmount) || inputPaidByUser);
// The take of the step that set the input side, when the total
// starts from it and it stated no amount (see below), or null.
let firstTakeUnstated = null;
if (permit === null || sameAddress(inputToken, permit.token)) {
total = inputAmount;
if (inputEstablished && inputPaidByUser && !present(inputAmount)) {
if (hasFirstTake && !present(inputAmount)) {
firstTakeUnstated = { token: inputToken, amount: null };
}
} else if (inputEstablished) {
} else if (hasFirstTake) {
// The step that set the input side spent a token other than the
// permit's, or one not known, so the total starts with no figure.
spentFromSender.push({ token: inputToken, amount: inputAmount });
+30
View File
@@ -2395,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", "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.