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 39 additions and 123 deletions
+20 -23
View File
@@ -3170,29 +3170,26 @@ Currently supported:
`Token In`; one of `Token In` after an amount that is not a quantity (a V4
open delta, `ALREADY_PAID`, which keep their wording on the `Amount` line, or
no stated amount, as of a V4 exact-out step); and one whose amount the decoder
does not know. When the step that sets the input side states no amount and
there is no `Amount` line, that step gets them too whenever another take's
lines give a figure, so that figure cannot read as all that leaves. 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
take from the user's wallet what the decoder did not read: a V4 step, 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`; 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
does not know. 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 take from the user's wallet what the decoder did not read: a V4 step,
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`; 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
-17
View File
@@ -1795,14 +1795,8 @@ 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 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 && !present(inputAmount)) {
firstTakeUnstated = { token: inputToken, amount: null };
}
} else if (inputEstablished) {
// 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.
@@ -1824,17 +1818,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
if (ofTokenIn && !present(t.amount)) total = null;
}
inputAmount = permitStands ? permit.amount : total;
// With no Amount figure, a later take's own figure could read as all
// that leaves, so the step that set the input side, when it stated
// no amount, then gets lines of its own too, first, saying its amount
// is not known.
if (
!present(inputAmount) &&
firstTakeUnstated !== null &&
spentFromSender.some((s) => present(s.amount))
) {
spentFromSender.unshift(firstTakeUnstated);
}
// The commands of an EXECUTE_SUB_PLAN step may spend the user's tokens
// or pay the user, before or after any other step, and a permit among
+19 -83
View File
@@ -1942,9 +1942,7 @@ describe("uniswap decoder", () => {
// 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
// quantity, gets lines of its own. When that step stated no amount,
// it gets lines of its own too, saying so, so the 400 USDT cannot
// read as all that leaves.
// quantity, gets lines of its own.
const V2_EXACT_OUT_400 = encodeV2SwapExactOut(
USER_ADDR,
390000000n,
@@ -1956,37 +1954,28 @@ describe("uniswap decoder", () => {
["Token moved", "USDT (" + USDT_ADDR + ")"],
["Moved to", "The swap step that spends it"],
];
const UNKNOWN_USDT = [
["Moved from the sender", UNDECODED],
["Token moved", "USDT (" + USDT_ADDR + ")"],
["Moved to", "The swap step that spends it"],
];
// A V4 exact-out step of at most 600 USDT for 1 WETH, in the shape
// of Uniswap's own routes: a SETTLE of OPEN_DELTA by the user pays
// what the swap spends.
const V4_EXACT_OUT_600 = 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,
),
encodeV4Settle(USDT_ADDR),
encodeV4TakeAll(WETH_ADDR, 0n),
],
);
test.each([
{
kind: "a V4 exact-out step with a SETTLE of OPEN_DELTA by the user",
commands: [0x10, 0x09],
first: V4_EXACT_OUT_600,
lines: [...UNKNOWN_USDT, ...UP_TO_400_USDT],
first: 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,
),
encodeV4Settle(USDT_ADDR),
encodeV4TakeAll(WETH_ADDR, 0n),
],
),
lines: UP_TO_400_USDT,
},
{
kind: "a V4 exact-in step of the open delta",
@@ -2031,59 +2020,6 @@ describe("uniswap decoder", () => {
},
);
// 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.
test.each([
{
kind: "a V2 exact-out of at most 400 USDT, then a V4 exact-out step of at most 600 USDT",
commands: [0x09, 0x10],
inputs: [V2_EXACT_OUT_400, V4_EXACT_OUT_600],
lines: UNKNOWN_USDT,
},
{
kind: "a PERMIT2_PERMIT of 1 USDT to another spender, a V4 exact-out step of at most 600 USDT, then a V2 exact-out of at most 400 USDT",
commands: [0x0a, 0x10, 0x09],
inputs: [
encodePermit2(USDT_ADDR, 1000000n, OTHER_ADDR),
V4_EXACT_OUT_600,
V2_EXACT_OUT_400,
],
lines: [...UNKNOWN_USDT, ...UP_TO_400_USDT],
},
{
kind: "a V4 step whose swap action cannot be read, with a SETTLE of WETH by the user, then a V2 exact-out of at most 400 USDT",
commands: [0x10, 0x09],
inputs: [
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_SETTLE_ID,
V4_TAKE_ALL_ID,
]),
[
UNREAD_SWAP,
encodeV4Settle(WETH_ADDR),
encodeV4TakeAll(USDC_ADDR, 0n),
],
),
V2_EXACT_OUT_400,
],
lines: [
["Moved from the sender", UNDECODED],
["Token moved", "WETH (" + WETH_ADDR + ")"],
["Moved to", "The swap step that spends it"],
...UP_TO_400_USDT,
],
},
])(
"$kind, all paid by the user, shows that an amount is not known",
({ commands, inputs, lines }) => {
const result = decodeSteps(commands, inputs);
expect(linesBetweenTokens(result)).toEqual(lines);
},
);
// 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", () => {