fix: WETH paid to the user before an UNWRAP_WETH reads as WETH (closes #607)
An UNWRAP_WETH made Token Out ETH whenever the output side was WETH, though the router unwraps only the WETH it holds. decode() now records where the step that set the output side paid its output, and an UNWRAP_WETH makes the output ETH only when that step paid WETH to the router and the router still holds WETH. When that step paid only part of its WETH to the router, or made a payout the decoder cannot read, the output reads ETH with no minimum: the decoder does not count how much of that step's minimum reaches the router, nor read the UNWRAP_WETH's own minimum. A BALANCE_CHECK_ERC20 pays nobody, so it is never read as unwrapped. README Display Consistency says the same. Model: opus-5-5
This commit was merged in pull request #640.
This commit is contained in:
+59
-15
@@ -1219,6 +1219,16 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
let minOutput = null;
|
||||
let outputEstablished = false;
|
||||
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.
|
||||
let outputUnwrapped = false;
|
||||
// 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,
|
||||
// with null for a payout of that step this decoder did not read. A
|
||||
// BALANCE_CHECK_ERC20 pays nobody.
|
||||
let outputRecipients = [];
|
||||
let hasV2OrV3ExactOut = false;
|
||||
let swapUndecoded = false;
|
||||
let hasSubPlan = false;
|
||||
@@ -1255,11 +1265,13 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
if (!present(token) && !present(amount)) return;
|
||||
setInput(token, amount);
|
||||
};
|
||||
const setOutput = (token, amount) => {
|
||||
const setOutput = (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;
|
||||
};
|
||||
|
||||
// What the steps take from the sender beyond the amount of the step
|
||||
@@ -1505,7 +1517,7 @@ 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(b.token, minBalance, []);
|
||||
}
|
||||
}
|
||||
} else if (command === "V3_SWAP_EXACT_IN") {
|
||||
@@ -1519,7 +1531,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);
|
||||
setOutput(s.tokenOut, s.amountOutMin, [s.recipient]);
|
||||
if (!s.payerIsUser) {
|
||||
spendFromRouter(s.tokenIn, s.amountIn);
|
||||
}
|
||||
@@ -1539,7 +1551,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
} else if (s.payerIsUser) {
|
||||
addUserPaid(s.tokenIn, s.amountIn);
|
||||
}
|
||||
setOutput(s.tokenOut, s.amountOutMin);
|
||||
setOutput(s.tokenOut, s.amountOutMin, [s.recipient]);
|
||||
if (!s.payerIsUser) {
|
||||
spendFromRouter(s.tokenIn, s.amountIn);
|
||||
}
|
||||
@@ -1570,7 +1582,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
} else if (s.payerIsUser) {
|
||||
addUserPaid(s.tokenIn, s.amountInMax);
|
||||
}
|
||||
setOutput(s.tokenOut, s.amountOut);
|
||||
setOutput(s.tokenOut, s.amountOut, [s.recipient]);
|
||||
if (!isHop(i, s.tokenOut, s.recipient)) {
|
||||
addPayee(s.recipient, s.tokenOut);
|
||||
}
|
||||
@@ -1728,6 +1740,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
(p) => p.currency === v4.tokenIn,
|
||||
);
|
||||
const amountIn = paidIn ? paidIn.amount : v4?.amountIn;
|
||||
// Where the step pays its output, if it sets the output
|
||||
// side (see outputRecipients).
|
||||
const takeRecipients = v4
|
||||
? v4.takes
|
||||
.filter((t) => t.currency === v4.tokenOut)
|
||||
.map((t) => t.recipient)
|
||||
.concat(v4.recipientUnread ? [null] : [])
|
||||
: [];
|
||||
if (v4 && v4.hasSwapAction) {
|
||||
// A step with a swap action always sets the output
|
||||
// side, as the last swap step wins, and sets the
|
||||
@@ -1744,13 +1764,15 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
outputToken = v4.tokenOut;
|
||||
minOutput = v4.amountOutMin;
|
||||
outputEstablished = true;
|
||||
outputUnwrapped = false;
|
||||
outputRecipients = takeRecipients;
|
||||
} 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);
|
||||
setOutput(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
|
||||
@@ -1772,6 +1794,29 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
}
|
||||
} else if (command === "UNWRAP_WETH") {
|
||||
hasUnwrapWeth = true;
|
||||
// The router unwraps all the WETH it holds, even when the
|
||||
// recipient cannot be read. The output side's WETH is
|
||||
// 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.
|
||||
const toRouter = (r) => present(r) && isRouter(r);
|
||||
if (
|
||||
present(outputToken) &&
|
||||
WETH_ADDRESSES.includes(outputToken.toLowerCase()) &&
|
||||
heldByRouter.has(outputToken.toLowerCase()) &&
|
||||
outputRecipients.some(toRouter)
|
||||
) {
|
||||
outputUnwrapped = true;
|
||||
if (!outputRecipients.every(toRouter)) {
|
||||
minOutput = null;
|
||||
}
|
||||
}
|
||||
const u = decodeUnwrapWeth(inputs[i]);
|
||||
if (u) {
|
||||
addPayee(u.recipient);
|
||||
@@ -1912,23 +1957,22 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
outputToken = null;
|
||||
minOutput = null;
|
||||
outputEstablished = true;
|
||||
outputUnwrapped = false;
|
||||
}
|
||||
|
||||
// Resolve token info. A null token on either side means no step named
|
||||
// a currency for it, a V4 step with a swap action left it unknown, or
|
||||
// an EXECUTE_SUB_PLAN step did; tokenInfo() refuses rather than calling
|
||||
// it ETH. UNWRAP_WETH turns WETH into ETH, so it makes the output ETH
|
||||
// when the output side is WETH, or when no step set the output side. A
|
||||
// V4 step with a swap action, or an EXECUTE_SUB_PLAN step, did set it,
|
||||
// even when it set it unknown. Any other output keeps its own token
|
||||
// and figure: a swap that buys USDC and then unwraps the WETH it did
|
||||
// not spend receives USDC.
|
||||
const outputIsWeth =
|
||||
present(outputToken) &&
|
||||
WETH_ADDRESSES.includes(outputToken.toLowerCase());
|
||||
// when it unwraps the output side's WETH (`outputUnwrapped`), or when
|
||||
// no step set the output side. A V4 step with a swap action, or
|
||||
// an EXECUTE_SUB_PLAN step, did set it, even when it set it unknown.
|
||||
// Any other output keeps its own token and figure: a swap that pays
|
||||
// its WETH to the user and then unwraps receives WETH, and one that
|
||||
// buys USDC and then unwraps the WETH it did not spend receives USDC.
|
||||
const inInfo = tokenInfo(inputToken, sources);
|
||||
const outInfo =
|
||||
hasUnwrapWeth && (outputIsWeth || !outputEstablished)
|
||||
outputUnwrapped || (hasUnwrapWeth && !outputEstablished)
|
||||
? { symbol: "ETH", decimals: 18, address: null }
|
||||
: tokenInfo(outputToken, sources);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user