Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3745a2acf |
@@ -1250,23 +1250,20 @@ 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`,
|
A V4 step with no swap action sets only the sides its `SETTLE`, `SETTLE_ALL`,
|
||||||
`TAKE` or `TAKE_ALL` names.
|
`TAKE` or `TAKE_ALL` names.
|
||||||
|
|
||||||
An `UNWRAP_WETH` step makes `Token Out` ETH only when no step set the output
|
An `UNWRAP_WETH` step makes `Token Out` ETH only when the output side is WETH,
|
||||||
side, or when the output side is WETH, on mainnet or Sepolia, that the step that
|
on mainnet or Sepolia, that the router holds when the step runs (Transaction
|
||||||
set it paid to the router and that the router still holds when the `UNWRAP_WETH`
|
Decoding below gives what the router holds), or when no step set the output
|
||||||
runs (Transaction Decoding below gives what the router holds). The router
|
side; a WETH `Min. received` figure then reads in ETH. A later step that sets
|
||||||
unwraps only the WETH it holds, so WETH that step paid to the user, or to anyone
|
the output side sets it anew. The router unwraps only the WETH it holds, so a
|
||||||
else, stays WETH: a swap that pays the router WETH, then a swap that pays the
|
swap that pays its WETH to the user and then unwraps shows WETH. The decoder
|
||||||
user WETH, then an unwrap, shows WETH with the second swap's minimum. The
|
follows what the router holds by token, not by step: a swap that pays part of
|
||||||
minimum reads in ETH when that step paid all its WETH to the router. When it
|
its WETH to the user and part to the router, and then unwraps, shows ETH with
|
||||||
paid only part of it to the router, as a V4 step does that `TAKE`s some of its
|
the minimum of the step that set the output side. Otherwise `Token Out` and
|
||||||
WETH to the user and the rest to the router, or paid the router WETH beside a
|
`Min. received` keep the output side's own token and figure, whether the swap
|
||||||
payout the decoder cannot read, the swap shows ETH with no minimum, since the
|
was paid in ETH or in a token: a V2 exact-out swap that buys USDC and then
|
||||||
calldata states none for the ETH. A later step that sets the output side sets it
|
unwraps the WETH it did not spend shows USDC. The token permission warning on
|
||||||
anew. Otherwise `Token Out` and `Min. received` keep the output side's own token
|
the signature screen has its own amount wording, including `Unknown`; the
|
||||||
and figure, whether the swap was paid in ETH or in a token: a V2 exact-out swap
|
SignApproval section below describes it.
|
||||||
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.
|
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
|
`2026-02-27 08:25:51`. A JavaScript date reaches only to 275760-09-13 00:00:00
|
||||||
|
|||||||
+11
-37
@@ -1223,11 +1223,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// which the router holds; cleared whenever the output side is set
|
// which the router holds; cleared whenever the output side is set
|
||||||
// again.
|
// again.
|
||||||
let outputUnwrapped = false;
|
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 hasV2OrV3ExactOut = false;
|
||||||
let swapUndecoded = false;
|
let swapUndecoded = false;
|
||||||
let hasSubPlan = false;
|
let hasSubPlan = false;
|
||||||
@@ -1264,13 +1259,12 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
if (!present(token) && !present(amount)) return;
|
if (!present(token) && !present(amount)) return;
|
||||||
setInput(token, amount);
|
setInput(token, amount);
|
||||||
};
|
};
|
||||||
const setOutput = (token, amount, recipients) => {
|
const setOutput = (token, amount) => {
|
||||||
if (!present(token) && !present(amount)) return;
|
if (!present(token) && !present(amount)) return;
|
||||||
outputToken = present(token) ? token : null;
|
outputToken = present(token) ? token : null;
|
||||||
minOutput = present(amount) ? amount : null;
|
minOutput = present(amount) ? amount : null;
|
||||||
outputEstablished = true;
|
outputEstablished = true;
|
||||||
outputUnwrapped = false;
|
outputUnwrapped = false;
|
||||||
outputRecipients = recipients;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// What the steps take from the sender beyond the amount of the step
|
// What the steps take from the sender beyond the amount of the step
|
||||||
@@ -1516,7 +1510,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// does. It pays nobody.
|
// does. It pays nobody.
|
||||||
const minBalance = allowedToFail ? 0n : b.minBalance;
|
const minBalance = allowedToFail ? 0n : b.minBalance;
|
||||||
if (!(minBalance === 0n && present(minOutput))) {
|
if (!(minBalance === 0n && present(minOutput))) {
|
||||||
setOutput(b.token, minBalance, []);
|
setOutput(b.token, minBalance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (command === "V3_SWAP_EXACT_IN") {
|
} else if (command === "V3_SWAP_EXACT_IN") {
|
||||||
@@ -1530,7 +1524,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// Always update output: in multi-step swaps (V3 → V4),
|
// Always update output: in multi-step swaps (V3 → V4),
|
||||||
// the last swap step determines the final output token
|
// the last swap step determines the final output token
|
||||||
// and minimum received amount.
|
// and minimum received amount.
|
||||||
setOutput(s.tokenOut, s.amountOutMin, [s.recipient]);
|
setOutput(s.tokenOut, s.amountOutMin);
|
||||||
if (!s.payerIsUser) {
|
if (!s.payerIsUser) {
|
||||||
spendFromRouter(s.tokenIn, s.amountIn);
|
spendFromRouter(s.tokenIn, s.amountIn);
|
||||||
}
|
}
|
||||||
@@ -1550,7 +1544,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
} else if (s.payerIsUser) {
|
} else if (s.payerIsUser) {
|
||||||
addUserPaid(s.tokenIn, s.amountIn);
|
addUserPaid(s.tokenIn, s.amountIn);
|
||||||
}
|
}
|
||||||
setOutput(s.tokenOut, s.amountOutMin, [s.recipient]);
|
setOutput(s.tokenOut, s.amountOutMin);
|
||||||
if (!s.payerIsUser) {
|
if (!s.payerIsUser) {
|
||||||
spendFromRouter(s.tokenIn, s.amountIn);
|
spendFromRouter(s.tokenIn, s.amountIn);
|
||||||
}
|
}
|
||||||
@@ -1581,7 +1575,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
} else if (s.payerIsUser) {
|
} else if (s.payerIsUser) {
|
||||||
addUserPaid(s.tokenIn, s.amountInMax);
|
addUserPaid(s.tokenIn, s.amountInMax);
|
||||||
}
|
}
|
||||||
setOutput(s.tokenOut, s.amountOut, [s.recipient]);
|
setOutput(s.tokenOut, s.amountOut);
|
||||||
if (!isHop(i, s.tokenOut, s.recipient)) {
|
if (!isHop(i, s.tokenOut, s.recipient)) {
|
||||||
addPayee(s.recipient, s.tokenOut);
|
addPayee(s.recipient, s.tokenOut);
|
||||||
}
|
}
|
||||||
@@ -1739,14 +1733,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
(p) => p.currency === v4.tokenIn,
|
(p) => p.currency === v4.tokenIn,
|
||||||
);
|
);
|
||||||
const amountIn = paidIn ? paidIn.amount : v4?.amountIn;
|
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) {
|
if (v4 && v4.hasSwapAction) {
|
||||||
// A step with a swap action always sets the output
|
// A step with a swap action always sets the output
|
||||||
// side, as the last swap step wins, and sets the
|
// side, as the last swap step wins, and sets the
|
||||||
@@ -1764,14 +1750,13 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
minOutput = v4.amountOutMin;
|
minOutput = v4.amountOutMin;
|
||||||
outputEstablished = true;
|
outputEstablished = true;
|
||||||
outputUnwrapped = false;
|
outputUnwrapped = false;
|
||||||
outputRecipients = takeRecipients;
|
|
||||||
} else if (v4) {
|
} else if (v4) {
|
||||||
// A step with no swap action sets only the sides its
|
// A step with no swap action sets only the sides its
|
||||||
// SETTLE, SETTLE_ALL, TAKE or TAKE_ALL names:
|
// SETTLE, SETTLE_ALL, TAKE or TAKE_ALL names:
|
||||||
// setInputOnce() and setOutput() leave alone a side
|
// setInputOnce() and setOutput() leave alone a side
|
||||||
// whose two halves are both unstated.
|
// whose two halves are both unstated.
|
||||||
setInputOnce(v4.tokenIn, amountIn);
|
setInputOnce(v4.tokenIn, amountIn);
|
||||||
setOutput(v4.tokenOut, v4.amountOutMin, takeRecipients);
|
setOutput(v4.tokenOut, v4.amountOutMin);
|
||||||
}
|
}
|
||||||
// Every V4 step, with a swap action or without, takes
|
// Every V4 step, with a swap action or without, takes
|
||||||
// from the sender what the user pays it (an amount an
|
// from the sender what the user pays it (an amount an
|
||||||
@@ -1793,27 +1778,16 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
}
|
}
|
||||||
} else if (command === "UNWRAP_WETH") {
|
} else if (command === "UNWRAP_WETH") {
|
||||||
hasUnwrapWeth = true;
|
hasUnwrapWeth = true;
|
||||||
// The router unwraps all the WETH it holds, even when the
|
// The router unwraps all the WETH it holds, the output
|
||||||
// recipient cannot be read. The output side's WETH is
|
// side's included when it is WETH the router holds, even
|
||||||
// among it when the step that set the output side paid it
|
// when the recipient cannot be read. WETH a step paid to
|
||||||
// to the router and the router still holds WETH. WETH
|
// anyone else is not unwrapped.
|
||||||
// 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 (
|
if (
|
||||||
present(outputToken) &&
|
present(outputToken) &&
|
||||||
WETH_ADDRESSES.includes(outputToken.toLowerCase()) &&
|
WETH_ADDRESSES.includes(outputToken.toLowerCase()) &&
|
||||||
heldByRouter.has(outputToken.toLowerCase()) &&
|
heldByRouter.has(outputToken.toLowerCase())
|
||||||
outputRecipients.some(toRouter)
|
|
||||||
) {
|
) {
|
||||||
outputUnwrapped = true;
|
outputUnwrapped = true;
|
||||||
if (!outputRecipients.every(toRouter)) {
|
|
||||||
minOutput = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const u = decodeUnwrapWeth(inputs[i]);
|
const u = decodeUnwrapWeth(inputs[i]);
|
||||||
if (u) {
|
if (u) {
|
||||||
|
|||||||
@@ -270,28 +270,18 @@ const SWAP_USDT_FOR_ETH = [
|
|||||||
[[[ZERO, USDT, 500, 10, ZERO], false, USDT_2000, ONE_WETH, "0x"]],
|
[[[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.
|
// SWAP_EXACT_IN_SINGLE with no parameters, which cannot be read.
|
||||||
const UNREADABLE_SWAP = [0x06, "0x"];
|
const UNREADABLE_SWAP = [0x06, "0x"];
|
||||||
const SETTLE_USDT = [
|
const SETTLE_USDT = [
|
||||||
0x0b,
|
0x0b,
|
||||||
coder.encode(["address", "uint256", "bool"], [USDT, 0n, true]),
|
coder.encode(["address", "uint256", "bool"], [USDT, 0n, true]),
|
||||||
];
|
];
|
||||||
// A TAKE of `amount`, by default zero: the open delta, all the step owes.
|
function take(currency, recipient) {
|
||||||
function take(currency, recipient, amount = 0n) {
|
|
||||||
return [
|
return [
|
||||||
0x0e,
|
0x0e,
|
||||||
coder.encode(
|
coder.encode(
|
||||||
["address", "address", "uint256"],
|
["address", "address", "uint256"],
|
||||||
[currency, recipient, amount],
|
[currency, recipient, 0n],
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -604,9 +594,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
|
// The user receives the first step's WETH as WETH and the second's as
|
||||||
// ETH. The second step sets the output side and pays its WETH to the
|
// ETH. The second step sets the output side, and the router holds its
|
||||||
// router, so the output reads as ETH with the second step's minimum.
|
// WETH, 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", () => {
|
test("a swap paying part of its WETH to the user and the rest to the router, then an UNWRAP_WETH, reads as ETH", () => {
|
||||||
const result = decode(
|
const result = decode(
|
||||||
execute(
|
execute(
|
||||||
swapToWeth(MSG_SENDER),
|
swapToWeth(MSG_SENDER),
|
||||||
@@ -621,69 +611,6 @@ describe("an UNWRAP_WETH makes the output ETH only when the router holds it", ()
|
|||||||
expect(detail(result, "Min. received").value).toBe("0.5000 ETH");
|
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.
|
// 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", () => {
|
test("a swap paying WETH to the user after an UNWRAP_WETH of an earlier swap's WETH reads as WETH", () => {
|
||||||
const result = decode(
|
const result = decode(
|
||||||
|
|||||||
Reference in New Issue
Block a user