1 Commits
Author SHA1 Message Date
clawbot 73b7db9d79 fix: a swap split across steps the user pays for shows every amount it spends (closes #625)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
The swap's Amount line took only the first swap step's amount. It now
gives the total the steps take of Token In: the amount of the step that
sets the input side plus what each later step the user pays takes of it.
A take of another token, one after an amount that is not a quantity, or
one the decoder cannot tell, gets lines of its own; an unread take, a
V3_SWAP_EXACT_OUT's included, leaves no figure wherever it stands. A V4
step counts every SETTLE paid by the user and every SETTLE_ALL. A
PERMIT2_PERMIT names Token In, and its amount stands only in the plain
shape of Uniswap's own routes, with every later step read.

Model: opus-5-5
2026-10-20 03:06:47 +00:00
3 changed files with 85 additions and 38 deletions
+10 -9
View File
@@ -3180,15 +3180,16 @@ Currently supported:
other than the one that sets the input side, whose input or one of whose
`SETTLE`s or `SETTLE_ALL`s it cannot read, which the router still runs; a V2
or V3 exact-in or V2 exact-out step or a `PERMIT2_TRANSFER_FROM` whose input
it cannot read; every `PERMIT2_TRANSFER_FROM_BATCH`; and, once the input side
is set, every `V3_SWAP_EXACT_OUT`, which the decoder does not read, and every
step it fails on. Each such step gives one `Moved from the sender` line among
those, reading `Unknown (could not be decoded)`. That line, or any line for a
token the decoder does not know, leaves no `Amount` line, whose figure might
be too low. Then each `PERMIT2_TRANSFER_FROM` but one into the router, in step
order, gives three lines: `Moved from the sender`, the amount it moves, for
example `1000000.0000 USDT`; `Token moved`, giving the token's address as the
`Token In` line does; and `Moved to`, which reads
it cannot read; every `PERMIT2_TRANSFER_FROM_BATCH`; every
`V3_SWAP_EXACT_OUT`, which the decoder does not read, wherever it stands; and
every step it fails on. Each such step gives one `Moved from the sender` line
among those, reading `Unknown (could not be decoded)`. That line, or any line
for a token the decoder does not know, leaves no `Amount` line, whose figure
might be too low, and such a step before a `PERMIT2_PERMIT` keeps the permit's
amount from standing. Then each `PERMIT2_TRANSFER_FROM` but one into the
router, in step order, gives three lines: `Moved from the sender`, the amount
it moves, for example `1000000.0000 USDT`; `Token moved`, giving the token's
address as the `Token In` line does; and `Moved to`, which reads
`The V2 pair the next swap spends` for a hop and `The sender` for `address(1)`
on every router but `UniversalRouterV1`, and otherwise gives the address. A
move into the router needs none of them, because its `Paid to` line and
+13 -13
View File
@@ -1234,10 +1234,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// V4 step, other than the one that sets the input side, whose input
// or one of whose SETTLEs or SETTLE_ALLs it cannot read; a V2 or V3
// exact-in or V2 exact-out step or PERMIT2_TRANSFER_FROM whose input
// it cannot read; a PERMIT2_TRANSFER_FROM_BATCH; and, once the input
// side is set, a V3_SWAP_EXACT_OUT or a step it fails on. It is
// recorded with a null token and amount: one line says so, and there
// is no Amount line, whose figure may then be too low.
// it cannot read; a PERMIT2_TRANSFER_FROM_BATCH; a V3_SWAP_EXACT_OUT;
// and a step it fails on. It is recorded with a null token and
// amount: one line says so, there is no Amount line, whose figure may
// then be too low, and the amount of a permit after it does not stand
// (see permitStands).
const spentUnread = () => {
userPaid.push({ token: null, amount: null });
};
@@ -1489,11 +1490,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} else if (command === "V3_SWAP_EXACT_OUT") {
// This decoder does not read it
// (https://git.eeqj.de/sneak/AutistMask/issues/601). It
// may pay any address and take from the sender, so once
// the input side is set it leaves no Amount figure.
// may pay any address and take from the sender, so
// wherever it stands it leaves no Amount figure.
payeeUnread = true;
stepsUnread.push(i);
if (inputEstablished) spentUnread();
spentUnread();
} else if (command === "V2_SWAP_EXACT_IN") {
const s = decodeV2SwapExactIn(inputs[i]);
if (s) {
@@ -1749,7 +1750,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// take from the sender.
payeeUnread = true;
stepsUnread.push(i);
if (inputEstablished) spentUnread();
spentUnread();
}
if (allowedToFail) {
commandNames[i] += " (allowed to fail)";
@@ -1774,11 +1775,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// it in full (see stepsUnread). Permit2 then lets the router take no
// more of the token than that amount, so the permit covers every take
// of it. A later step this decoder did not read may raise what Permit2
// lets the router take, as a PERMIT2_PERMIT_BATCH can. A step this
// decoder does not read that may take from the sender leaves no
// figure, permit or not, when it comes after the step that sets the
// input side (see spentUnread()), and an EXECUTE_SUB_PLAN step leaves
// both sides unknown (below).
// lets the router take, as a PERMIT2_PERMIT_BATCH can. A step that
// may take from the sender what this decoder did not read leaves no
// figure, permit or not (see spentUnread()), and an EXECUTE_SUB_PLAN
// step leaves both sides unknown (below).
const stepCommands = Array.from(
commandsBytes,
(b) => commands[b & commandTypeMask],
+62 -16
View File
@@ -1876,23 +1876,69 @@ describe("uniswap decoder", () => {
},
);
// This decoder does not read a V3_SWAP_EXACT_OUT, so what one takes
// after the step that sets the input side leaves no Amount figure.
test("a V3 exact-in of 600 USDT, then a V3_SWAP_EXACT_OUT the user pays for, shows no Amount figure", () => {
// Its input is (recipient, amountOut, amountInMax, path,
// payerIsUser), laid out as encodeV3SwapExactIn() writes one; an
// exact-out path lists the token bought first.
const exactOut = encodeV3SwapExactIn(
USER_ADDR,
390000000n,
USDT_400,
[USDC_ADDR, USDT_ADDR],
);
const result = decodeSteps([0x00, 0x01], [V3_600, exactOut]);
expect(linesBetweenTokens(result)).toEqual([
["Moved from the sender", UNDECODED],
// A V3_SWAP_EXACT_OUT the user pays for, of at most `amountInMax`
// USDT for USDC. Its input is (recipient, amountOut, amountInMax,
// path, payerIsUser), laid out as encodeV3SwapExactIn() writes one;
// an exact-out path lists the token bought first.
const v3ExactOut = (amountInMax) =>
encodeV3SwapExactIn(USER_ADDR, 390000000n, amountInMax, [
USDC_ADDR,
USDT_ADDR,
]);
});
// A split exact-out route: a V3_SWAP_EXACT_OUT of at most 6,000 USDT
// and a V2_SWAP_EXACT_OUT of at most 4,000 USDT, both paid by the
// user, then a SWEEP of USDC to the user.
const EXACT_OUT_SPLIT = [
v3ExactOut(6000000000n),
encodeV2SwapExactOut(USER_ADDR, 390000000n, 4000000000n, [
USDT_ADDR,
USDC_ADDR,
]),
coder.encode(
["address", "address", "uint256"],
[USDC_ADDR, USER_ADDR, 0n],
),
];
// This decoder does not read a V3_SWAP_EXACT_OUT, so what one takes
// leaves no Amount figure wherever it stands, and a permit's amount
// does not stand.
test.each([
{
kind: "a V3 exact-in of 600 USDT, then a V3_SWAP_EXACT_OUT",
commands: [0x00, 0x01],
inputs: [V3_600, v3ExactOut(USDT_400)],
},
{
kind: "a PERMIT2_PERMIT of 10,000 USDT, then a V3_SWAP_EXACT_OUT, a V2_SWAP_EXACT_OUT and a SWEEP",
commands: [0x0a, 0x01, 0x09, 0x04],
inputs: [
encodePermit2(USDT_ADDR, 10000000000n, ROUTER_ADDR),
...EXACT_OUT_SPLIT,
],
},
{
kind: "a V3_SWAP_EXACT_OUT, a V2_SWAP_EXACT_OUT and a SWEEP",
commands: [0x01, 0x09, 0x04],
inputs: EXACT_OUT_SPLIT,
},
{
kind: "a V3_SWAP_EXACT_OUT of at most 9,400 USDT, then a PERMIT2_PERMIT of 600 USDT and a V3 exact-in of 600 USDT",
commands: [0x01, 0x0a, 0x00],
inputs: [
v3ExactOut(9400000000n),
encodePermit2(USDT_ADDR, USDT_600, ROUTER_ADDR),
V3_600,
],
},
])(
"$kind, all paid by the user, shows no Amount figure",
({ commands, inputs }) => {
const result = decodeSteps(commands, inputs);
expect(linesBetweenTokens(result)).toEqual([
["Moved from the sender", UNDECODED],
]);
},
);
// A later step's amount that cannot be added to the Amount line,
// because the amount of the step that set the input side is not a