1 Commits
Author SHA1 Message Date
clawbot 08d760d195 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. Each later
step the user pays for now adds what it takes of Token In, and what it
takes of another token, or an amount the decoder cannot tell, gets lines
of its own. A V4 step counts every SETTLE paid by the user and every
SETTLE_ALL, with or without a swap action. A step that may take from the
sender what the decoder did not read leaves no Amount figure and gets one
line saying so. Only a permit to the router that the router does not let
fail still covers later takes of its token.

Model: opus-5-5
2026-10-19 23:53:32 +00:00
4 changed files with 156 additions and 309 deletions
+45 -58
View File
@@ -2368,11 +2368,11 @@ view would leave a wallet one click from deletion.
- Site origin (bold, scheme and port included) + "wants to send a - Site origin (bold, scheme and port included) + "wants to send a
transaction" transaction"
- Decoded action (if calldata is recognized): action name, token details, - Decoded action (if calldata is recognized): action name, token details,
amounts, what each swap step spends that the `Amount` line does not give, amounts, what each swap step the user pays for spends of a token other
what each `PERMIT2_TRANSFER_FROM` moves and where to, steps, deadline, and than `Token In`, what each `PERMIT2_TRANSFER_FROM` moves and where to,
unless a swap pays only the From address, a `Paid to` line for each other steps, deadline, and unless a swap pays only the From address, a `Paid to`
address it pays, or saying that a token stays in the router or who it pays line for each other address it pays, or saying that a token stays in the
is unknown (see Transaction Decoding) router or who it pays is unknown (see Transaction Decoding)
- From: color dot + full address + etherscan link - From: color dot + full address + etherscan link
- Contract: color dot + full address + etherscan link, token symbol label if - Contract: color dot + full address + etherscan link, token symbol label if
known; for a contract creation, the same sentence as on WaitTx known; for a contract creation, the same sentence as on WaitTx
@@ -3125,61 +3125,48 @@ Currently supported:
name); and one more reads `Unknown (could not be decoded)` when a step that name); and one more reads `Unknown (could not be decoded)` when a step that
may pay an address went unread. When no step pays anyone, a single line reads may pay an address went unread. When no step pays anyone, a single line reads
`Unknown (not named in the calldata)`. Wherever there is a `Paid to` line, the `Unknown (not named in the calldata)`. Wherever there is a `Paid to` line, the
minimum reads `Min. paid out`. The `Amount` line gives the total the steps minimum reads `Min. paid out`. The `Amount` line gives the amount of the step
take of `Token In`, from the step that sets the input side on: the first V2 or that sets the input side: the last `PERMIT2_PERMIT` step if there is one,
V3 exact-in, V2 exact-out, `WRAP_ETH` or V4 swap step that names an input otherwise the first V2 or V3 exact-in, V2 exact-out, `WRAP_ETH` or V4 swap
token or amount. A V4 step with a swap action sets the input side when no step that names an input token or amount. A V4 step with a swap action sets
earlier step set it, even when it names neither (see Display Consistency). the input side when no earlier step set it, even when it names neither (see
That step counts its own amount, whoever pays it: a V2 exact-out step its Display Consistency). A V2 exact-out step gives its `amountInMax`, and a V4
`amountInMax`, and a V4 swap step the `amountIn` of the swap action that swap step the `amountIn` of the swap action that spends its input currency.
spends its input currency. Each later V2 or V3 exact-in or V2 exact-out step Each later V2 or V3 exact-in or V2 exact-out step whose `payerIsUser` is true
whose `payerIsUser` is true takes its amount, found in the same way, from the also takes its amount, found in the same way, from the user's wallet. So does
user's wallet. So does every V4 step but the one that sets the input side, every V4 step but the one that sets the input side, with a swap action or
with a swap action or without: it takes what its `SETTLE`s paid by the user without: it takes what its `SETTLE`s paid by the user (`payerIsUser` true) and
(`payerIsUser` true) and its `SETTLE_ALL`s pay, for each token on its own. A its `SETTLE_ALL`s pay, for each token on its own. A `SETTLE` of any amount but
`SETTLE` of any amount but `OPEN_DELTA` pays that amount, and a `SETTLE` of `OPEN_DELTA` pays that amount, and a `SETTLE` of `OPEN_DELTA` or a
`OPEN_DELTA` or a `SETTLE_ALL` pays what the step owes, as above. A later step `SETTLE_ALL` pays what the step owes, as above. What a step takes of
paid from the router spends what the router holds and takes nothing, and so `Token In` is added to the line's, unless a `PERMIT2_PERMIT` set the input
does a V2 exact-in whose `amountIn` is `ALREADY_PAID`. A later take of side, whose amount the line keeps. A permit whose spender is the router and
`Token In` is added to the total while both are quantities: a V3 exact-in of which the router does not let fail caps what the router may take of its token
after it; any other permit caps nothing the router takes. So a V3 exact-in of
600 USDT and a V2 exact-in of 400 USDT, both paid by the user, read 600 USDT and a V2 exact-in of 400 USDT, both paid by the user, read
`1000.0000 USDT`. When the decoder does not know a take's amount, the total is `1000.0000 USDT`. When either amount is not a quantity (a V4 open delta,
not known and there is no `Amount` line. `Token In` is the token of the last `ALREADY_PAID`, or an amount the decoder does not know), the total is not
`PERMIT2_PERMIT` the decoder reads, when there is one. Its amount, `Unlimited` known and there is no `Amount` line. A step paid from the router spends what
included, stands as the `Amount` figure in place of the total only in the the router holds and adds nothing, and so does a V2 exact-in whose `amountIn`
plain shape of Uniswap's own routes: it is the only `PERMIT2_PERMIT`, its is `ALREADY_PAID`, which takes nothing from the user. What a step takes of a
spender is the router, the router may not let it fail, no step before it takes token other than `Token In`, or of `Token In` when the decoder does not know
its token or a token the decoder does not know, and no `PERMIT2_PERMIT_BATCH` the amount or when a permit that caps nothing set the input side, gives three
comes after it. Permit2 then lets the router take no more of the token than lines after the `Amount` line (after `Token In` when there is none), in step
that amount, so the permit covers every take of it. In any other case the line order: `Moved from the sender`, its amount, worded as the `Amount` line words
gives the total, as with no permit. When the step that sets the input side one, or `Unknown (could not be decoded)` when the decoder does not know it;
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 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 `Token In` when there is none): one
of a token other than `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. 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 `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 `Moved to`, which reads `The swap step that spends it`. A step may also take
does not know gives the `Moved from the sender` line alone. A step may also from the user's wallet what the decoder did not read: a V4 step, other than
take from the user's wallet what the decoder did not read: a V4 step, other the one that sets the input side, whose input or one of whose `SETTLE`s or
than the one that sets the input side, whose input or one of whose `SETTLE`s `SETTLE_ALL`s it cannot read, which the router still runs; a V2 or V3 exact-in
or `SETTLE_ALL`s it cannot read, which the router still runs; a V2 or V3 or V2 exact-out step or a `PERMIT2_TRANSFER_FROM` whose input it cannot read;
exact-in or V2 exact-out step or a `PERMIT2_TRANSFER_FROM` whose input it and every `PERMIT2_TRANSFER_FROM_BATCH`. Each such step gives one
cannot read; and every `PERMIT2_TRANSFER_FROM_BATCH`. Each such step gives one `Moved from the sender` line, among those lines in step order, reading
`Moved from the sender` line among those, reading `Unknown (could not be decoded)`, and there is then no `Amount` line, whose
`Unknown (could not be decoded)`. That line, or any line for a token the figure might be too low. 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
Then each `PERMIT2_TRANSFER_FROM` but one into the router, in step order, it moves, for example `1000000.0000 USDT`; `Token moved`, giving the token's
gives three lines: `Moved from the sender`, the amount it moves, for example address as the `Token In` line does; and `Moved to`, which reads
`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
+62 -104
View File
@@ -357,11 +357,6 @@ function present(value) {
return value !== null && value !== undefined; return value !== null && value !== undefined;
} }
// Whether `a` and `b` are both known and the same address.
function sameAddress(a, b) {
return present(a) && present(b) && a.toLowerCase() === b.toLowerCase();
}
// Uniswap V4 spells "use the whole open delta" as an amount of zero: // Uniswap V4 spells "use the whole open delta" as an amount of zero:
// v4-periphery `src/libraries/ActionConstants.sol` declares // v4-periphery `src/libraries/ActionConstants.sol` declares
// `uint128 internal constant OPEN_DELTA = 0` ("used to signal that an action // `uint128 internal constant OPEN_DELTA = 0` ("used to signal that an action
@@ -1166,9 +1161,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
let inputToken = null; let inputToken = null;
let inputAmount = null; let inputAmount = null;
let inputEstablished = false; let inputEstablished = false;
// The last PERMIT2_PERMIT this decoder read, or null (see let inputFromPermit = false;
// permitStands after the loop). let permitCapsInput = false;
let permit = null;
let outputToken = null; let outputToken = null;
let minOutput = null; let minOutput = null;
let outputEstablished = false; let outputEstablished = false;
@@ -1196,9 +1190,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// states neither half: the output side always, and the input side // states neither half: the output side always, and the input side
// when no earlier hop set it. A half the establishing hop did not // when no earlier hop set it. A half the establishing hop did not
// state stays null and the line says so, rather than being filled in // state stays null and the line says so, rather than being filled in
// from a different hop. A PERMIT2_PERMIT sets no side here: after the // from a different hop. Each later swap step the user pays for adds
// loop, the last one read names Token In, and the Amount line is // what it spends, in addUserPaid(), the one place the total is formed,
// formed there, in one place (see permitStands). // and a step that may take what this decoder did not read leaves no
// total at all (see spentUnread()).
const setInput = (token, amount) => { const setInput = (token, amount) => {
inputToken = present(token) ? token : null; inputToken = present(token) ? token : null;
inputAmount = present(amount) ? amount : null; inputAmount = present(amount) ? amount : null;
@@ -1216,25 +1211,53 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
outputEstablished = true; outputEstablished = true;
}; };
// What each step but the one that sets the input side takes from the // What steps take from the sender that the Amount line does not give,
// sender, as { token, amount }, in step order. After the loop each // as { token, amount }, in step order (see addUserPaid()). A step that
// joins the Amount line or gets lines of its own (see permitStands). // may take from the sender what this decoder did not read is here
const userPaid = []; // with a null token (see spentUnread()).
// A swap step the user pays for (`payerIsUser` true) takes `amount` const spentFromSender = [];
// of `token` from the sender. A V2 exact-in of ALREADY_PAID takes // A swap step the user pays for (`payerIsUser` true), after the input
// nothing. // side is set, takes `amount` of `token` from the sender. What it
// takes of the input side's token is added to the input side's
// amount, so the Amount line is what the steps spend of it together;
// when either is not a quantity the total is not known (null), and an
// amount not known also goes in `spentFromSender`, so the screen says
// so. What it takes of another token goes in `spentFromSender`. A V2
// exact-in of ALREADY_PAID takes nothing. When a permit set the input
// side, the line keeps the permit's amount: a permit that caps what
// the router takes of its token (`permitCapsInput`) covers the take,
// and after any other permit the take goes in `spentFromSender`.
const addUserPaid = (token, amount) => { const addUserPaid = (token, amount) => {
if (amount !== ALREADY_PAID) userPaid.push({ token, amount }); if (amount === ALREADY_PAID) return;
const sameToken =
present(inputToken) &&
token.toLowerCase() === inputToken.toLowerCase();
if (sameToken && inputFromPermit) {
if (!permitCapsInput) {
spentFromSender.push({ token, amount });
}
return;
}
if (!sameToken) {
spentFromSender.push({ token, amount });
return;
}
inputAmount =
typeof inputAmount === "bigint" && typeof amount === "bigint"
? inputAmount + amount
: null;
if (!present(amount)) {
spentFromSender.push({ token, amount });
}
}; };
// A step may take from the sender what this decoder did not read: a // A step may take from the sender what this decoder did not read: a
// V4 step, other than the one that sets the input side, whose input // 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 // or one of whose SETTLEs or SETTLE_ALLs it cannot read; a V2 or V3
// swap step or PERMIT2_TRANSFER_FROM whose input it cannot read; or a // swap step or PERMIT2_TRANSFER_FROM whose input it cannot read; or a
// PERMIT2_TRANSFER_FROM_BATCH. It is recorded with a null token and // PERMIT2_TRANSFER_FROM_BATCH. One line says so, and there is no
// amount: one line says so, and there is no Amount line, whose figure // Amount line, whose figure may then be too low.
// may then be too low.
const spentUnread = () => { const spentUnread = () => {
userPaid.push({ token: null, amount: null }); spentFromSender.push({ token: null, amount: null });
}; };
// Who the steps pay, in whichever step and of whatever token. // Who the steps pay, in whichever step and of whatever token.
@@ -1373,22 +1396,19 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
payeeUnread = true; payeeUnread = true;
} else if (command === "PERMIT2_PERMIT") { } else if (command === "PERMIT2_PERMIT") {
const p = decodePermit2(inputs[i]); const p = decodePermit2(inputs[i]);
// A permit states both halves itself, so it may replace an
// input side an earlier hop established without breaking
// the invariant.
if (p) { if (p) {
// Whether a step before it took its token, or may setInput(p.token, p.amount);
// have: the step that set the input side or one in inputFromPermit = true;
// `userPaid`, when its token is the permit's or not // Permit2 lets the spender take no more of the token
// known. // than the permit's amount. Only a permit to this
const mayBeItsToken = (token) => // router that the router does not let fail caps what
!present(token) || sameAddress(token, p.token); // the later steps take (see addUserPaid()).
const tookBefore = permitCapsInput =
(inputEstablished && mayBeItsToken(inputToken)) || !allowedToFail &&
userPaid.some((t) => mayBeItsToken(t.token)); p.spender.toLowerCase() === toAddress.toLowerCase();
permit = {
...p,
step: i,
allowedToFail,
beforeEveryTake: !tookBefore,
};
} }
} else if (command === "PERMIT2_TRANSFER_FROM") { } else if (command === "PERMIT2_TRANSFER_FROM") {
const p = decodePermit2TransferFrom(inputs[i]); const p = decodePermit2TransferFrom(inputs[i]);
@@ -1717,71 +1737,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} }
} }
// The Amount line, and the lines for what the steps take beyond it.
//
// The total is what the steps take of Token In: the amount of the
// step that set the input side, with each later take of that token
// added to it while both are quantities. A take that cannot be added,
// being of another token or coming after an amount that is not a
// quantity (a V4 open delta, ALREADY_PAID, or none stated), gets
// lines of its own. One whose amount is not known also leaves the
// total unknown, with no figure.
//
// The last PERMIT2_PERMIT read names Token In. Its amount stands as
// the Amount figure in place of the total only in the plain shape of
// Uniswap's own routes: it is the only PERMIT2_PERMIT, its spender is
// the router, the router may not let it fail, no step before it took
// its token or one not known, and no PERMIT2_PERMIT_BATCH comes after
// it. Permit2 then
// lets the router take no more of the token than that amount, so the
// permit covers every take of it. A step this decoder does not read
// that may take from the sender leaves no figure in any case, and an
// EXECUTE_SUB_PLAN step leaves both sides unknown (below).
const stepCommands = Array.from(
commandsBytes,
(b) => commands[b & commandTypeMask],
);
const permitStands =
permit !== null &&
stepCommands.filter((c) => c === "PERMIT2_PERMIT").length === 1 &&
sameAddress(permit.spender, toAddress) &&
!permit.allowedToFail &&
permit.beforeEveryTake &&
!stepCommands
.slice(permit.step + 1)
.includes("PERMIT2_PERMIT_BATCH");
// What the steps take that the Amount line does not give, as
// { token, amount }. A null token is a step this decoder did not read
// (see spentUnread()), or a token it does not know.
const spentFromSender = [];
let total = null;
if (permit === null || sameAddress(inputToken, permit.token)) {
total = inputAmount;
} 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.
spentFromSender.push({ token: inputToken, amount: inputAmount });
}
if (permit !== null) inputToken = permit.token;
for (const t of userPaid) {
const ofTokenIn = sameAddress(t.token, inputToken);
if (ofTokenIn && permitStands) continue;
if (
ofTokenIn &&
typeof total === "bigint" &&
typeof t.amount === "bigint"
) {
total += t.amount;
continue;
}
spentFromSender.push(t);
if (ofTokenIn && !present(t.amount)) total = null;
}
inputAmount = permitStands ? permit.amount : total;
// 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
// them would name Token In. What the other steps state is // them would replace the input side. What the other steps state is
// then not the whole swap, so both sides are set unknown, with no // then not the whole swap, so both sides are set unknown, with no
// figure, wherever the step stands. // figure, wherever the step stands.
if (hasSubPlan) { if (hasSubPlan) {
@@ -1887,9 +1845,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
return amountText(amount, info); return amountText(amount, info);
}; };
// A null token in `spentFromSender`, a step this decoder did not read // A step that may take from the sender what this decoder did not read,
// or a token it does not know, leaves no Amount figure, which may // a null token in `spentFromSender`, leaves no Amount figure (see
// then be too low. // spentUnread()).
if ( if (
present(inputAmount) && present(inputAmount) &&
spentFromSender.every((s) => present(s.token)) spentFromSender.every((s) => present(s.token))
+23 -138
View File
@@ -1720,9 +1720,8 @@ describe("uniswap decoder", () => {
]); ]);
}); });
// A PERMIT2_PERMIT in the plain shape of Uniswap's own routes keeps // A PERMIT2_PERMIT that sets the input side keeps its own amount: the
// its own amount: Permit2 lets the router take no more of the token, // steps it lets the router spend do not add to it.
// so the steps after it do not add to it.
test("a split after a PERMIT2_PERMIT shows the permit's amount", () => { test("a split after a PERMIT2_PERMIT shows the permit's amount", () => {
const data = buildExecute( const data = buildExecute(
solidityPacked(["uint8", "uint8", "uint8"], [0x0a, 0x00, 0x08]), solidityPacked(["uint8", "uint8", "uint8"], [0x0a, 0x00, 0x08]),
@@ -1753,36 +1752,27 @@ describe("uniswap decoder", () => {
expect(detail(result, "Moved from the sender")).toBeUndefined(); expect(detail(result, "Moved from the sender")).toBeUndefined();
}); });
// Out of that shape the permit caps nothing the Amount line can rely // A permit to another spender, or one the router lets fail (0x80 on
// on, so the line gives the total the steps take, as with no permit. // the command byte), caps nothing the router takes, so what each
const ONE_USDT_PERMIT = (spender) => // later step takes of its token gets its own lines.
encodePermit2(USDT_ADDR, 1000000n, spender);
test.each([ test.each([
{ {
kind: "to another spender", kind: "to another spender",
commands: [0x0a], command: 0x0a,
permits: [ONE_USDT_PERMIT(OTHER_ADDR)], spender: OTHER_ADDR,
}, },
{ {
kind: "that the router lets fail", kind: "that the router lets fail",
commands: [0x8a], command: 0x8a,
permits: [ONE_USDT_PERMIT(ROUTER_ADDR)], spender: ROUTER_ADDR,
},
{
kind: "followed by a second one",
commands: [0x0a, 0x0a],
permits: [
ONE_USDT_PERMIT(ROUTER_ADDR),
ONE_USDT_PERMIT(ROUTER_ADDR),
],
}, },
])( ])(
"a split after a PERMIT2_PERMIT $kind shows the total the steps take", "a split after a PERMIT2_PERMIT $kind shows what each step takes",
({ commands, permits }) => { ({ command, spender }) => {
const result = decodeSteps( const result = decodeSteps(
[...commands, 0x00, 0x08], [command, 0x00, 0x08],
[ [
...permits, encodePermit2(USDT_ADDR, 1000000n, spender), // 1 USDT
V3_600, V3_600,
encodeV2SwapExactIn(USER_ADDR, USDT_400, 390000000n, [ encodeV2SwapExactIn(USER_ADDR, USDT_400, 390000000n, [
USDT_ADDR, USDT_ADDR,
@@ -1790,126 +1780,21 @@ describe("uniswap decoder", () => {
]), ]),
], ],
); );
// The V3 step names its token as its path spells it, in
// lower case.
const moved = ["Moved to", "The swap step that spends it"];
expect(linesBetweenTokens(result)).toEqual([ expect(linesBetweenTokens(result)).toEqual([
["Amount", "1000.0000 USDT"], ["Amount", "1.0000 USDT"],
["Moved from the sender", "600.0000 USDT"],
["Token moved", "USDT (" + USDT_ADDR.toLowerCase() + ")"],
moved,
["Moved from the sender", "400.0000 USDT"],
["Token moved", "USDT (" + USDT_ADDR + ")"],
moved,
]); ]);
}, },
); );
// A PERMIT2_PERMIT_BATCH, which this decoder does not read, may raise
// what the router may take of the permit's token again.
test("a split after a PERMIT2_PERMIT and a PERMIT2_PERMIT_BATCH shows the total the steps take", () => {
const permitBatch = coder.encode(
[
"tuple(tuple(address,uint160,uint48,uint48)[],address,uint256)",
"bytes",
],
[
[
[[USDT_ADDR, 10000000000n, 0, 0]], // 10,000 USDT
ROUTER_ADDR,
9999999999,
],
"0x1234",
],
);
const result = decodeSteps(
[0x0a, 0x03, 0x00, 0x08],
[
encodePermit2(USDT_ADDR, USDT_600, ROUTER_ADDR),
permitBatch,
V3_600,
encodeV2SwapExactIn(USER_ADDR, 9400000000n, 1n, [
USDT_ADDR,
USDC_ADDR,
]),
],
);
expect(linesBetweenTokens(result)).toEqual([
["Amount", "10000.0000 USDT"],
]);
});
// 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.
const V2_EXACT_OUT_400 = encodeV2SwapExactOut(
USER_ADDR,
390000000n,
USDT_400,
[USDT_ADDR, USDC_ADDR],
);
const UP_TO_400_USDT = [
["Moved from the sender", "Up to 400.0000 USDT"],
["Token moved", "USDT (" + USDT_ADDR + ")"],
["Moved to", "The swap step that spends it"],
];
test.each([
{
kind: "a V4 exact-out step with a SETTLE of OPEN_DELTA by the user",
commands: [0x10, 0x09],
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",
commands: [0x10, 0x09],
first: encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_SETTLE_ID,
V4_TAKE_ALL_ID,
]),
[
encodeV4ExactInSingle(USDT_ADDR, USDC_ADDR, 0n, 1n),
encodeV4Settle(USDT_ADDR),
encodeV4TakeAll(USDC_ADDR, 0n),
],
),
lines: [
["Amount", "All available (V4 open delta)"],
...UP_TO_400_USDT,
],
},
{
kind: "a V2 exact-in step of ALREADY_PAID",
commands: [0x08, 0x09],
first: encodeV2SwapExactIn(USER_ADDR, 0n, 1n, [
USDT_ADDR,
USDC_ADDR,
]),
lines: [
[
"Amount",
"Whatever an earlier step sent to the pair (V2 already paid)",
],
...UP_TO_400_USDT,
],
},
])(
"$kind, then a V2 exact-out of at most 400 USDT the user pays for, shows the 400 USDT",
({ commands, first, lines }) => {
const result = decodeSteps(commands, [first, V2_EXACT_OUT_400]);
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", () => {
+26 -9
View File
@@ -160,8 +160,8 @@ function transfer(command, token, recipient, amount) {
]; ];
} }
// PERMIT2_PERMIT (0x0a) of `amount` of `token`, for the router to spend. // PERMIT2_PERMIT (0x0a) of `amount` of `token`, for `spender` to spend.
function permit2Permit(token, amount) { function permit2Permit(token, amount, spender) {
return [ return [
0x0a, 0x0a,
coder.encode( coder.encode(
@@ -169,7 +169,7 @@ function permit2Permit(token, amount) {
"tuple(tuple(address,uint160,uint48,uint48),address,uint256)", "tuple(tuple(address,uint160,uint48,uint48),address,uint256)",
"bytes", "bytes",
], ],
[[[token, amount, 0, 0], ROUTER, 0n], "0x"], [[[token, amount, 0, 0], spender, 0n], "0x"],
), ),
]; ];
} }
@@ -2255,6 +2255,10 @@ describe("the allow-revert flag", () => {
}); });
// Only the routers from UniversalRouterV2 on let a PERMIT2_PERMIT fail. // Only the routers from UniversalRouterV2 on let a PERMIT2_PERMIT fail.
// A permit to the router caps what the router takes of its token after
// it, unless the router lets it fail: then what the swap takes gets its
// own lines (https://git.eeqj.de/sneak/AutistMask/issues/625). The swap
// otherwise reads as without the flag.
test.each([ test.each([
{ router: "UniversalRouterV1", address: ROUTER_V1, mark: "" }, { router: "UniversalRouterV1", address: ROUTER_V1, mark: "" },
{ {
@@ -2266,17 +2270,19 @@ describe("the allow-revert flag", () => {
router: "UniversalRouterV2", router: "UniversalRouterV2",
address: ROUTER, address: ROUTER,
mark: " (allowed to fail)", mark: " (allowed to fail)",
taken: "2000.0000 USDT",
}, },
{ {
router: "UniversalRouterV2_1_2", router: "UniversalRouterV2_1_2",
address: ROUTER_V2_1_2, address: ROUTER_V2_1_2,
mark: " (allowed to fail)", mark: " (allowed to fail)",
taken: "2000.0000 USDT",
}, },
])( ])(
"on $router, a PERMIT2_PERMIT with the flag is listed as 'Permit2 Permit$mark', and the swap reads as without the flag", "on $router, a PERMIT2_PERMIT with the flag is listed as 'Permit2 Permit$mark'",
({ address, mark }) => { ({ address, mark, taken }) => {
const steps = [ const steps = [
permit2Permit(USDT, USDT_2000), permit2Permit(USDT, USDT_2000, address),
swapToWeth(MSG_SENDER), swapToWeth(MSG_SENDER),
]; ];
const plain = decode(execute(...steps), USER, address); const plain = decode(execute(...steps), USER, address);
@@ -2288,9 +2294,20 @@ describe("the allow-revert flag", () => {
expect(detail(withFlag, "Steps").value).toBe( expect(detail(withFlag, "Steps").value).toBe(
"Permit2 Permit" + mark + " → V3 Swap (Exact In)", "Permit2 Permit" + mark + " → V3 Swap (Exact In)",
); );
const withoutSteps = (r) => const takenLines = [
r.details.filter((d) => d.label !== "Steps"); "Moved from the sender",
expect(withoutSteps(withFlag)).toEqual(withoutSteps(plain)); "Token moved",
"Moved to",
];
const rest = (r) =>
r.details.filter(
(d) => d.label !== "Steps" && !takenLines.includes(d.label),
);
expect(rest(withFlag)).toEqual(rest(plain));
expect(detail(plain, "Moved from the sender")).toBeUndefined();
expect(detail(withFlag, "Moved from the sender")?.value).toBe(
taken,
);
expect(detail(withFlag, "Min. received").value).toBe("1.0000 WETH"); expect(detail(withFlag, "Min. received").value).toBe("1.0000 WETH");
}, },
); );