Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69f8e10cc7 |
@@ -1250,20 +1250,23 @@ Decoding). So a step whose USDT-for-USDC swap action could not be read, with a
|
||||
A V4 step with no swap action sets only the sides its `SETTLE`, `SETTLE_ALL`,
|
||||
`TAKE` or `TAKE_ALL` names.
|
||||
|
||||
An `UNWRAP_WETH` step makes `Token Out` ETH only when the output side is WETH,
|
||||
on mainnet or Sepolia, that the router holds when the step runs (Transaction
|
||||
Decoding below gives what the router holds), or when no step set the output
|
||||
side; a WETH `Min. received` figure then reads in ETH. A later step that sets
|
||||
the output side sets it anew. The router unwraps only the WETH it holds, so a
|
||||
swap that pays its WETH to the user and then unwraps shows WETH. The decoder
|
||||
follows what the router holds by token, not by step: a swap that pays part of
|
||||
its WETH to the user and part to the router, and then unwraps, shows ETH with
|
||||
the minimum of the step that set the output side. Otherwise `Token Out` and
|
||||
`Min. received` keep the output side's own token and figure, whether the swap
|
||||
was paid in ETH or in a token: a V2 exact-out swap that buys USDC and then
|
||||
unwraps the WETH it did not spend shows USDC. The token permission warning on
|
||||
the signature screen has its own amount wording, including `Unknown`; the
|
||||
SignApproval section below describes it.
|
||||
An `UNWRAP_WETH` step makes `Token Out` ETH only when no step set the output
|
||||
side, or when the output side is WETH, on mainnet or Sepolia, that the step that
|
||||
set it paid to the router and that the router still holds when the `UNWRAP_WETH`
|
||||
runs (Transaction Decoding below gives what the router holds). The router
|
||||
unwraps only the WETH it holds, so WETH that step paid to the user, or to anyone
|
||||
else, stays WETH: a swap that pays the router WETH, then a swap that pays the
|
||||
user WETH, then an unwrap, shows WETH with the second swap's minimum. The
|
||||
minimum reads in ETH when that step paid all its WETH to the router. When it
|
||||
paid only part of it to the router, as a V4 step does that `TAKE`s some of its
|
||||
WETH to the user and the rest to the router, or paid the router WETH beside a
|
||||
payout the decoder cannot read, the swap shows ETH with no minimum, since the
|
||||
calldata states none for the ETH. A later step that sets the output side sets it
|
||||
anew. Otherwise `Token Out` and `Min. received` keep the output side's own token
|
||||
and figure, whether the swap was paid in ETH or in a token: a V2 exact-out swap
|
||||
that buys USDC and then unwraps the WETH it did not spend shows USDC. The token
|
||||
permission warning on the signature screen has its own amount wording, including
|
||||
`Unknown`; the SignApproval section below describes it.
|
||||
|
||||
The swap's `Deadline` line is the router's deadline as a UTC date and time, e.g.
|
||||
`2026-02-27 08:25:51`. A JavaScript date reaches only to 275760-09-13 00:00:00
|
||||
|
||||
+37
-11
@@ -1223,6 +1223,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// 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 and
|
||||
// TAKE_PORTION of it in a V4 step, 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;
|
||||
@@ -1259,12 +1264,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
|
||||
@@ -1510,7 +1516,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") {
|
||||
@@ -1524,7 +1530,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);
|
||||
}
|
||||
@@ -1544,7 +1550,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);
|
||||
}
|
||||
@@ -1575,7 +1581,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);
|
||||
}
|
||||
@@ -1733,6 +1739,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
|
||||
@@ -1750,13 +1764,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
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
|
||||
@@ -1778,16 +1793,27 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
}
|
||||
} else if (command === "UNWRAP_WETH") {
|
||||
hasUnwrapWeth = true;
|
||||
// The router unwraps all the WETH it holds, the output
|
||||
// side's included when it is WETH the router holds, even
|
||||
// when the recipient cannot be read. WETH a step paid to
|
||||
// anyone else is not unwrapped.
|
||||
// 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. The calldata then
|
||||
// states no minimum for the ETH, and 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())
|
||||
heldByRouter.has(outputToken.toLowerCase()) &&
|
||||
outputRecipients.some(toRouter)
|
||||
) {
|
||||
outputUnwrapped = true;
|
||||
if (!outputRecipients.every(toRouter)) {
|
||||
minOutput = null;
|
||||
}
|
||||
}
|
||||
const u = decodeUnwrapWeth(inputs[i]);
|
||||
if (u) {
|
||||
|
||||
@@ -270,18 +270,28 @@ const SWAP_USDT_FOR_ETH = [
|
||||
[[[ZERO, USDT, 500, 10, ZERO], false, USDT_2000, ONE_WETH, "0x"]],
|
||||
),
|
||||
];
|
||||
const SWAP_USDT_FOR_WETH = [
|
||||
0x06, // SWAP_EXACT_IN_SINGLE
|
||||
coder.encode(
|
||||
[
|
||||
"tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
|
||||
],
|
||||
[[[USDT, WETH, 500, 10, ZERO], true, USDT_2000, ONE_WETH, "0x"]],
|
||||
),
|
||||
];
|
||||
// SWAP_EXACT_IN_SINGLE with no parameters, which cannot be read.
|
||||
const UNREADABLE_SWAP = [0x06, "0x"];
|
||||
const SETTLE_USDT = [
|
||||
0x0b,
|
||||
coder.encode(["address", "uint256", "bool"], [USDT, 0n, true]),
|
||||
];
|
||||
function take(currency, recipient) {
|
||||
// A TAKE of `amount`, by default zero: the open delta, all the step owes.
|
||||
function take(currency, recipient, amount = 0n) {
|
||||
return [
|
||||
0x0e,
|
||||
coder.encode(
|
||||
["address", "address", "uint256"],
|
||||
[currency, recipient, 0n],
|
||||
[currency, recipient, amount],
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -594,9 +604,9 @@ describe("an UNWRAP_WETH makes the output ETH only when the router holds it", ()
|
||||
});
|
||||
|
||||
// The user receives the first step's WETH as WETH and the second's as
|
||||
// ETH. The second step sets the output side, and the router holds its
|
||||
// WETH, so the output reads as ETH with the second step's minimum.
|
||||
test("a swap paying part of its WETH to the user and the rest to the router, then an UNWRAP_WETH, reads as ETH", () => {
|
||||
// ETH. The second step sets the output side and pays its WETH to the
|
||||
// router, so the output reads as ETH with the second step's minimum.
|
||||
test("two swaps, the second paying its WETH to the router, then an UNWRAP_WETH, reads as ETH", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
swapToWeth(MSG_SENDER),
|
||||
@@ -611,6 +621,69 @@ describe("an UNWRAP_WETH makes the output ETH only when the router holds it", ()
|
||||
expect(detail(result, "Min. received").value).toBe("0.5000 ETH");
|
||||
});
|
||||
|
||||
// The same two swaps the other way round. The user receives the first
|
||||
// step's WETH as ETH and the second's as WETH. The second step sets the
|
||||
// output side and pays its WETH to the user, so the output reads as WETH
|
||||
// with the second step's minimum, though the router holds the first's.
|
||||
test("two swaps, the second paying its WETH to the user, then an UNWRAP_WETH, reads as WETH", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
v2Swap(0x08, ADDRESS_THIS, USDC_1990, ONE_WETH / 2n, [
|
||||
USDC,
|
||||
WETH,
|
||||
]),
|
||||
swapToWeth(MSG_SENDER),
|
||||
unwrapWeth(MSG_SENDER),
|
||||
),
|
||||
);
|
||||
expect(result.name).toBe("Swap USDC → WETH");
|
||||
expect(detail(result, "Min. received").value).toBe("1.0000 WETH");
|
||||
});
|
||||
|
||||
// One V4 step buys at least 1 WETH and TAKEs 0.6 WETH to the user and the
|
||||
// rest to the router. The user receives 0.6 WETH and the rest as ETH, so
|
||||
// neither minimum is shown: the calldata states none for the ETH.
|
||||
test("one V4 step paying part of its WETH to the user and the rest to the router, then an UNWRAP_WETH, reads as ETH with no minimum", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
v4Swap(
|
||||
SWAP_USDT_FOR_WETH,
|
||||
SETTLE_USDT,
|
||||
take(WETH, MSG_SENDER, (ONE_WETH * 6n) / 10n),
|
||||
take(WETH, ADDRESS_THIS),
|
||||
),
|
||||
unwrapWeth(MSG_SENDER),
|
||||
),
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → ETH");
|
||||
expect(labels(result)).toEqual([
|
||||
"Protocol",
|
||||
"Token In",
|
||||
"Amount",
|
||||
"Token Out",
|
||||
"Steps",
|
||||
"Deadline",
|
||||
]);
|
||||
});
|
||||
|
||||
// A TAKE the decoder cannot read may pay part of the WETH to the user.
|
||||
test("one V4 step paying WETH to the router beside a TAKE that cannot be read, then an UNWRAP_WETH, reads as ETH with no minimum", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
v4Swap(
|
||||
SWAP_USDT_FOR_WETH,
|
||||
SETTLE_USDT,
|
||||
withDirtyWord(take(WETH, MSG_SENDER), 1),
|
||||
take(WETH, ADDRESS_THIS),
|
||||
),
|
||||
unwrapWeth(MSG_SENDER),
|
||||
),
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → ETH");
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
expect(detail(result, "Min. paid out")).toBeUndefined();
|
||||
});
|
||||
|
||||
// A step after the UNWRAP_WETH that sets the output side sets it anew.
|
||||
test("a swap paying WETH to the user after an UNWRAP_WETH of an earlier swap's WETH reads as WETH", () => {
|
||||
const result = decode(
|
||||
|
||||
Reference in New Issue
Block a user