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
transaction"
- Decoded action (if calldata is recognized): action name, token details,
amounts, what each swap step spends that the `Amount` line does not give,
what each `PERMIT2_TRANSFER_FROM` moves and where to, steps, deadline, and
unless a swap pays only the From address, a `Paid to` line for each other
address it pays, or saying that a token stays in the router or who it pays
is unknown (see Transaction Decoding)
amounts, what each swap step the user pays for spends of a token other
than `Token In`, what each `PERMIT2_TRANSFER_FROM` moves and where to,
steps, deadline, and unless a swap pays only the From address, a `Paid to`
line for each other address it pays, or saying that a token stays in the
router or who it pays is unknown (see Transaction Decoding)
- From: color dot + full address + etherscan link
- Contract: color dot + full address + etherscan link, token symbol label if
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
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
minimum reads `Min. paid out`. The `Amount` line gives the total the steps
take of `Token In`, from the step that sets the input side on: the first V2 or
V3 exact-in, V2 exact-out, `WRAP_ETH` or V4 swap step that names an input
token or amount. A V4 step with a swap action sets the input side when no
earlier step set it, even when it names neither (see Display Consistency).
That step counts its own amount, whoever pays it: a V2 exact-out step its
`amountInMax`, and a V4 swap step the `amountIn` of the swap action that
spends its input currency. Each later V2 or V3 exact-in or V2 exact-out step
whose `payerIsUser` is true takes its amount, found in the same way, from the
user's wallet. So does every V4 step but the one that sets the input side,
with a swap action or without: it takes what its `SETTLE`s paid by the user
(`payerIsUser` true) and its `SETTLE_ALL`s pay, for each token on its own. A
`SETTLE` of any amount but `OPEN_DELTA` pays that amount, and a `SETTLE` of
`OPEN_DELTA` or a `SETTLE_ALL` pays what the step owes, as above. A later step
paid from the router spends what the router holds and takes nothing, and so
does a V2 exact-in whose `amountIn` is `ALREADY_PAID`. A later take of
`Token In` is added to the total while both are quantities: a V3 exact-in of
minimum reads `Min. paid out`. The `Amount` line gives the amount of the step
that sets the input side: the last `PERMIT2_PERMIT` step if there is one,
otherwise the first V2 or V3 exact-in, V2 exact-out, `WRAP_ETH` or V4 swap
step that names an input token or amount. A V4 step with a swap action sets
the input side when no earlier step set it, even when it names neither (see
Display Consistency). A V2 exact-out step gives its `amountInMax`, and a V4
swap step the `amountIn` of the swap action that spends its input currency.
Each later V2 or V3 exact-in or V2 exact-out step whose `payerIsUser` is true
also takes its amount, found in the same way, from the user's wallet. So does
every V4 step but the one that sets the input side, with a swap action or
without: it takes what its `SETTLE`s paid by the user (`payerIsUser` true) and
its `SETTLE_ALL`s pay, for each token on its own. A `SETTLE` of any amount but
`OPEN_DELTA` pays that amount, and a `SETTLE` of `OPEN_DELTA` or a
`SETTLE_ALL` pays what the step owes, as above. What a step takes of
`Token In` is added to the line's, unless a `PERMIT2_PERMIT` set the input
side, whose amount the line keeps. A permit whose spender is the router and
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
`1000.0000 USDT`. When the decoder does not know a take's amount, the total is
not known and there is no `Amount` line. `Token In` is the token of the last
`PERMIT2_PERMIT` the decoder reads, when there is one. Its amount, `Unlimited`
included, 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 takes
its token or a token the decoder does not know, 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. 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 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;
`1000.0000 USDT`. When either amount is not a quantity (a V4 open delta,
`ALREADY_PAID`, or an amount the decoder does not know), the total is not
known and there is no `Amount` line. A step paid from the router spends what
the router holds and adds nothing, and so does a V2 exact-in whose `amountIn`
is `ALREADY_PAID`, which takes nothing from the user. What a step takes of a
token other than `Token In`, or of `Token In` when the decoder does not know
the amount or when a permit that caps nothing set the input side, gives three
lines after the `Amount` line (after `Token In` when there is none), in step
order: `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; and every `PERMIT2_TRANSFER_FROM_BATCH`. 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
`Moved to`, which reads `The swap step that spends it`. 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;
and every `PERMIT2_TRANSFER_FROM_BATCH`. Each such step gives one
`Moved from the sender` line, among those lines in step order, reading
`Unknown (could not be decoded)`, and there is then 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
`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
+62 -104
View File
@@ -357,11 +357,6 @@ function present(value) {
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:
// v4-periphery `src/libraries/ActionConstants.sol` declares
// `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 inputAmount = null;
let inputEstablished = false;
// The last PERMIT2_PERMIT this decoder read, or null (see
// permitStands after the loop).
let permit = null;
let inputFromPermit = false;
let permitCapsInput = false;
let outputToken = null;
let minOutput = null;
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
// 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
// from a different hop. A PERMIT2_PERMIT sets no side here: after the
// loop, the last one read names Token In, and the Amount line is
// formed there, in one place (see permitStands).
// from a different hop. Each later swap step the user pays for adds
// what it spends, in addUserPaid(), the one place the total is formed,
// and a step that may take what this decoder did not read leaves no
// total at all (see spentUnread()).
const setInput = (token, amount) => {
inputToken = present(token) ? token : null;
inputAmount = present(amount) ? amount : null;
@@ -1216,25 +1211,53 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
outputEstablished = true;
};
// What each step but the one that sets the input side takes from the
// sender, as { token, amount }, in step order. After the loop each
// joins the Amount line or gets lines of its own (see permitStands).
const userPaid = [];
// A swap step the user pays for (`payerIsUser` true) takes `amount`
// of `token` from the sender. A V2 exact-in of ALREADY_PAID takes
// nothing.
// What steps take from the sender that the Amount line does not give,
// as { token, amount }, in step order (see addUserPaid()). A step that
// may take from the sender what this decoder did not read is here
// with a null token (see spentUnread()).
const spentFromSender = [];
// A swap step the user pays for (`payerIsUser` true), after the input
// 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) => {
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
// 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
// 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
// amount: one line says so, and there is no Amount line, whose figure
// may then be too low.
// PERMIT2_TRANSFER_FROM_BATCH. One line says so, and there is no
// Amount line, whose figure may then be too low.
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.
@@ -1373,22 +1396,19 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
payeeUnread = true;
} else if (command === "PERMIT2_PERMIT") {
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) {
// Whether a step before it took its token, or may
// have: the step that set the input side or one in
// `userPaid`, when its token is the permit's or not
// known.
const mayBeItsToken = (token) =>
!present(token) || sameAddress(token, p.token);
const tookBefore =
(inputEstablished && mayBeItsToken(inputToken)) ||
userPaid.some((t) => mayBeItsToken(t.token));
permit = {
...p,
step: i,
allowedToFail,
beforeEveryTake: !tookBefore,
};
setInput(p.token, p.amount);
inputFromPermit = true;
// Permit2 lets the spender take no more of the token
// than the permit's amount. Only a permit to this
// router that the router does not let fail caps what
// the later steps take (see addUserPaid()).
permitCapsInput =
!allowedToFail &&
p.spender.toLowerCase() === toAddress.toLowerCase();
}
} else if (command === "PERMIT2_TRANSFER_FROM") {
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
// 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
// figure, wherever the step stands.
if (hasSubPlan) {
@@ -1887,9 +1845,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
return amountText(amount, info);
};
// A null token in `spentFromSender`, a step this decoder did not read
// or a token it does not know, leaves no Amount figure, which may
// then be too low.
// A step that may take from the sender what this decoder did not read,
// a null token in `spentFromSender`, leaves no Amount figure (see
// spentUnread()).
if (
present(inputAmount) &&
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
// its own amount: Permit2 lets the router take no more of the token,
// so the steps after it do not add to it.
// A PERMIT2_PERMIT that sets the input side keeps its own amount: the
// steps it lets the router spend do not add to it.
test("a split after a PERMIT2_PERMIT shows the permit's amount", () => {
const data = buildExecute(
solidityPacked(["uint8", "uint8", "uint8"], [0x0a, 0x00, 0x08]),
@@ -1753,36 +1752,27 @@ describe("uniswap decoder", () => {
expect(detail(result, "Moved from the sender")).toBeUndefined();
});
// Out of that shape the permit caps nothing the Amount line can rely
// on, so the line gives the total the steps take, as with no permit.
const ONE_USDT_PERMIT = (spender) =>
encodePermit2(USDT_ADDR, 1000000n, spender);
// A permit to another spender, or one the router lets fail (0x80 on
// the command byte), caps nothing the router takes, so what each
// later step takes of its token gets its own lines.
test.each([
{
kind: "to another spender",
commands: [0x0a],
permits: [ONE_USDT_PERMIT(OTHER_ADDR)],
command: 0x0a,
spender: OTHER_ADDR,
},
{
kind: "that the router lets fail",
commands: [0x8a],
permits: [ONE_USDT_PERMIT(ROUTER_ADDR)],
},
{
kind: "followed by a second one",
commands: [0x0a, 0x0a],
permits: [
ONE_USDT_PERMIT(ROUTER_ADDR),
ONE_USDT_PERMIT(ROUTER_ADDR),
],
command: 0x8a,
spender: ROUTER_ADDR,
},
])(
"a split after a PERMIT2_PERMIT $kind shows the total the steps take",
({ commands, permits }) => {
"a split after a PERMIT2_PERMIT $kind shows what each step takes",
({ command, spender }) => {
const result = decodeSteps(
[...commands, 0x00, 0x08],
[command, 0x00, 0x08],
[
...permits,
encodePermit2(USDT_ADDR, 1000000n, spender), // 1 USDT
V3_600,
encodeV2SwapExactIn(USER_ADDR, USDT_400, 390000000n, [
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([
["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,
// the total included.
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.
function permit2Permit(token, amount) {
// PERMIT2_PERMIT (0x0a) of `amount` of `token`, for `spender` to spend.
function permit2Permit(token, amount, spender) {
return [
0x0a,
coder.encode(
@@ -169,7 +169,7 @@ function permit2Permit(token, amount) {
"tuple(tuple(address,uint160,uint48,uint48),address,uint256)",
"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.
// 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([
{ router: "UniversalRouterV1", address: ROUTER_V1, mark: "" },
{
@@ -2266,17 +2270,19 @@ describe("the allow-revert flag", () => {
router: "UniversalRouterV2",
address: ROUTER,
mark: " (allowed to fail)",
taken: "2000.0000 USDT",
},
{
router: "UniversalRouterV2_1_2",
address: ROUTER_V2_1_2,
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",
({ address, mark }) => {
"on $router, a PERMIT2_PERMIT with the flag is listed as 'Permit2 Permit$mark'",
({ address, mark, taken }) => {
const steps = [
permit2Permit(USDT, USDT_2000),
permit2Permit(USDT, USDT_2000, address),
swapToWeth(MSG_SENDER),
];
const plain = decode(execute(...steps), USER, address);
@@ -2288,9 +2294,20 @@ describe("the allow-revert flag", () => {
expect(detail(withFlag, "Steps").value).toBe(
"Permit2 Permit" + mark + " → V3 Swap (Exact In)",
);
const withoutSteps = (r) =>
r.details.filter((d) => d.label !== "Steps");
expect(withoutSteps(withFlag)).toEqual(withoutSteps(plain));
const takenLines = [
"Moved from the sender",
"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");
},
);