Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d5c5e0e6b |
@@ -1261,24 +1261,26 @@ 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 only when that step paid all its WETH to the router and
|
||||
every step between it and the `UNWRAP_WETH` is one the decoder read that cannot
|
||||
pay out WETH the router holds, add to it, or wrap the router's ETH: a
|
||||
every step between it and the `UNWRAP_WETH` is one the decoder read of these: a
|
||||
`PERMIT2_PERMIT`, a `BALANCE_CHECK_ERC20`, or a `SWEEP`, `TRANSFER`,
|
||||
`PAY_PORTION` or `PAY_PORTION_FULL_PRECISION` of a token other than WETH.
|
||||
Otherwise the swap shows ETH with no minimum, as it does after a V4 step that
|
||||
`TAKE`s some of its WETH to the user and the rest to the router, or that pays
|
||||
the router WETH beside a payout the decoder cannot read, and with a WETH
|
||||
interface fee paid by `PAY_PORTION` before the `UNWRAP_WETH`: the decoder does
|
||||
not count how much of that step's minimum reaches the `UNWRAP_WETH`, nor read
|
||||
the `UNWRAP_WETH`'s own minimum. When that `UNWRAP_WETH` pays the router, which
|
||||
then holds the ETH, any later step but those listed may wrap the ETH and pay it
|
||||
out as WETH: after one, the swap shows WETH with no minimum. 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.
|
||||
`PAY_PORTION` or `PAY_PORTION_FULL_PRECISION` of a token other than WETH, but
|
||||
not of ETH to a WETH contract, which pays the router WETH. None of these can
|
||||
take WETH out of the router or pay anyone WETH; the decoder treats every other
|
||||
step as one that may. Otherwise the swap shows ETH with no minimum, as it does
|
||||
after a V4 step that `TAKE`s some of its WETH to the user and the rest to the
|
||||
router, or that pays the router WETH beside a payout the decoder cannot read,
|
||||
and with a WETH interface fee paid by `PAY_PORTION` before the `UNWRAP_WETH`:
|
||||
the decoder does not count how much of that step's minimum reaches the
|
||||
`UNWRAP_WETH`, nor read the `UNWRAP_WETH`'s own minimum. When that `UNWRAP_WETH`
|
||||
pays the router, or its recipient cannot be read, the router may hold the ETH,
|
||||
and a later step may pay it out as WETH, as a `WRAP_ETH` of it to the user does:
|
||||
after any later step but those listed, the swap shows WETH with no minimum. 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
-22
@@ -1223,17 +1223,18 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// which the router holds; cleared whenever the output side is set
|
||||
// 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.
|
||||
// it may have paid the ETH to the router, which then holds it: its
|
||||
// recipient is the router or cannot be read. Otherwise it is 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).
|
||||
// The index of the last step this decoder treats as one that may take
|
||||
// WETH out of the router or pay anyone WETH: any step but one it 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, and not of ETH to a WETH contract, which pays the
|
||||
// router WETH (see keepsRouterWeth in the loop). None of those can.
|
||||
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
|
||||
@@ -1434,8 +1435,8 @@ 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).
|
||||
// Set by a branch below that reads a step that cannot take WETH
|
||||
// out of the router or pay anyone WETH (see lastWethStep).
|
||||
let keepsRouterWeth = false;
|
||||
|
||||
try {
|
||||
@@ -1623,8 +1624,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
if (!isRouter(s.recipient)) {
|
||||
heldByRouter.delete(s.token.toLowerCase());
|
||||
}
|
||||
keepsRouterWeth = !WETH_ADDRESSES.includes(
|
||||
s.token.toLowerCase(),
|
||||
// ETH paid to a WETH contract pays the router WETH.
|
||||
keepsRouterWeth = !(
|
||||
WETH_ADDRESSES.includes(s.token.toLowerCase()) ||
|
||||
(s.token ===
|
||||
"0x0000000000000000000000000000000000000000" &&
|
||||
WETH_ADDRESSES.includes(
|
||||
s.recipient.toLowerCase(),
|
||||
))
|
||||
);
|
||||
} else {
|
||||
payeeUnread = true;
|
||||
@@ -1663,8 +1670,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
) {
|
||||
heldByRouter.delete(t.token.toLowerCase());
|
||||
}
|
||||
keepsRouterWeth = !WETH_ADDRESSES.includes(
|
||||
t.token.toLowerCase(),
|
||||
// ETH paid to a WETH contract pays the router WETH.
|
||||
keepsRouterWeth = !(
|
||||
WETH_ADDRESSES.includes(t.token.toLowerCase()) ||
|
||||
(t.token ===
|
||||
"0x0000000000000000000000000000000000000000" &&
|
||||
WETH_ADDRESSES.includes(
|
||||
t.recipient.toLowerCase(),
|
||||
))
|
||||
);
|
||||
} else {
|
||||
payeeUnread = true;
|
||||
@@ -1832,9 +1845,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// that step paid to the user, or to anyone else, stays
|
||||
// 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.
|
||||
// or this decoder treats a step between the two as one
|
||||
// that may take WETH out of the router or pay anyone 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
|
||||
@@ -1849,7 +1863,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
) {
|
||||
outputUnwrapped = true;
|
||||
unwrapIntoRouterStep =
|
||||
u !== null && isRouter(u.recipient) ? i : null;
|
||||
u === null || isRouter(u.recipient) ? i : null;
|
||||
if (
|
||||
!outputRecipients.every(toRouter) ||
|
||||
lastWethStep > outputStep
|
||||
@@ -1898,11 +1912,12 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// After an UNWRAP_WETH that may have paid the output side's WETH to
|
||||
// the router as ETH, a later step may pay that ETH out as WETH, as a
|
||||
// WRAP_ETH of it to the user does. This decoder treats every later
|
||||
// step but those lastWethStep names as one that may, and does not
|
||||
// count how much of that ETH each pays out: after one, the output
|
||||
// side reads as WETH with no figure.
|
||||
if (
|
||||
outputUnwrapped &&
|
||||
unwrapIntoRouterStep !== null &&
|
||||
|
||||
@@ -747,11 +747,13 @@ describe("an UNWRAP_WETH makes the output ETH only when the router holds it", ()
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/642: a step between the swap and
|
||||
// the UNWRAP_WETH may pay out the router's WETH or add to it, and a step after
|
||||
// an UNWRAP_WETH into the router may wrap its ETH and pay it out as WETH. Each
|
||||
// read as ETH with the swap's minimum. A step the decoder read that is a
|
||||
// the UNWRAP_WETH may take WETH out of the router or pay it WETH, and a step
|
||||
// after an UNWRAP_WETH into the router may pay its ETH out as WETH, yet each
|
||||
// route read as ETH with the swap's minimum. 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 does neither.
|
||||
// PAY_PORTION_FULL_PRECISION of a token other than WETH, and not of ETH to a
|
||||
// WETH contract, cannot take WETH out of the router or pay anyone WETH; the
|
||||
// decoder treats every other step as one that may.
|
||||
describe("what reaches the UNWRAP_WETH of the swap's WETH", () => {
|
||||
const NO_MINIMUM_TO_USER = PAID_TO_USER.filter(
|
||||
(l) => l !== "Min. received",
|
||||
@@ -825,9 +827,13 @@ describe("what reaches the UNWRAP_WETH of the swap's WETH", () => {
|
||||
steps: [permit2Permit(USDT, USDT_2000), unwrapWeth(MSG_SENDER)],
|
||||
},
|
||||
{
|
||||
kind: "a BALANCE_CHECK_ERC20 between them",
|
||||
kind: "a BALANCE_CHECK_ERC20 of another address between them",
|
||||
steps: [balanceCheck(USDC, 1n, OTHER), unwrapWeth(MSG_SENDER)],
|
||||
},
|
||||
{
|
||||
kind: "a BALANCE_CHECK_ERC20 of the sender with a zero minimum between them",
|
||||
steps: [balanceCheck(USDC, 0n), unwrapWeth(MSG_SENDER)],
|
||||
},
|
||||
{
|
||||
kind: "a SWEEP of USDC between them",
|
||||
steps: [sweep(USDC, MSG_SENDER), unwrapWeth(MSG_SENDER)],
|
||||
@@ -869,6 +875,57 @@ describe("what reaches the UNWRAP_WETH of the swap's WETH", () => {
|
||||
expect(labels(result)).toEqual(NO_MINIMUM_TO_USER);
|
||||
});
|
||||
|
||||
// The router reads the recipient from the last 20 bytes of its word,
|
||||
// here address(2), the router itself.
|
||||
test("an UNWRAP_WETH whose recipient cannot be read, then a WRAP_ETH of all the router's ETH to the user, reads as WETH with no minimum", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
swapToWeth(ADDRESS_THIS),
|
||||
withDirtyWord(unwrapWeth(ADDRESS_THIS), 0),
|
||||
wrapEth(MSG_SENDER, CONTRACT_BALANCE),
|
||||
),
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → WETH");
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
expect(detail(result, "Min. paid out")).toBeUndefined();
|
||||
});
|
||||
|
||||
// A SWEEP of WETH pays out WETH, and ETH paid to a WETH contract pays the
|
||||
// router WETH for it. The decoder treats each as a step that may pay out
|
||||
// as WETH the swap's output, which the router holds as ETH.
|
||||
test.each([
|
||||
{
|
||||
kind: "a SWEEP of WETH to the user",
|
||||
step: sweep(WETH, MSG_SENDER),
|
||||
},
|
||||
{
|
||||
kind: "a SWEEP of its ETH to the WETH contract",
|
||||
step: sweep(ZERO, WETH),
|
||||
},
|
||||
{
|
||||
kind: "a TRANSFER of 0.5 ETH to the WETH contract",
|
||||
step: transfer(0x05, ZERO, WETH, ONE_WETH / 2n),
|
||||
},
|
||||
{
|
||||
kind: "a PAY_PORTION of all its ETH to the WETH contract",
|
||||
step: transfer(0x06, ZERO, WETH, 10000n),
|
||||
},
|
||||
])(
|
||||
"an UNWRAP_WETH into the router, then $kind, reads as WETH with no minimum",
|
||||
({ step }) => {
|
||||
const result = decode(
|
||||
execute(
|
||||
swapToWeth(ADDRESS_THIS),
|
||||
unwrapWeth(ADDRESS_THIS),
|
||||
step,
|
||||
),
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → WETH");
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
expect(detail(result, "Min. paid out")).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
// The user receives 0.5 WETH and the rest of the swap's output as ETH.
|
||||
test("an UNWRAP_WETH into the router, then a WRAP_ETH of 0.5 ETH to the user, reads as WETH with no minimum", () => {
|
||||
const result = decode(
|
||||
|
||||
Reference in New Issue
Block a user