fix: the swap decoder shows the ETH of a WRAP_ETH that does not set the input side (closes #721)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

A later WRAP_ETH of a stated amount now counts as a take from the sender.
When it pays anyone but the router, it gets its own Moved from the sender
lines, with the recipient on Moved to. When it pays the router, it joins
the Amount line if Token In is ETH, and otherwise gets those lines too.
When the router may already hold ETH, the amount reads Up to.

A WRAP_ETH whose input this decoder cannot read gets a line saying the
amount could not be decoded.

A WRAP_ETH of CONTRACT_BALANCE states no amount and reads as before, so
Uniswap's own routes do not change.

Model: opus-5-5
This commit was merged in pull request #725.
This commit is contained in:
2026-11-03 06:51:14 +01:00
parent 267b69382e
commit 42a5d94b49
4 changed files with 226 additions and 17 deletions
+30 -7
View File
@@ -1368,7 +1368,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// After the loop each joins the Amount line or gets lines of its own
// (see permitStands). A take with `atMost` set may take less than
// `amount`: it never joins the Amount line, and its line reads
// "Up to" and the amount.
// "Up to" and the amount. A take with a `recipient`, the ETH of a
// later WRAP_ETH that pays anyone but the router, never joins the
// Amount line either: its Moved to line names that recipient.
const userPaid = [];
// A swap step the user pays for (`payerIsUser` true) takes `amount`
// of `token` from the sender, or at most that when `atMost` is set. A
@@ -1380,8 +1382,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
};
// 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
// cannot read; a V2 or V3 swap step or PERMIT2_TRANSFER_FROM whose
// input it cannot read; a PERMIT2_TRANSFER_FROM_BATCH; an
// cannot read; a V2 or V3 swap step, PERMIT2_TRANSFER_FROM or WRAP_ETH
// whose input it cannot read; a PERMIT2_TRANSFER_FROM_BATCH; an
// EXECUTE_SUB_PLAN; and a step it fails on. It is recorded with a null
// token and amount: one line says so, there is no Amount line, whose
// figure may then be too low, and the amount of a permit after it
@@ -1907,6 +1909,25 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
if (!isRouter(w.recipient)) {
wrapEthRecipient = w.recipient;
}
} else if (
w.amount > 0n &&
w.amount !== CONTRACT_BALANCE
) {
// The ETH of a stated amount comes from the
// transaction's value. ETH an earlier step left in
// the router may pay part of it, and the sender
// then pays at most that amount. CONTRACT_BALANCE
// states no amount to take.
userPaid.push({
token: "0x0000000000000000000000000000000000000000",
amount: w.amount,
atMost: heldByRouter.has(
"0x0000000000000000000000000000000000000000",
),
recipient: isRouter(w.recipient)
? null
: w.recipient,
});
}
addPayee(w.recipient);
// CONTRACT_BALANCE wraps all the router's ETH.
@@ -1931,6 +1952,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} else {
payeeUnread = true;
stepsUnread.push(i);
spentUnread();
}
} else if (command === "V4_SWAP") {
const v4 = decodeV4Swap(inputs[i]);
@@ -2203,8 +2225,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// added to it while both are quantities. A take that cannot be added,
// being of another token, coming after an amount that is not a
// quantity (a V4 open delta, ALREADY_PAID, or none stated), or having
// `atMost` set, gets lines of its own. One whose amount is not known
// also leaves the total unknown, with no figure.
// `atMost` or a `recipient` set, gets lines of its own. One whose
// amount is not known also leaves the total unknown, with no figure.
//
// 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
@@ -2245,7 +2267,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// { token, amount, atMost, recipient }. A null token is a step this
// decoder did not read (see spentUnread()), or a token it does not
// know. `atMost` is as in `userPaid`. Only the take of a WRAP_ETH that
// pays elsewhere has a recipient (see wrapEthRecipient).
// pays elsewhere has a recipient (see wrapEthRecipient and userPaid).
const spentFromSender = [];
let total = null;
// The takes from the sender that `total` adds up, each a quantity, in
@@ -2292,7 +2314,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
ofTokenIn &&
typeof total === "bigint" &&
typeof t.amount === "bigint" &&
!t.atMost
!t.atMost &&
!present(t.recipient)
) {
total += t.amount;
inTotal.push(t);