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

No files matched your search

+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 `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 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 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 does not know. They come in step order, the step that sets the input side
there is no `Amount` line, that step gets them too whenever another take's first when it has them: `Moved from the sender`, its amount, worded as the
lines give a figure, so that figure cannot read as all that leaves. They come `Amount` line words one, or `Unknown (could not be decoded)` when the decoder
in step order, the step that sets the input side first when it has them: does not know it; `Token moved`, giving the token's address as the `Token In`
`Moved from the sender`, its amount, worded as the `Amount` line words one, or line does; and `Moved to`, which reads `The swap step that spends it`. A token
`Unknown (could not be decoded)` when the decoder does not know it; the decoder does not know gives the `Moved from the sender` line alone. A step
`Token moved`, giving the token's address as the `Token In` line does; and may also take from the user's wallet what the decoder did not read: a V4 step,
`Moved to`, which reads `The swap step that spends it`. A token the decoder other than the one that sets the input side, whose input or one of whose
does not know gives the `Moved from the sender` line alone. A step may also `SETTLE`s or `SETTLE_ALL`s it cannot read, which the router still runs; a V2
take from the user's wallet what the decoder did not read: a V4 step, other or V3 exact-in or V2 exact-out step or a `PERMIT2_TRANSFER_FROM` whose input
than the one that sets the input side, whose input or one of whose `SETTLE`s it cannot read; every `PERMIT2_TRANSFER_FROM_BATCH`; every
or `SETTLE_ALL`s it cannot read, which the router still runs; a V2 or V3 `V3_SWAP_EXACT_OUT`, which the decoder does not read, wherever it stands; and
exact-in or V2 exact-out step or a `PERMIT2_TRANSFER_FROM` whose input it every step it fails on. Each such step gives one `Moved from the sender` line
cannot read; every `PERMIT2_TRANSFER_FROM_BATCH`; every `V3_SWAP_EXACT_OUT`, among those, reading `Unknown (could not be decoded)`. That line, or any line
which the decoder does not read, wherever it stands; and every step it fails for a token the decoder does not know, leaves no `Amount` line, whose figure
on. Each such step gives one `Moved from the sender` line among those, reading might be too low, and such a step before a `PERMIT2_PERMIT` keeps the permit's
`Unknown (could not be decoded)`. That line, or any line for a token the amount from standing. Then each `PERMIT2_TRANSFER_FROM` but one into the
decoder does not know, leaves no `Amount` line, whose figure might be too low, router, in step order, gives three lines: `Moved from the sender`, the amount
and such a step before a `PERMIT2_PERMIT` keeps the permit's amount from it moves, for example `1000000.0000 USDT`; `Token moved`, giving the token's
standing. Then each `PERMIT2_TRANSFER_FROM` but one into the router, in step address as the `Token In` line does; and `Moved to`, which reads
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)` `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 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 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. // (see spentUnread()), or a token it does not know.
const spentFromSender = []; const spentFromSender = [];
let total = null; 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)) { if (permit === null || sameAddress(inputToken, permit.token)) {
total = inputAmount; total = inputAmount;
if (inputEstablished && !present(inputAmount)) {
firstTakeUnstated = { token: inputToken, amount: null };
}
} else if (inputEstablished) { } else if (inputEstablished) {
// The step that set the input side spent a token other than the // 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. // 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; if (ofTokenIn && !present(t.amount)) total = null;
} }
inputAmount = permitStands ? permit.amount : total; 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 // 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 // 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, // 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 // 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, // quantity, gets lines of its own.
// it gets lines of its own too, saying so, so the 400 USDT cannot
// read as all that leaves.
const V2_EXACT_OUT_400 = encodeV2SwapExactOut( const V2_EXACT_OUT_400 = encodeV2SwapExactOut(
USER_ADDR, USER_ADDR,
390000000n, 390000000n,
@@ -1956,37 +1954,28 @@ describe("uniswap decoder", () => {
["Token moved", "USDT (" + USDT_ADDR + ")"], ["Token moved", "USDT (" + USDT_ADDR + ")"],
["Moved to", "The swap step that spends it"], ["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([ test.each([
{ {
kind: "a V4 exact-out step with a SETTLE of OPEN_DELTA by the user", kind: "a V4 exact-out step with a SETTLE of OPEN_DELTA by the user",
commands: [0x10, 0x09], commands: [0x10, 0x09],
first: V4_EXACT_OUT_600, first: encodeV4Swap(
lines: [...UNKNOWN_USDT, ...UP_TO_400_USDT], 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", 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, // A V2 exact-out step makes every amount the swap steps spend a maximum,
// the total included. // the total included.
test("a split with a V2 exact-out step the user pays for shows the total as a maximum", () => { test("a split with a V2 exact-out step the user pays for shows the total as a maximum", () => {