fix: a swap's WETH minimum reads as ETH only when all its WETH reaches the UNWRAP_WETH (closes #642)
Between the step that set the output side and the UNWRAP_WETH, only a step the decoder read that is a PERMIT2_PERMIT, a BALANCE_CHECK_ERC20, or a SWEEP, TRANSFER, PAY_PORTION or PAY_PORTION_FULL_PRECISION of a token other than WETH keeps the swap's WETH minimum as an ETH minimum; any other step leaves ETH with no minimum. After an UNWRAP_WETH into the router, any later step but those makes the swap read WETH with no minimum. A WETH fee paid by PAY_PORTION before the UNWRAP_WETH now shows no minimum. README Display Consistency states the same rule. Model: opus-5-5
This commit is contained in:
+71
-15
@@ -1221,8 +1221,20 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
let hasUnwrapWeth = false;
|
||||
// Set when an UNWRAP_WETH pays out, as ETH, the output side's WETH,
|
||||
// which the router holds; cleared whenever the output side is set
|
||||
// again.
|
||||
// again, or a later step may pay that ETH out as WETH (see after the
|
||||
// loop). `unwrapIntoRouterStep` is the index of that UNWRAP_WETH when
|
||||
// it paid the ETH to the router, which then holds it, otherwise null.
|
||||
let outputUnwrapped = false;
|
||||
let unwrapIntoRouterStep = null;
|
||||
// The index of the step that set the output side.
|
||||
let outputStep = -1;
|
||||
// The index of the last step that may change the router's WETH: pay
|
||||
// out WETH the router holds, add to it, or wrap the router's ETH.
|
||||
// Every step may, but one this decoder read that is a PERMIT2_PERMIT,
|
||||
// a BALANCE_CHECK_ERC20, or a SWEEP, TRANSFER, PAY_PORTION or
|
||||
// PAY_PORTION_FULL_PRECISION of a token other than WETH (see
|
||||
// keepsRouterWeth in the loop).
|
||||
let lastWethStep = -1;
|
||||
// Where the step that set the output side paid its output: the
|
||||
// recipient of a swap step, as written, or of each TAKE, TAKE_ALL and
|
||||
// TAKE_PORTION of it in a V4 step, a TAKE_ALL as paid to the sender,
|
||||
@@ -1265,13 +1277,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
if (!present(token) && !present(amount)) return;
|
||||
setInput(token, amount);
|
||||
};
|
||||
const setOutput = (token, amount, recipients) => {
|
||||
const setOutput = (step, token, amount, recipients) => {
|
||||
if (!present(token) && !present(amount)) return;
|
||||
outputToken = present(token) ? token : null;
|
||||
minOutput = present(amount) ? amount : null;
|
||||
outputEstablished = true;
|
||||
outputUnwrapped = false;
|
||||
outputRecipients = recipients;
|
||||
outputStep = step;
|
||||
};
|
||||
|
||||
// What the steps take from the sender beyond the amount of the step
|
||||
@@ -1421,6 +1434,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
? COMMAND_NAMES[command]
|
||||
: "Command 0x" + cmdId.toString(16).padStart(2, "0"),
|
||||
);
|
||||
// Set by a branch below that reads a step that cannot change the
|
||||
// router's WETH (see lastWethStep).
|
||||
let keepsRouterWeth = false;
|
||||
|
||||
try {
|
||||
if (command === "EXECUTE_SUB_PLAN") {
|
||||
@@ -1452,6 +1468,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
allowedToFail,
|
||||
beforeEveryTake: !tookBefore,
|
||||
};
|
||||
keepsRouterWeth = true;
|
||||
} else {
|
||||
stepsUnread.push(i);
|
||||
}
|
||||
@@ -1507,6 +1524,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// what the user receives: it sets no side, and the
|
||||
// step says so.
|
||||
commandNames[i] += " (of another address)";
|
||||
keepsRouterWeth = true;
|
||||
} else {
|
||||
// The router passes this check whenever the owner
|
||||
// holds at least minBalance, so a zero one guarantees
|
||||
@@ -1517,8 +1535,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// does. It pays nobody.
|
||||
const minBalance = allowedToFail ? 0n : b.minBalance;
|
||||
if (!(minBalance === 0n && present(minOutput))) {
|
||||
setOutput(b.token, minBalance, []);
|
||||
setOutput(i, b.token, minBalance, []);
|
||||
}
|
||||
keepsRouterWeth = true;
|
||||
}
|
||||
} else if (command === "V3_SWAP_EXACT_IN") {
|
||||
const s = decodeV3SwapExactIn(inputs[i]);
|
||||
@@ -1531,7 +1550,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// Always update output: in multi-step swaps (V3 → V4),
|
||||
// the last swap step determines the final output token
|
||||
// and minimum received amount.
|
||||
setOutput(s.tokenOut, s.amountOutMin, [s.recipient]);
|
||||
setOutput(i, s.tokenOut, s.amountOutMin, [s.recipient]);
|
||||
if (!s.payerIsUser) {
|
||||
spendFromRouter(s.tokenIn, s.amountIn);
|
||||
}
|
||||
@@ -1551,7 +1570,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
} else if (s.payerIsUser) {
|
||||
addUserPaid(s.tokenIn, s.amountIn);
|
||||
}
|
||||
setOutput(s.tokenOut, s.amountOutMin, [s.recipient]);
|
||||
setOutput(i, s.tokenOut, s.amountOutMin, [s.recipient]);
|
||||
if (!s.payerIsUser) {
|
||||
spendFromRouter(s.tokenIn, s.amountIn);
|
||||
}
|
||||
@@ -1582,7 +1601,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
} else if (s.payerIsUser) {
|
||||
addUserPaid(s.tokenIn, s.amountInMax);
|
||||
}
|
||||
setOutput(s.tokenOut, s.amountOut, [s.recipient]);
|
||||
setOutput(i, s.tokenOut, s.amountOut, [s.recipient]);
|
||||
if (!isHop(i, s.tokenOut, s.recipient)) {
|
||||
addPayee(s.recipient, s.tokenOut);
|
||||
}
|
||||
@@ -1604,6 +1623,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
if (!isRouter(s.recipient)) {
|
||||
heldByRouter.delete(s.token.toLowerCase());
|
||||
}
|
||||
keepsRouterWeth = !WETH_ADDRESSES.includes(
|
||||
s.token.toLowerCase(),
|
||||
);
|
||||
} else {
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
@@ -1641,6 +1663,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
) {
|
||||
heldByRouter.delete(t.token.toLowerCase());
|
||||
}
|
||||
keepsRouterWeth = !WETH_ADDRESSES.includes(
|
||||
t.token.toLowerCase(),
|
||||
);
|
||||
} else {
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
@@ -1766,13 +1791,19 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
outputEstablished = true;
|
||||
outputUnwrapped = false;
|
||||
outputRecipients = takeRecipients;
|
||||
outputStep = i;
|
||||
} else if (v4) {
|
||||
// A step with no swap action sets only the sides its
|
||||
// SETTLE, SETTLE_ALL, TAKE or TAKE_ALL names:
|
||||
// setInputOnce() and setOutput() leave alone a side
|
||||
// whose two halves are both unstated.
|
||||
setInputOnce(v4.tokenIn, amountIn);
|
||||
setOutput(v4.tokenOut, v4.amountOutMin, takeRecipients);
|
||||
setOutput(
|
||||
i,
|
||||
v4.tokenOut,
|
||||
v4.amountOutMin,
|
||||
takeRecipients,
|
||||
);
|
||||
}
|
||||
// Every V4 step, with a swap action or without, takes
|
||||
// from the sender what the user pays it (an amount an
|
||||
@@ -1799,12 +1830,16 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// among it when the step that set the output side paid it
|
||||
// to the router and the router still holds WETH. WETH
|
||||
// that step paid to the user, or to anyone else, stays
|
||||
// WETH, so when that step also paid some of its output
|
||||
// elsewhere, or made a payout this decoder did not read,
|
||||
// only part of it may be unwrapped. This decoder does not
|
||||
// count how much of that step's minimum then reaches the
|
||||
// router, nor read the UNWRAP_WETH's own minimum, so the
|
||||
// output side keeps no figure.
|
||||
// WETH. When that step also paid some of its output
|
||||
// elsewhere or made a payout this decoder did not read,
|
||||
// or a step between the two may have changed the
|
||||
// router's WETH (see lastWethStep), the WETH unwrapped
|
||||
// may be only part of the output side's, or none of it.
|
||||
// This decoder does not count how much of that step's
|
||||
// minimum then reaches the UNWRAP_WETH, nor read the
|
||||
// UNWRAP_WETH's own minimum, so the output side keeps no
|
||||
// figure.
|
||||
const u = decodeUnwrapWeth(inputs[i]);
|
||||
const toRouter = (r) => present(r) && isRouter(r);
|
||||
if (
|
||||
present(outputToken) &&
|
||||
@@ -1813,11 +1848,15 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
outputRecipients.some(toRouter)
|
||||
) {
|
||||
outputUnwrapped = true;
|
||||
if (!outputRecipients.every(toRouter)) {
|
||||
unwrapIntoRouterStep =
|
||||
u !== null && isRouter(u.recipient) ? i : null;
|
||||
if (
|
||||
!outputRecipients.every(toRouter) ||
|
||||
lastWethStep > outputStep
|
||||
) {
|
||||
minOutput = null;
|
||||
}
|
||||
}
|
||||
const u = decodeUnwrapWeth(inputs[i]);
|
||||
if (u) {
|
||||
addPayee(u.recipient);
|
||||
// It pays out all the router's WETH, as ETH. Into the
|
||||
@@ -1854,6 +1893,23 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
if (allowedToFail) {
|
||||
commandNames[i] += " (allowed to fail)";
|
||||
}
|
||||
if (!keepsRouterWeth) {
|
||||
lastWethStep = i;
|
||||
}
|
||||
}
|
||||
|
||||
// After an UNWRAP_WETH that paid the output side's WETH to the router
|
||||
// as ETH, a later step that may change the router's WETH (see
|
||||
// lastWethStep) may wrap that ETH and pay it out as WETH. The output
|
||||
// side then reads as WETH with no figure, as this decoder does not
|
||||
// count how much of that ETH each later step pays out.
|
||||
if (
|
||||
outputUnwrapped &&
|
||||
unwrapIntoRouterStep !== null &&
|
||||
lastWethStep > unwrapIntoRouterStep
|
||||
) {
|
||||
outputUnwrapped = false;
|
||||
minOutput = null;
|
||||
}
|
||||
|
||||
// The Amount line, and the lines for what the steps take beyond it.
|
||||
|
||||
Reference in New Issue
Block a user