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)
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 from that amount. Without such an earlier step, the lines read as before. Model: opus-5-5
This commit was merged in pull request #679.
This commit is contained in:
+59
-19
@@ -1264,6 +1264,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// ERC-20 token from the router takes none of that token from the
|
||||
// sender.
|
||||
let inputPaidByUser = false;
|
||||
// The recipient of a WRAP_ETH that set the input side, as written,
|
||||
// when it is not the router; otherwise null. That WRAP_ETH pays its
|
||||
// WETH there, not to a swap step (see the Moved to lines).
|
||||
let wrapEthRecipient = null;
|
||||
// The last PERMIT2_PERMIT this decoder read before a step set the
|
||||
// input side, or null (see permitStands after the loop).
|
||||
let permit = null;
|
||||
@@ -1364,10 +1368,12 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// "Up to" and the amount.
|
||||
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.
|
||||
const addUserPaid = (token, amount) => {
|
||||
if (amount !== ALREADY_PAID) userPaid.push({ token, amount });
|
||||
// of `token` from the sender, or at most that when `atMost` is set. A
|
||||
// V2 exact-in of ALREADY_PAID takes nothing.
|
||||
const addUserPaid = (token, amount, atMost = false) => {
|
||||
if (amount !== ALREADY_PAID) {
|
||||
userPaid.push({ token, amount, atMost });
|
||||
}
|
||||
};
|
||||
// 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
|
||||
@@ -1895,6 +1901,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
w.amount,
|
||||
);
|
||||
inputPaidByUser = true;
|
||||
if (!isRouter(w.recipient)) {
|
||||
wrapEthRecipient = w.recipient;
|
||||
}
|
||||
}
|
||||
addPayee(w.recipient);
|
||||
// CONTRACT_BALANCE wraps all the router's ETH.
|
||||
@@ -2055,9 +2064,18 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// router still runs it.
|
||||
const setInputHere = inputEstablished && !inputSetBefore;
|
||||
if (setInputHere) inputPaidByUser = paidIn !== undefined;
|
||||
// ETH an earlier step left in the router may pay part of
|
||||
// the ETH that the user pays this step. The sender then
|
||||
// pays at most that amount.
|
||||
for (const p of paidByUser) {
|
||||
if (setInputHere && p === paidIn) continue;
|
||||
addUserPaid(p.currency, p.amount);
|
||||
addUserPaid(
|
||||
p.currency,
|
||||
p.amount,
|
||||
routerHeldEth &&
|
||||
p.currency ===
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
);
|
||||
}
|
||||
// A SETTLE of ETH from the router that `paidByUser` leaves
|
||||
// out is a take of its own, even on the step that sets
|
||||
@@ -2221,15 +2239,17 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
permit.beforeEveryTake &&
|
||||
!stepsUnread.some((step) => step > permit.step);
|
||||
// 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.
|
||||
// { 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).
|
||||
const spentFromSender = [];
|
||||
let total = null;
|
||||
// The takes from the sender that `total` adds up, each a quantity, as
|
||||
// { token, amount }. When there is no Amount line, each gets lines of
|
||||
// its own (see below). The amount of the step that set the input side
|
||||
// counts in `total` whoever pays that step. That amount is among them
|
||||
// only when the user pays the step (see inputPaidByUser).
|
||||
// The takes from the sender that `total` adds up, each a quantity, in
|
||||
// the shape of `spentFromSender`. When there is no Amount line, each
|
||||
// gets lines of its own (see below). The amount of the step that set
|
||||
// the input side counts in `total` whoever pays that step. That amount
|
||||
// is among them only when the user pays the step (see inputPaidByUser).
|
||||
const inTotal = [];
|
||||
// Whether the step that set the input side counts as a take from the
|
||||
// sender, which may get lines of its own. The amount it stated counts
|
||||
@@ -2243,7 +2263,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
if (!permitStands || sameAddress(inputToken, permit.token)) {
|
||||
total = inputAmount;
|
||||
if (typeof inputAmount === "bigint" && inputPaidByUser) {
|
||||
inTotal.push({ token: inputToken, amount: inputAmount });
|
||||
inTotal.push({
|
||||
token: inputToken,
|
||||
amount: inputAmount,
|
||||
recipient: wrapEthRecipient,
|
||||
});
|
||||
}
|
||||
if (hasFirstTake && !present(inputAmount)) {
|
||||
firstTakeUnstated = { token: inputToken, amount: null };
|
||||
@@ -2251,7 +2275,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
} else if (hasFirstTake) {
|
||||
// The step that set the input side spent a token other than the
|
||||
// permit's, or one not known, so its amount gets lines of its own.
|
||||
spentFromSender.push({ token: inputToken, amount: inputAmount });
|
||||
spentFromSender.push({
|
||||
token: inputToken,
|
||||
amount: inputAmount,
|
||||
recipient: wrapEthRecipient,
|
||||
});
|
||||
}
|
||||
if (permitStands) inputToken = permit.token;
|
||||
for (const t of userPaid) {
|
||||
@@ -2434,7 +2462,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// What steps take from the sender beyond the Amount line, in the shape
|
||||
// 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
|
||||
// that one line alone.
|
||||
// that one line alone. A take with no recipient goes to the swap step
|
||||
// that spends it. A take with a recipient goes to that recipient;
|
||||
// `MSG_SENDER` means the sender.
|
||||
for (const s of spentFromSender) {
|
||||
details.push({
|
||||
label: "Moved from the sender",
|
||||
@@ -2447,10 +2477,20 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
details.push(
|
||||
tokenMovedLine("Token moved", tokenInfo(s.token, sources)),
|
||||
);
|
||||
details.push({
|
||||
label: "Moved to",
|
||||
value: "The swap step that spends it",
|
||||
});
|
||||
if (!present(s.recipient)) {
|
||||
details.push({
|
||||
label: "Moved to",
|
||||
value: "The swap step that spends it",
|
||||
});
|
||||
} else if (s.recipient.toLowerCase() === MSG_SENDER) {
|
||||
details.push({ label: "Moved to", value: "The sender" });
|
||||
} else {
|
||||
details.push({
|
||||
label: "Moved to",
|
||||
value: s.recipient,
|
||||
address: s.recipient,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user