fix: WETH paid to the user before an UNWRAP_WETH reads as WETH (closes #607)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s

An UNWRAP_WETH made Token Out ETH whenever the output side was WETH, so
a swap that paid its WETH to the user before it read as ETH, though the
user receives WETH. decode() now turns the output into ETH only when the
router holds the output side's WETH when the step runs, which it reads
from what it already follows the router to hold; a later step that sets
the output side sets it anew. So an UNWRAP_WETH that runs before the
swap no longer makes the WETH the swap leaves in the router read as ETH.
README Display Consistency says the same.

Model: opus-5-5
This commit is contained in:
2026-10-20 10:52:09 +00:00
parent 070ad82338
commit e3745a2acf
4 changed files with 98 additions and 16 deletions
+2 -2
View File
@@ -875,7 +875,7 @@ describe("uniswap decoder", () => {
const data = buildExecute(
solidityPacked(["uint8", "uint8"], [0x08, 0x0c]),
[
encodeV2SwapExactIn(USER_ADDR, 1000000n, 500000000000000n, [
encodeV2SwapExactIn(ROUTER_ADDR, 1000000n, 500000000000000n, [
USDC_ADDR,
SEPOLIA_WETH_ADDR,
]),
@@ -958,7 +958,7 @@ describe("uniswap decoder", () => {
const data = buildExecute(
solidityPacked(["uint8", "uint8"], [0x08, 0x0c]),
[
encodeV2SwapExactIn(USER_ADDR, 1000000n, 500000000000000n, [
encodeV2SwapExactIn(ROUTER_ADDR, 1000000n, 500000000000000n, [
USDT_ADDR,
WETH_ADDR,
]),
+63 -3
View File
@@ -7,9 +7,11 @@
// (https://git.eeqj.de/sneak/AutistMask/issues/592), the address whose
// balance a BALANCE_CHECK_ERC20 checks
// (https://git.eeqj.de/sneak/AutistMask/issues/600), any token a swap
// leaves in the router (https://git.eeqj.de/sneak/AutistMask/issues/606), and
// leaves in the router (https://git.eeqj.de/sneak/AutistMask/issues/606),
// what each PERMIT2_TRANSFER_FROM moves
// (https://git.eeqj.de/sneak/AutistMask/issues/611).
// (https://git.eeqj.de/sneak/AutistMask/issues/611), and when an
// UNWRAP_WETH makes the output ETH
// (https://git.eeqj.de/sneak/AutistMask/issues/607).
//
// Every swap command, SWEEP, WRAP_ETH, UNWRAP_WETH, TRANSFER, PAY_PORTION,
// PAY_PORTION_FULL_PRECISION, PERMIT2_TRANSFER_FROM, ACROSS_V4_DEPOSIT_V3 and
@@ -531,7 +533,7 @@ describe("an output left in the router is not received", () => {
{
kind: "an UNWRAP_WETH to the user, then the swap",
data: execute(unwrapWeth(MSG_SENDER), toRouter),
min: "1.0000 ETH",
min: "1.0000 WETH",
},
{
kind: "the swap, then a WRAP_ETH of zero to the user",
@@ -568,6 +570,64 @@ describe("an output left in the router is not received", () => {
});
});
// https://git.eeqj.de/sneak/AutistMask/issues/607: an UNWRAP_WETH pays out,
// as ETH, the WETH the router holds when it runs. Token Out read ETH whenever
// the output side was WETH, so a swap that paid its WETH to the user before
// the UNWRAP_WETH read as ETH, though the user receives WETH.
describe("an UNWRAP_WETH makes the output ETH only when the router holds it", () => {
test("a swap paying its WETH to the user, then an UNWRAP_WETH, reads as WETH", () => {
const result = decode(
execute(swapToWeth(MSG_SENDER), unwrapWeth(MSG_SENDER)),
);
expect(result.name).toBe("Swap USDT → WETH");
expect(labels(result)).toEqual(PAID_TO_USER);
expect(detail(result, "Min. received").value).toBe("1.0000 WETH");
});
test("a swap paying its WETH to the router, then an UNWRAP_WETH to the user, reads as ETH", () => {
const result = decode(
execute(swapToWeth(ADDRESS_THIS), unwrapWeth(MSG_SENDER)),
);
expect(result.name).toBe("Swap USDT → ETH");
expect(labels(result)).toEqual(PAID_TO_USER);
expect(detail(result, "Min. received").value).toBe("1.0000 ETH");
});
// 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", () => {
const result = decode(
execute(
swapToWeth(MSG_SENDER),
v2Swap(0x08, ADDRESS_THIS, USDC_1990, ONE_WETH / 2n, [
USDC,
WETH,
]),
unwrapWeth(MSG_SENDER),
),
);
expect(result.name).toBe("Swap USDT → ETH");
expect(detail(result, "Min. received").value).toBe("0.5000 ETH");
});
// 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(
execute(
swapToWeth(ADDRESS_THIS),
unwrapWeth(MSG_SENDER),
v2Swap(0x08, MSG_SENDER, USDC_1990, ONE_WETH / 2n, [
USDC,
WETH,
]),
),
);
expect(result.name).toBe("Swap USDT → WETH");
expect(detail(result, "Min. received").value).toBe("0.5000 WETH");
});
});
// https://git.eeqj.de/sneak/AutistMask/issues/606: any token a step pays the
// router stays there unless a later step pays out or spends all of it, not
// only the swap's output. A V4 SETTLE can take more of the user's tokens than