Author SHA1 Message Date
sneak cce6640d23 fix: the swap decoder names where a WRAP_ETH pays and shows a V4 step's ETH as a maximum when the router may hold ETH (closes #665)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
A `WRAP_ETH` that sets the input side and pays its WETH to anyone but
the router names that recipient on its `Moved to` line.

V4 pays a `SETTLE` of ETH from the router's own ETH, whichever payer it
names. When an earlier step may leave ETH in the router, the ETH that
the user pays a later V4 step gets lines of its own. Their
`Moved from the sender` line reads `Up to <amount>`, or
`Unknown (could not be decoded)` when the amount is not known. The
decoder never subtracts the router's ETH, so no line shows less ETH
than can leave the wallet. Without such an earlier step, the lines read
as before.

Model: opus-5-5
2026-10-27 01:46:35 +00:00
3 changed files with 434 additions and 234 deletions
+121 -123
View File
@@ -1265,7 +1265,9 @@ see Transaction Decoding), do not always read as a number. They can read:
the same words. `Unlimited`, `All available (V4 open delta)` and the same words. `Unlimited`, `All available (V4 open delta)` and
`Whatever an earlier step sent to the pair (V2 already paid)` keep their `Whatever an earlier step sent to the pair (V2 already paid)` keep their
wording. When a V2 or V3 exact-out step sets `Min. received`, that line shows wording. When a V2 or V3 exact-out step sets `Min. received`, that line shows
its `amountOut`, the exact amount it buys. its `amountOut`, the exact amount it buys. When the router may hold ETH from
an earlier step, the `Moved from the sender` line of ETH that the user pays a
V4 step also reads `Up to <amount>` (see Transaction Decoding).
- `All available (V4 open delta)`: the swap's `Amount` line, when the amount it - `All available (V4 open delta)`: the swap's `Amount` line, when the amount it
shows is a V4 exact-in `amountIn` of zero. V4 reads that zero as "use the shows is a V4 exact-in `amountIn` of zero. V4 reads that zero as "use the
whole open delta", so the calldata states no quantity. Transaction Decoding whole open delta", so the calldata states no quantity. Transaction Decoding
@@ -3412,47 +3414,43 @@ Currently supported:
as a swap paid in ETH does, but only when that amount is not as a swap paid in ETH does, but only when that amount is not
`CONTRACT_BALANCE` and the router held no WETH from an earlier step when the `CONTRACT_BALANCE` and the router held no WETH from an earlier step when the
`WRAP_ETH` ran: after two `WRAP_ETH`s of 0.5 ETH into the router, a swap of 1 `WRAP_ETH` ran: after two `WRAP_ETH`s of 0.5 ETH into the router, a swap of 1
WETH from it reads as leaving the WETH there. An `UNWRAP_WETH` into the router WETH from it reads as leaving the WETH there. A V2 or V3 exact-out step paid
turns all the router's WETH into as much ETH. The decoder then knows how much from the router spends an amount the calldata does not state, at most its
ETH the router holds only when it knew the WETH's amount and the router held `amountInMax`, so the decoder counts it as spending nothing the router holds.
no ETH before. A V4 `SETTLE` of ETH spends the ETH the router holds, whoever A `SETTLE` of `OPEN_DELTA` (zero), and a `SETTLE_ALL`, pay only what the step
pays it (see below). A V2 or V3 exact-out step paid from the router spends an still owes of their token when they run. The decoder takes that to be the
amount the calldata does not state, at most its `amountInMax`, so the decoder amounts the step's swap actions state they spend of that token only when every
counts it as spending nothing the router holds. A `SETTLE` of `OPEN_DELTA` action in the step could be read, every swap action names the token it spends
(zero), and a `SETTLE_ALL`, pay only what the step still owes of their token and the token it buys (a swap action with an empty path swaps nothing), the
when they run. The decoder takes that to be the amounts the step's swap `SETTLE` or `SETTLE_ALL` is the step's only `SETTLE` or `SETTLE_ALL` of that
actions state they spend of that token only when every action in the step token and comes after every swap action, no swap action buys that token, no
could be read, every swap action names the token it spends and the token it `TAKE`, `TAKE_ALL` or `TAKE_PORTION` in the step takes it (a `TAKE` of a
buys (a swap action with an empty path swaps nothing), the `SETTLE` or stated amount adds to what the step owes), and the swap actions that spend it
`SETTLE_ALL` is the step's only `SETTLE` or `SETTLE_ALL` of that token and state a quantity together; otherwise it does not know the amount, and such a
comes after every swap action, no swap action buys that token, no `TAKE`, `SETTLE` from the router counts as spending nothing. So the rest of a V4
`TAKE_ALL` or `TAKE_PORTION` in the step takes it (a `TAKE` of a stated amount `SETTLE` of more than its swap spends, taken to the router, the WETH of a
adds to what the step owes), and the swap actions that spend it state a `WRAP_ETH`, and a token a swap pays the router on the way to the output, each
quantity together; otherwise it does not know the amount, and such a `SETTLE` stays in the router when later steps do not spend all of it, where anyone can
from the router counts as spending nothing. So the rest of a V4 `SETTLE` of take it in a later transaction. The router sends ETH as written, so a
more than its swap spends, taken to the router, the WETH of a `WRAP_ETH`, and `TRANSFER` of `CONTRACT_BALANCE` of ETH always reverts and pays out nothing. A
a token a swap pays the router on the way to the output, each stays in the step paying a token to the V2 pair that the very next step spends, as a V2
router when later steps do not spend all of it, where anyone can take it in a exact-in of that token whose `amountIn` is `ALREADY_PAID` (zero) or
later transaction. The router sends ETH as written, so a `TRANSFER` of `CONTRACT_BALANCE`, is a hop, not a payout: Uniswap's mixed routes pay the
`CONTRACT_BALANCE` of ETH always reverts and pays out nothing. A step paying a next V2 pair directly, and the router enforces that swap's own minimum. The
token to the V2 pair that the very next step spends, as a V2 exact-in of that decoder checks that the address paid is that pair, deriving it from the V2
token whose `amountIn` is `ALREADY_PAID` (zero) or `CONTRACT_BALANCE`, is a factory and pair init code hash the network's routers were deployed with and
hop, not a payout: Uniswap's mixed routes pay the next V2 pair directly, and the first two tokens of that step's path; a payment to any other address is
the router enforces that swap's own minimum. The decoder checks that the named. (Sepolia's `UniversalRouterV1_2_V2Support` was deployed with
address paid is that pair, deriving it from the V2 factory and pair init code placeholders for both values, so no V2 swap step can succeed on that router.)
hash the network's routers were deployed with and the first two tokens of that Every other payment counts, in whichever step and of whatever token, a share
step's path; a payment to any other address is named. (Sepolia's of the output included: an interface fee paid to another address by
`UniversalRouterV1_2_V2Support` was deployed with placeholders for both `PAY_PORTION`, `TRANSFER` or `TAKE_PORTION` is named, and the minimum reads
values, so no V2 swap step can succeed on that router.) Every other payment `Min. paid out`, because the swap's minimum includes the fee. The minimum
counts, in whichever step and of whatever token, a share of the output reads `Min. received` only when some step pays the transaction's sender (the
included: an interface fee paid to another address by `PAY_PORTION`, From address on TxApproval, the sender the explorer reports on
`TRANSFER` or `TAKE_PORTION` is named, and the minimum reads `Min. paid out`, TransactionDetail), no `Paid to` line is shown, and no permit or batch grants
because the swap's minimum includes the fee. The minimum reads `Min. received` an allowance to a spender other than the router (see below). `Paid to` lines
only when some step pays the transaction's sender (the From address on after `Token Out` name each other address paid; one reads
TxApproval, the sender the explorer reports on TransactionDetail), no
`Paid to` line is shown, and no permit or batch grants an allowance to a
spender other than the router (see below). `Paid to` lines after `Token Out`
name each other address paid; one reads
`The bridge, which pays the deposit's recipient on another network` for a `The bridge, which pays the deposit's recipient on another network` for a
bridge deposit; one reads, for example, bridge deposit; one reads, for example,
`The router, 2000.0000 USDT from the sender (later steps may not spend all of it)` `The router, 2000.0000 USDT from the sender (later steps may not spend all of it)`
@@ -3481,89 +3479,89 @@ Currently supported:
of any amount but `OPEN_DELTA` pays that amount, and a `SETTLE` of 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. So a V4 step `OPEN_DELTA` or a `SETTLE_ALL` pays what the step owes, as above. So a V4 step
that swaps 400 USDT, with a `SETTLE` of 5,000 USDT paid by the user and the that swaps 400 USDT, with a `SETTLE` of 5,000 USDT paid by the user and the
rest taken to another address, reads `5000.0000 USDT`. V4 pays ETH from the rest taken to another address, reads `5000.0000 USDT`. V4 pays a `SETTLE` of
router's own ETH, whoever a `SETTLE` names as payer. So when a V4 step would ETH from the router's own ETH, whichever payer it names. So when the router
take ETH from the user's wallet, ETH the router already holds pays first. The may hold ETH from an earlier step (see above), the ETH that the user pays a V4
wallet pays only the rest. For example, an `UNWRAP_WETH` into the router step gets lines of its own (see below). A later step paid from the router
leaves ETH there. When the decoder does not know how much ETH the router spends what the router holds and takes nothing, and so does a V2 exact-in
holds, or how much the step would take, it counts the router as paying all of whose `amountIn` is `ALREADY_PAID`. A later take of `Token In` is added to the
that ETH. A later step paid from the router spends what the router holds and total while both are quantities: a V3 exact-in of 600 USDT and a V2 exact-in
takes nothing, and so does a V2 exact-in whose `amountIn` is `ALREADY_PAID`. A of 400 USDT, both paid by the user, read `1000.0000 USDT`. When the decoder
later take of `Token In` is added to the total while both are quantities: a V3 does not know a take's amount, the total is not known and there is no `Amount`
exact-in of 600 USDT and a V2 exact-in of 400 USDT, both paid by the user, line. A `PERMIT2_PERMIT` names `Token In` only when its amount, `Unlimited`
read `1000.0000 USDT`. When the decoder does not know a take's amount, the included, stands as the `Amount` figure in place of the total, which it does
total is not known and there is no `Amount` line. A `PERMIT2_PERMIT` names only in the plain shape of Uniswap's own routes: it comes before the step that
`Token In` only when its amount, `Unlimited` included, stands as the `Amount` sets the input side, it is the only `PERMIT2_PERMIT` in the transaction and
figure in place of the total, which it does only in the plain shape of there is no `PERMIT2_PERMIT_BATCH`, its spender is the router, the router may
Uniswap's own routes: it comes before the step that sets the input side, it is not let it fail, no step before it takes its token, a `PERMIT2_TRANSFER_FROM`
the only `PERMIT2_PERMIT` in the transaction and there is no of it included, or a token the decoder does not know, and the decoder reads
`PERMIT2_PERMIT_BATCH`, its spender is the router, the router may not let it every step after it in full. Permit2 then lets the router take no more of the
fail, no step before it takes its token, a `PERMIT2_TRANSFER_FROM` of it token than that amount, so the permit covers every take of it. A step before
included, or a token the decoder does not know, and the decoder reads every the permit takes under whatever allowance the router already had, so it may
step after it in full. Permit2 then lets the router take no more of the token take more than the permit's amount, which would then read lower than what
than that amount, so the permit covers every take of it. A step before the leaves the user's wallet: a `PERMIT2_TRANSFER_FROM` of 2,000 USDT into the
permit takes under whatever allowance the router already had, so it may take router, then a permit of 600 USDT to the router and a V3 exact-in of 2,000
more than the permit's amount, which would then read lower than what leaves USDT paid from the router, read `Amount 2000.0000 USDT`. A later permit or
the user's wallet: a `PERMIT2_TRANSFER_FROM` of 2,000 USDT into the router, batch may raise what Permit2 lets the router take, and so may a step the
then a permit of 600 USDT to the router and a V3 exact-in of 2,000 USDT paid decoder does not read in full: a command the decoder does not read (listed at
from the router, read `Amount 2000.0000 USDT`. A later permit or batch may the end of this section), a step whose input it cannot read, in whole or in
raise what Permit2 lets the router take, and so may a step the decoder does part, and a step it fails on. Every other `PERMIT2_PERMIT`, before the step
not read in full: a command the decoder does not read (listed at the end of that sets the input side or after it, leaves `Token In` and the `Amount` line
this section), a step whose input it cannot read, in whole or in part, and a as they read with no permit, unless it lets a spender other than the router
step it fails on. Every other `PERMIT2_PERMIT`, before the step that sets the take `Token In` (see below): a permit of 5,000 DAI to another address and then
input side or after it, leaves `Token In` and the `Amount` line as they read a V3 exact-in of 600 USDT read `Swap USDT → USDC` with `Amount 600.0000 USDT`,
with no permit, unless it lets a spender other than the router take `Token In` and so do that exact-in and then a permit of 5,000 DAI to the router. When a
(see below): a permit of 5,000 DAI to another address and then a V3 exact-in permit's amount stands and the step that sets the input side spends a token
of 600 USDT read `Swap USDT → USDC` with `Amount 600.0000 USDT`, and so do other than the permit's, or one the decoder does not know, that step's amount
that exact-in and then a permit of 5,000 DAI to the router. When a permit's gets lines of its own, unless it states no amount and the user pays it none of
amount stands and the step that sets the input side spends a token other than its input token. Each take the total does not count gives three lines after
the permit's, or one the decoder does not know, that step's amount gets lines the `Amount` line (after `Token In` when there is none): one of a token other
of its own, unless it states no amount and the user pays it none of its input than `Token In`; one of `Token In` after an amount that is not a quantity (a
token. Each take the total does not count gives three lines after the `Amount` V4 open delta, `ALREADY_PAID`, which keep their wording on the `Amount` line,
line (after `Token In` when there is none): one of a token other than or no stated amount, as of a V4 exact-out step); one whose amount the decoder
`Token In`; one of `Token In` after an amount that is not a quantity (a V4 does not know; and one of ETH that the user pays a V4 step when the router may
open delta, `ALREADY_PAID`, which keep their wording on the `Amount` line, or hold ETH. When there is no `Amount` line, each take the total counts gets them
no stated amount, as of a V4 exact-out step); and one whose amount the decoder too, so that every amount the decoder reads is shown: the amount the step that
does not know. When there is no `Amount` line, each take the total counts gets sets the input side states, when it is a quantity and the user pays that step
them too, so that every amount the decoder reads is shown: the amount the step (a V2 or V3 step whose `payerIsUser` is true, a V4 step the user pays some of
that sets the input side states, when it is a quantity and the user pays that its input token, or a `WRAP_ETH`, whose ETH comes from the transaction's
step (a V2 or V3 step whose `payerIsUser` is true, a V4 step the user pays value), and each later take of `Token In` added to it. A step that sets the
some of its input token, or a `WRAP_ETH`, whose ETH comes from the input side and is paid from the router takes nothing from the user's wallet
transaction's value), and each later take of `Token In` added to it. A step and gets none. A `PERMIT2_PERMIT` the decoder cannot read, then V3 exact-ins
that sets the input side and is paid from the router takes nothing from the of 600 USDT and 400 DAI paid by the user, give lines for both amounts. When
user's wallet and gets none. A `PERMIT2_PERMIT` the decoder cannot read, then the step that sets the input side states no amount, the user pays it some of
V3 exact-ins of 600 USDT and 400 DAI paid by the user, give lines for both its input token, and there is no `Amount` line, that step gets them too
amounts. When the step that sets the input side states no amount, the user whenever another take's lines give a figure, or a `PERMIT2_TRANSFER_FROM` of
pays it some of its input token, and there is no `Amount` line, that step gets its token gives one, on the lines below or, into the router, on its `Paid to`
them too whenever another take's lines give a figure, or a line, so that figure cannot read as all that leaves. When it states no amount
`PERMIT2_TRANSFER_FROM` of its token gives one, on the lines below or, into and the user pays it none of its input token, as when the router pays it, that
the router, on its `Paid to` line, so that figure cannot read as all that step takes none of that token from the user's wallet and never gets them, with
leaves. When it states no amount and the user pays it none of its input token, a permit or without. They come in step order, except that the step that sets
as when the router pays it, that step takes none of that token from the user's the input side comes first when it has them, followed, when there is no
wallet and never gets them, with a permit or without. They come in step order, `Amount` line, by the other takes the total counts: `Moved from the sender`,
except that the step that sets the input side comes first when it has them, its amount, worded as the `Amount` line words one, or
followed, when there is no `Amount` line, by the other takes the total counts:
`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; `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`. For a `WRAP_ETH` that `Moved to`, which reads `The swap step that spends it`. For ETH that the user
pays its WETH to anyone but the router, `Moved to` names that recipient pays a V4 step when the router may hold ETH, `Moved from the sender` reads
instead: `The sender` for `address(1)`, and otherwise the address. A token the `Up to <amount>` for a known amount. For a `WRAP_ETH` that pays its WETH to
decoder does not know gives the `Moved from the sender` line alone. A step may anyone but the router, `Moved to` names that recipient instead: `The sender`
also take from the user's wallet what the decoder did not read: a V4 step for `address(1)`, and otherwise the address. A token the decoder does not know
whose input or one of whose `SETTLE`s or `SETTLE_ALL`s it cannot read, which gives the `Moved from the sender` line alone. A step may also take from the
the router still runs; a V2 or V3 exact-in or exact-out step or a user's wallet what the decoder did not read: a V4 step whose input or one of
`PERMIT2_TRANSFER_FROM` whose input it cannot read, such as a V3 step whose whose `SETTLE`s or `SETTLE_ALL`s it cannot read, which the router still runs;
path is shorter than one token address or has bytes after its last hop; every a V2 or V3 exact-in or exact-out step or a `PERMIT2_TRANSFER_FROM` whose input
`PERMIT2_TRANSFER_FROM_BATCH`; every `EXECUTE_SUB_PLAN`, wherever it stands; it cannot read, such as a V3 step whose path is shorter than one token address
and every step it fails on. Each such step gives one `Moved from the sender` or has bytes after its last hop; every `PERMIT2_TRANSFER_FROM_BATCH`; every
line among those, reading `Unknown (could not be decoded)`. That line, or any `EXECUTE_SUB_PLAN`, wherever it stands; and every step it fails on. Each such
line for a token the decoder does not know, leaves no `Amount` line, whose step gives one `Moved from the sender` line among those, reading
figure might be too low, and such a step before a `PERMIT2_PERMIT` keeps the `Unknown (could not be decoded)`. That line, or any line for a token the
permit's amount from standing. Then each `PERMIT2_TRANSFER_FROM` but one into decoder does not know, leaves no `Amount` line, whose figure might be too low,
the router, in step order, gives three lines: `Moved from the sender`, the and such a step before a `PERMIT2_PERMIT` keeps the permit's amount from
amount it moves, for example `1000000.0000 USDT`; `Token moved`, giving the standing. Then each `PERMIT2_TRANSFER_FROM` but one into the router, in step
token's 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
+58 -78
View File
@@ -1340,15 +1340,18 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
}; };
// What the steps take from the sender beyond the amount of the step // What the steps take from the sender beyond the amount of the step
// that sets the input side, as { token, amount }, in step order. // that sets the input side, as { token, amount, atMost }, in step
// After the loop each joins the Amount line or gets lines of its own // order. After the loop each joins the Amount line or gets lines of
// (see permitStands). // its own (see permitStands). A take with `atMost` set takes at most
// its amount, and always gets lines of its own.
const userPaid = []; const userPaid = [];
// A swap step the user pays for (`payerIsUser` true) takes `amount` // A swap step the user pays for (`payerIsUser` true) takes `amount`
// of `token` from the sender. A V2 exact-in of ALREADY_PAID takes // of `token` from the sender, or at most that when `atMost` is set. A
// nothing. // V2 exact-in of ALREADY_PAID takes nothing.
const addUserPaid = (token, amount) => { const addUserPaid = (token, amount, atMost = false) => {
if (amount !== ALREADY_PAID) userPaid.push({ token, amount }); if (amount !== ALREADY_PAID) {
userPaid.push({ token, amount, atMost });
}
}; };
// 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 whose input or one of whose SETTLEs or SETTLE_ALLs it // V4 step whose input or one of whose SETTLEs or SETTLE_ALLs it
@@ -1403,11 +1406,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// token, an UNWRAP_WETH of WETH, a WRAP_ETH of CONTRACT_BALANCE of // token, an UNWRAP_WETH of WETH, a WRAP_ETH of CONTRACT_BALANCE of
// ETH, a TRANSFER, PAY_PORTION, PAY_PORTION_FULL_PRECISION or // ETH, a TRANSFER, PAY_PORTION, PAY_PORTION_FULL_PRECISION or
// ACROSS_V4_DEPOSIT_V3 of all of it, or a swap step or V4 SETTLE paid // ACROSS_V4_DEPOSIT_V3 of all of it, or a swap step or V4 SETTLE paid
// from the router (see spendFromRouter()). A V4 SETTLE of ETH paid by // from the router (see spendFromRouter()). The value is the amount the
// the user spends the router's ETH too (see the V4_SWAP branch). The // router holds, in base units, when a WRAP_ETH stated it, otherwise
// value is the amount the router holds, in base units, when a // null.
// WRAP_ETH stated it, otherwise null. An UNWRAP_WETH into the router
// turns that amount of WETH into the same amount of ETH.
const heldByRouter = new Map(); const heldByRouter = new Map();
const isRouter = (recipient) => { const isRouter = (recipient) => {
const r = recipient.toLowerCase(); const r = recipient.toLowerCase();
@@ -1415,8 +1416,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
}; };
// A swap step or V4 SETTLE pays `amount` of `token` from the router's // A swap step or V4 SETTLE pays `amount` of `token` from the router's
// own balance. CONTRACT_BALANCE pays all of it. Any other quantity // own balance. CONTRACT_BALANCE pays all of it. Any other quantity
// counts against a known amount the router holds, and the steps pay // counts against the amount a WRAP_ETH stated, and the steps pay all
// all of it once their amounts reach that one: a swap paid in ETH wraps // of it once their amounts reach that one: a swap paid in ETH wraps
// what its swap steps then spend. An amount that is not a quantity, // what its swap steps then spend. An amount that is not a quantity,
// such as the null decodeV4Swap() gives a SETTLE whose payment it // such as the null decodeV4Swap() gives a SETTLE whose payment it
// cannot tell, or one of a token whose amount is not known, pays out // cannot tell, or one of a token whose amount is not known, pays out
@@ -1824,45 +1825,20 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} else if (command === "V4_SWAP") { } else if (command === "V4_SWAP") {
const v4 = decodeV4Swap(inputs[i]); const v4 = decodeV4Swap(inputs[i]);
const inputSetBefore = inputEstablished; const inputSetBefore = inputEstablished;
// What the user pays the step, as decodeV4Swap() gives // V4Router pays a SETTLE of ETH from the router's own
// it, less the ETH the router already holds (below). // ETH, whichever payer it names. When the router may hold
// Empty when the step cannot be read. // ETH from an earlier step, that ETH may pay some of the
const paidByUser = []; // ETH that the user pays this step. The sender then pays
// at most the step's amount.
const routerMayHoldEth = heldByRouter.has(
"0x0000000000000000000000000000000000000000",
);
if (v4) { if (v4) {
// Each SETTLE paid from the router spends the amount // Each SETTLE paid from the router spends the amount
// decodeV4Swap() says it pays. // decodeV4Swap() says it pays.
for (const s of v4.settledFromRouter) { for (const s of v4.settledFromRouter) {
spendFromRouter(s.currency, s.amount); spendFromRouter(s.currency, s.amount);
} }
// V4Router pays ETH from the router's own ETH,
// whoever a SETTLE names as payer. So ETH the router
// holds from an earlier step pays first for the ETH
// the user would pay the step. The user pays only the
// rest. When this decoder does not know how much ETH
// the router holds, or how much the user would pay,
// it counts the router as paying all of that ETH.
for (const p of v4.paidByUser) {
if (
p.currency !==
"0x0000000000000000000000000000000000000000" ||
!heldByRouter.has(p.currency)
) {
paidByUser.push(p);
continue;
}
const held = heldByRouter.get(p.currency);
if (
typeof held === "bigint" &&
typeof p.amount === "bigint" &&
p.amount > held
) {
paidByUser.push({
currency: p.currency,
amount: p.amount - held,
});
}
spendFromRouter(p.currency, p.amount);
}
for (const t of v4.takes) { for (const t of v4.takes) {
if (!isHop(i, t.currency, t.recipient)) { if (!isHop(i, t.currency, t.recipient)) {
addPayee(t.recipient, t.currency); addPayee(t.recipient, t.currency);
@@ -1887,10 +1863,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
commandNames[i] += " (could not be decoded)"; commandNames[i] += " (could not be decoded)";
} }
// The step's amount, if it sets the input side: what the // The step's amount, if it sets the input side: what the
// user pays it of its input currency (see paidByUser), // user pays it of its input currency (see
// or, when the user pays none of it, what its swap // decodeV4Swap()), or, when the user pays none of it,
// actions spend of it, whoever pays that. // what its swap actions spend of it, whoever pays that.
const paidIn = paidByUser.find( const paidIn = v4?.paidByUser.find(
(p) => p.currency === v4.tokenIn, (p) => p.currency === v4.tokenIn,
); );
const amountIn = paidIn ? paidIn.amount : v4?.amountIn; const amountIn = paidIn ? paidIn.amount : v4?.amountIn;
@@ -1944,9 +1920,17 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// router still runs it. // router still runs it.
const setInputHere = inputEstablished && !inputSetBefore; const setInputHere = inputEstablished && !inputSetBefore;
if (setInputHere) inputPaidByUser = paidIn !== undefined; if (setInputHere) inputPaidByUser = paidIn !== undefined;
for (const p of paidByUser) { if (v4) {
if (setInputHere && p === paidIn) continue; for (const p of v4.paidByUser) {
addUserPaid(p.currency, p.amount); if (setInputHere && p === paidIn) continue;
addUserPaid(
p.currency,
p.amount,
routerMayHoldEth &&
p.currency ===
"0x0000000000000000000000000000000000000000",
);
}
} }
if (!v4 || v4.settleUnread) { if (!v4 || v4.settleUnread) {
spentUnread(); spentUnread();
@@ -1994,23 +1978,15 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
addPayee(u.recipient); addPayee(u.recipient);
// It pays out all the router's WETH, as ETH. Into the // It pays out all the router's WETH, as ETH. Into the
// router, the router then holds as ETH the WETH a // router, the router then holds as ETH the WETH a
// step paid it. That amount is known when the router // step paid it.
// held a known amount of WETH and no ETH before. const heldWeth = WETH_ADDRESSES.some((w) =>
const heldWeth = WETH_ADDRESSES.filter((w) =>
heldByRouter.has(w), heldByRouter.has(w),
); );
const ethAmount = WETH_ADDRESSES.forEach((w) => heldByRouter.delete(w));
heldWeth.length === 1 && if (isRouter(u.recipient) && heldWeth) {
!heldByRouter.has(
"0x0000000000000000000000000000000000000000",
)
? heldByRouter.get(heldWeth[0])
: null;
heldWeth.forEach((w) => heldByRouter.delete(w));
if (isRouter(u.recipient) && heldWeth.length > 0) {
heldByRouter.set( heldByRouter.set(
"0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000",
ethAmount, null,
); );
} }
} else { } else {
@@ -2064,7 +2040,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// being of another token or coming after an amount that is not a // being of another token or coming after an amount that is not a
// quantity (a V4 open delta, ALREADY_PAID, or none stated), gets // quantity (a V4 open delta, ALREADY_PAID, or none stated), gets
// lines of its own. One whose amount is not known also leaves the // lines of its own. One whose amount is not known also leaves the
// total unknown, with no figure. // total unknown, with no figure. A take of at most its amount (see
// userPaid) is never added, and gets lines of its own too.
// //
// A PERMIT2_PERMIT names Token In only when its amount stands as the // A PERMIT2_PERMIT names Token In only when its amount stands as the
// Amount figure in place of the total, which it does only in the // Amount figure in place of the total, which it does only in the
@@ -2099,10 +2076,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
permit.beforeEveryTake && permit.beforeEveryTake &&
!stepsUnread.some((step) => step > permit.step); !stepsUnread.some((step) => step > permit.step);
// What the steps take that the Amount line does not give, as // What the steps take that the Amount line does not give, as
// { token, amount, recipient }. A null token is a step this decoder // { token, amount, atMost, recipient }. A null token is a step this
// did not read (see spentUnread()), or a token it does not know. Only // decoder did not read (see spentUnread()), or a token it does not
// the take of a WRAP_ETH that pays elsewhere has a recipient (see // know. `atMost` is as in `userPaid`. Only the take of a WRAP_ETH that
// wrapEthRecipient). // pays elsewhere has a recipient (see wrapEthRecipient).
const spentFromSender = []; const spentFromSender = [];
let total = null; let total = null;
// The takes from the sender that `total` adds up, each a quantity, in // The takes from the sender that `total` adds up, each a quantity, in
@@ -2148,6 +2125,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
if (ofTokenIn && permitStands) continue; if (ofTokenIn && permitStands) continue;
if ( if (
ofTokenIn && ofTokenIn &&
!t.atMost &&
typeof total === "bigint" && typeof total === "bigint" &&
typeof t.amount === "bigint" typeof t.amount === "bigint"
) { ) {
@@ -2252,8 +2230,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// V2's already-paid zero, neither of which is a quantity at all (see // V2's already-paid zero, neither of which is a quantity at all (see
// OPEN_DELTA and ALREADY_PAID), and an unbounded permit. Those two // OPEN_DELTA and ALREADY_PAID), and an unbounded permit. Those two
// tests come first — the sentinels are not bigints and cannot be // tests come first — the sentinels are not bigints and cannot be
// compared with one. // compared with one. `atMost` says the amount is a maximum (see
const spentText = (amount, info) => { // userPaid).
const spentText = (amount, info, atMost = false) => {
if (amount === OPEN_DELTA) { if (amount === OPEN_DELTA) {
return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT }; return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
} }
@@ -2266,7 +2245,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
if (amount >= MAX_UINT160) { if (amount >= MAX_UINT160) {
return { raw: "Unlimited", display: "Unlimited" }; return { raw: "Unlimited", display: "Unlimited" };
} }
if (hasV2OrV3ExactOut) { if (hasV2OrV3ExactOut || atMost) {
// A V2 or V3 exact-out swap spends at most this figure, // A V2 or V3 exact-out swap spends at most this figure,
// whichever steps gave it (an amountInMax, the WRAP_ETH of a // whichever steps gave it (an amountInMax, the WRAP_ETH of a
// swap paid in ETH, a permit, or several steps' total), so it // swap paid in ETH, a permit, or several steps' total), so it
@@ -2320,13 +2299,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// of the lines below for a PERMIT2_TRANSFER_FROM. An amount this // of the lines below for a PERMIT2_TRANSFER_FROM. An amount this
// decoder does not know is said so, and a step it did not read gets // decoder does not know is said so, and a step it did not read gets
// that one line alone. A take with no recipient goes to the swap step // that one line alone. A take with no recipient goes to the swap step
// that spends it. A take with a recipient goes there, and MSG_SENDER // that spends it. A take with a recipient goes to that recipient;
// there is the sender. // `MSG_SENDER` means the sender.
for (const s of spentFromSender) { for (const s of spentFromSender) {
details.push({ details.push({
label: "Moved from the sender", label: "Moved from the sender",
value: present(s.amount) value: present(s.amount)
? spentText(s.amount, tokenInfo(s.token, sources)).display ? spentText(s.amount, tokenInfo(s.token, sources), s.atMost)
.display
: UNDECODED_CURRENCY, : UNDECODED_CURRENCY,
}); });
if (present(s.token)) { if (present(s.token)) {
+255 -33
View File
@@ -1428,8 +1428,8 @@ describe("uniswap decoder", () => {
]; ];
// execute() of these command bytes and inputs, decoded as sent by // execute() of these command bytes and inputs, decoded as sent by
// the user. // the user to `router`.
function decodeSteps(commands, inputs) { function decodeSteps(commands, inputs, router = ROUTER_ADDR) {
const data = buildExecute( const data = buildExecute(
solidityPacked( solidityPacked(
commands.map(() => "uint8"), commands.map(() => "uint8"),
@@ -1438,7 +1438,7 @@ describe("uniswap decoder", () => {
inputs, inputs,
9999999999n, 9999999999n,
); );
return uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR); return uniswap.decode(data, router, "0x1", {}, USER_ADDR);
} }
// The first step: a V3 exact-in of 600 USDT for USDC, paid by the // The first step: a V3 exact-in of 600 USDT for USDC, paid by the
@@ -2470,13 +2470,20 @@ describe("uniswap decoder", () => {
expect(linesBetweenTokens(result)).toEqual(lines); expect(linesBetweenTokens(result)).toEqual(lines);
}); });
// https://git.eeqj.de/sneak/AutistMask/issues/665 — V4Router pays ETH // https://git.eeqj.de/sneak/AutistMask/issues/665 — V4Router pays a
// from the router's own ETH, whoever a SETTLE names as payer. ETH the // SETTLE of ETH from the router's own ETH, whichever payer it names.
// router holds from an UNWRAP_WETH into it pays first, so the sender // After an earlier step that may leave ETH in the router, the ETH that
// pays only the ETH beyond it. On next each row also took the V4 // the user pays a V4 step reads as a maximum, on lines of its own.
// step's whole ETH from the sender: 1 ETH more in the first two, and // Every row reads at least the ETH that can leave the wallet. The two
// 1.5 ETH in the last. // rows marked "reads as on next" read the same on next. On next every
const unwrapIntoRouter = encodeWrapEth(ROUTER_ADDR, 0n); // UNWRAP_WETH, same encoding // other row added that ETH to the Amount line, or read it as an exact
// amount.
//
// ADDRESS_THIS is the router on every release.
const ADDRESS_THIS = "0x0000000000000000000000000000000000000002";
const WRAP_INTO_ROUTER = encodeWrapEth(ADDRESS_THIS, ONE_ETH);
// UNWRAP_WETH has the layout of WRAP_ETH.
const UNWRAP_INTO_ROUTER = encodeWrapEth(ADDRESS_THIS, 0n);
// A V4 step swapping `amount` of ETH for USDC, with a SETTLE of // A V4 step swapping `amount` of ETH for USDC, with a SETTLE of
// OPEN_DELTA by the user. // OPEN_DELTA by the user.
const v4EthForUsdc = (amount) => const v4EthForUsdc = (amount) =>
@@ -2492,42 +2499,257 @@ describe("uniswap decoder", () => {
encodeV4TakeAll(USDC_ADDR, 0n), encodeV4TakeAll(USDC_ADDR, 0n),
], ],
); );
const V4_ONE_ETH = v4EthForUsdc(ONE_ETH);
// A TRANSFER or PAY_PORTION of `amount` of `token` to another
// address.
const payOut = (token, amount) =>
coder.encode(
["address", "address", "uint256"],
[token, OTHER_ADDR, amount],
);
// An ACROSS_V4_DEPOSIT_V3 of `amount` of ETH (`useNative` set), from
// the sender to the sender on chain id 8453.
const acrossEth = (amount) =>
coder.encode(
[
"tuple(address,address,address,address,uint256,uint256,uint256,address,uint32,uint32,uint32,bytes,bool)",
],
[
[
USER_ADDR,
USER_ADDR,
WETH_ADDR,
WETH_ADDR,
amount,
1n,
8453n,
ZERO_ADDR,
0,
0,
0,
"0x",
true,
],
],
);
// The lines of ETH the sender pays a V4 step, of `amount`.
const ethLines = (amount) => [
["Moved from the sender", amount],
["Token moved", "ETH"],
["Moved to", "The swap step that spends it"],
];
test.each([ test.each([
{ {
kind: "a V3 exact-in of 600 USDT for WETH paid to the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles", kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 1 ETH that the user settles, reads as on next",
commands: [0x00, 0x0c, 0x10], commands: [0x0b, 0x10],
inputs: [ inputs: [WRAP_INTO_ROUTER, V4_ONE_ETH],
encodeV3SwapExactIn(ROUTER_ADDR, USDT_600, 1n, [ lines: [["Amount", "2.0000 ETH"]],
USDT_ADDR,
WETH_ADDR,
]),
unwrapIntoRouter,
v4EthForUsdc(ONE_ETH),
],
lines: [["Amount", "600.0000 USDT"]],
}, },
{ {
kind: "a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles", kind: "a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles",
commands: [0x0b, 0x0c, 0x10], commands: [0x0b, 0x0c, 0x10],
inputs: [ inputs: [WRAP_INTO_ROUTER, UNWRAP_INTO_ROUTER, V4_ONE_ETH],
encodeWrapEth(ROUTER_ADDR, ONE_ETH), lines: [
unwrapIntoRouter, ["Amount", "1.0000 ETH"],
v4EthForUsdc(ONE_ETH), ...ethLines("Up to 1.0000 ETH"),
], ],
lines: [["Amount", "1.0000 ETH"]],
}, },
{ {
kind: "a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 swap of 1.5 ETH that the user settles", kind: "the same with a TRANSFER of 1 ETH to another address after the UNWRAP_WETH",
commands: [0x0b, 0x0c, 0x05, 0x10],
inputs: [
WRAP_INTO_ROUTER,
UNWRAP_INTO_ROUTER,
payOut(ZERO_ADDR, ONE_ETH),
V4_ONE_ETH,
],
lines: [
["Amount", "1.0000 ETH"],
...ethLines("Up to 1.0000 ETH"),
],
},
{
kind: "the same with a PAY_PORTION of 5,000 bips of WETH to another address before the UNWRAP_WETH",
commands: [0x0b, 0x06, 0x0c, 0x10],
inputs: [
WRAP_INTO_ROUTER,
payOut(WETH_ADDR, 5000n),
UNWRAP_INTO_ROUTER,
V4_ONE_ETH,
],
lines: [
["Amount", "1.0000 ETH"],
...ethLines("Up to 1.0000 ETH"),
],
},
{
kind: "the same with a WRAP_ETH of 1 ETH to another address after the UNWRAP_WETH",
commands: [0x0b, 0x0c, 0x0b, 0x10],
inputs: [
WRAP_INTO_ROUTER,
UNWRAP_INTO_ROUTER,
encodeWrapEth(OTHER_ADDR, ONE_ETH),
V4_ONE_ETH,
],
lines: [
["Amount", "1.0000 ETH"],
...ethLines("Up to 1.0000 ETH"),
],
},
{
kind: "the same on UniversalRouterV2_1_2 with an ACROSS_V4_DEPOSIT_V3 of 1 ETH after the UNWRAP_WETH",
router: ROUTER_V2_1_2_ADDR,
commands: [0x0b, 0x0c, 0x40, 0x10],
inputs: [
WRAP_INTO_ROUTER,
UNWRAP_INTO_ROUTER,
acrossEth(ONE_ETH),
V4_ONE_ETH,
],
lines: [
["Amount", "1.0000 ETH"],
...ethLines("Up to 1.0000 ETH"),
],
},
{
kind: "a V3 exact-in of 600 USDT that the user pays, a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles",
commands: [0x00, 0x0b, 0x0c, 0x10],
inputs: [
V3_600,
WRAP_INTO_ROUTER,
UNWRAP_INTO_ROUTER,
V4_ONE_ETH,
],
lines: [
["Amount", "600.0000 USDT"],
...ethLines("Up to 1.0000 ETH"),
],
},
{
kind: "a V3 exact-in of 600 USDT for WETH paid to the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles",
commands: [0x00, 0x0c, 0x10],
inputs: [
encodeV3SwapExactIn(ADDRESS_THIS, USDT_600, 1n, [
USDT_ADDR,
WETH_ADDR,
]),
UNWRAP_INTO_ROUTER,
V4_ONE_ETH,
],
lines: [
["Amount", "600.0000 USDT"],
...ethLines("Up to 1.0000 ETH"),
],
},
{
kind: "a V4 swap of 600 USDT that takes ETH into the router, then a V4 swap of 5 ETH that the user settles",
commands: [0x10, 0x10],
inputs: [
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_SETTLE_ID,
V4_TAKE_ID,
]),
[
encodeV4ExactInSingle(
USDT_ADDR,
ZERO_ADDR,
USDT_600,
1n,
),
settle(USDT_ADDR, 0n, true),
take(ZERO_ADDR, ADDRESS_THIS, 0n),
],
),
v4EthForUsdc(5n * ONE_ETH),
],
lines: [
["Amount", "600.0000 USDT"],
...ethLines("Up to 5.0000 ETH"),
],
},
{
kind: "a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 exact-out paid in ETH with a SETTLE_ALL of at most 3 ETH, reads as on next",
commands: [0x0b, 0x0c, 0x10], commands: [0x0b, 0x0c, 0x10],
inputs: [ inputs: [
encodeWrapEth(ROUTER_ADDR, ONE_ETH), WRAP_INTO_ROUTER,
unwrapIntoRouter, UNWRAP_INTO_ROUTER,
v4EthForUsdc((ONE_ETH * 3n) / 2n), encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_OUT_SINGLE_ID,
V4_SETTLE_ALL_ID,
V4_TAKE_ALL_ID,
]),
[
encodeV4ExactOutSingle(
ZERO_ADDR,
USDC_ADDR,
390000000n,
3n * ONE_ETH,
),
encodeV4SettleAll(ZERO_ADDR, 3n * ONE_ETH),
encodeV4TakeAll(USDC_ADDR, 0n),
],
),
],
lines: [
...wrapLines("The swap step that spends it"),
...ethLines(UNDECODED),
], ],
lines: [["Amount", "1.5000 ETH"]],
}, },
])("$kind", ({ commands, inputs, lines }) => { {
const result = decodeSteps(commands, inputs); kind: "a WRAP_ETH of 1 ETH into the router, a V3 exact-out of at most 1 WETH paid from the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles",
commands: [0x0b, 0x01, 0x0c, 0x10],
inputs: [
WRAP_INTO_ROUTER,
encodeV3SwapExactOut(
USER_ADDR,
390000000n,
ONE_ETH,
[USDC_ADDR, WETH_ADDR],
false,
),
UNWRAP_INTO_ROUTER,
V4_ONE_ETH,
],
lines: [
["Amount", "Up to 1.0000 ETH"],
...ethLines("Up to 1.0000 ETH"),
],
},
{
kind: "a WRAP_ETH of 1 ETH into the router, a V4 exact-out paid from the router's WETH, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles",
commands: [0x0b, 0x10, 0x0c, 0x10],
inputs: [
WRAP_INTO_ROUTER,
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_OUT_SINGLE_ID,
V4_SETTLE_ID,
V4_TAKE_ALL_ID,
]),
[
encodeV4ExactOutSingle(
WETH_ADDR,
USDC_ADDR,
390000000n,
ONE_ETH,
),
settle(WETH_ADDR, 0n, false),
encodeV4TakeAll(USDC_ADDR, 0n),
],
),
UNWRAP_INTO_ROUTER,
V4_ONE_ETH,
],
lines: [
["Amount", "1.0000 ETH"],
...ethLines("Up to 1.0000 ETH"),
],
},
])("$kind", ({ router, commands, inputs, lines }) => {
const result = decodeSteps(commands, inputs, router);
expect(linesBetweenTokens(result)).toEqual(lines); expect(linesBetweenTokens(result)).toEqual(lines);
}); });