1 Commits
Author SHA1 Message Date
sneak 5dc24d2d57 fix: a WRAP_ETH says where its ETH goes when no swap step spends it (closes #747)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
The `Moved to` line of a WRAP_ETH into the router now reads `The router`,
unless a later swap step pays from that WETH.

A later WRAP_ETH of CONTRACT_BALANCE to anyone but the router now gets a
`Moved from the sender` line reading `All the ETH the transaction carries`,
and `Moved to` names the recipient. It gets no lines after an earlier step
paid ETH to the router, because Uniswap's routes pay back unspent ETH this way.

README states the new rule.

Model: opus-5-5
2026-11-03 13:18:49 +00:00
5 changed files with 96 additions and 162 deletions
+12 -16
View File
@@ -471,9 +471,7 @@ When "Back" returns to a screen that this page load has not drawn yet,
Home is shown instead. "Back" onto a screen already drawn only unhides it, so Home is shown instead. "Back" onto a screen already drawn only unhides it, so
unsaved input in Settings survives. Home is drawn again on every "Back". When a unsaved input in Settings survives. Home is drawn again on every "Back". When a
balance refresh returns while AddressDetail or AddressToken is showing, that balance refresh returns while AddressDetail or AddressToken is showing, that
screen draws its balances again. When a refresh returns while Send is showing, screen draws its balances again.
Send draws its From address and current balance again. Its inputs and dropdown
stay as the user left them.
A network switch, or a change to "Use Blockscout", cuts the stack at the first A network switch, or a change to "Use Blockscout", cuts the stack at the first
screen that cannot be drawn from saved state. The cut also drops AddressToken screen that cannot be drawn from saved state. The cut also drops AddressToken
@@ -1414,19 +1412,17 @@ when both tokens are known. The lines are:
the sender that the `Amount` line does not include. The same three lines show the sender that the `Amount` line does not include. The same three lines show
what a `PERMIT2_TRANSFER_FROM` moves and where. A `WRAP_ETH` can pay its WETH what a `PERMIT2_TRANSFER_FROM` moves and where. A `WRAP_ETH` can pay its WETH
to a recipient other than the router. When its amount gets these lines, its to a recipient other than the router. When its amount gets these lines, its
`Moved to` line names that recipient, or reads `The sender`. For a `WRAP_ETH` `Moved to` line names that recipient, or reads `The sender`. A `WRAP_ETH`
into the router, `Moved to` reads `The router`. A `WRAP_ETH` after the step after the step that sets `Token In` takes the amount it states from the
that sets `Token In` takes the amount it states from the sender. When it pays sender. When it pays anyone but the router, that amount gets these lines. When
anyone but the router, that amount gets these lines. When it pays the router, it pays the router, that amount joins the `Amount` line if `Token In` is ETH,
that amount joins the `Amount` line if `Token In` is ETH, and gets these lines and gets these lines if not. A later `WRAP_ETH` of `CONTRACT_BALANCE` to
if not. A later `WRAP_ETH` of `CONTRACT_BALANCE` to anyone but the router gets anyone but the router gets these lines, unless an earlier step paid ETH to the
these lines. It gets none after an `UNWRAP_WETH` into the router, or while the router. Its `Moved from the sender` line reads
router holds ETH from a V4 step. When it gets them, its `All the ETH the transaction carries`. ETH that the sender pays a V4 step or a
`Moved from the sender` line reads `All the ETH the transaction carries`. ETH `WRAP_ETH` of a stated amount can get these three lines when the router may
that the sender pays a V4 step or a `WRAP_ETH` of a stated amount can get hold ETH from an earlier step. When the decoder knows the amount, its
these three lines when the router may hold ETH from an earlier step. When the `Moved from the sender` line reads `Up to` and the amount.
decoder knows the amount, its `Moved from the sender` line reads `Up to` and
the amount.
- `Permit2 allowance`, `Allowance token` and `Allowance spender`: each allowance - `Permit2 allowance`, `Allowance token` and `Allowance spender`: each allowance
that a `PERMIT2_PERMIT` or `PERMIT2_PERMIT_BATCH` grants, unless the permit's that a `PERMIT2_PERMIT` or `PERMIT2_PERMIT_BATCH` grants, unless the permit's
amount already stands as the `Amount` figure. amount already stands as the `Amount` figure.
+2 -5
View File
@@ -107,10 +107,8 @@ let refreshTimer = null;
// tracked token list stays as it is. // tracked token list stays as it is.
// //
// Once the save is done, the wallet list is drawn again. So are the balances // Once the save is done, the wallet list is drawn again. So are the balances
// on AddressDetail or AddressToken when that screen is up. When Send is up, its // on AddressDetail or AddressToken when that screen is up. No other screen is
// From address and current balance are drawn again, and its inputs and dropdown // drawn again, DeleteAddress and its balance lines included.
// stay as the user left them. No other screen is drawn again, DeleteAddress and
// its balance lines included.
async function doRefreshAndRender() { async function doRefreshAndRender() {
if (refreshInFlight) { if (refreshInFlight) {
refreshAgain = true; refreshAgain = true;
@@ -166,7 +164,6 @@ async function doRefreshAndRender() {
renderWalletList(); renderWalletList();
if (state.currentView === "address") addressDetail.renderBalances(); if (state.currentView === "address") addressDetail.renderBalances();
if (state.currentView === "address-token") addressToken.renderBalance(); if (state.currentView === "address-token") addressToken.renderBalance();
if (state.currentView === "send") send.updateSendBalance();
} catch (e) { } catch (e) {
// No caller waits for this function, so it must never reject. An // No caller waits for this function, so it must never reject. An
// unhandled rejection reports nothing. The save inside it reports its // unhandled rejection reports nothing. The save inside it reports its
+61 -37
View File
@@ -1273,10 +1273,12 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// ERC-20 token from the router takes none of that token from the // ERC-20 token from the router takes none of that token from the
// sender. // sender.
let inputPaidByUser = false; let inputPaidByUser = false;
// The recipient of a WRAP_ETH that set the input side, as written; // The recipient of a WRAP_ETH that set the input side, as written,
// otherwise null. That WRAP_ETH pays its WETH there (see the Moved to // when it is not the router; otherwise null. That WRAP_ETH pays its
// lines). // WETH there, not to a swap step (see the Moved to lines).
let wrapEthRecipient = null; let wrapEthRecipient = null;
// The index of the WRAP_ETH that set the input side, or null.
let wrapEthStep = null;
// The last PERMIT2_PERMIT this decoder read before a step set the // The last PERMIT2_PERMIT this decoder read before a step set the
// input side, or null (see permitStands after the loop). // input side, or null (see permitStands after the loop).
let permit = null; let permit = null;
@@ -1317,6 +1319,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// other than WETH, and not of ETH to a WETH contract, which pays the // 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. // router WETH (see keepsRouterWeth in the loop). None of those can.
let lastWethStep = -1; let lastWethStep = -1;
// The index of the last swap step paid from WETH the router holds, a
// V4 SETTLE included, or -1 (see spendFromRouter()). Only such a step
// spends the WETH of an earlier WRAP_ETH into the router.
let lastRouterWethSwapStep = -1;
// Where the step that set the output side paid its output: the // 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 // 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, // TAKE_PORTION of it in a V4 step, a TAKE_ALL as paid to the sender,
@@ -1377,9 +1383,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// After the loop each joins the Amount line or gets lines of its own // After the loop each joins the Amount line or gets lines of its own
// (see permitStands). A take with `atMost` set may take less than // (see permitStands). A take with `atMost` set may take less than
// `amount`: it never joins the Amount line, and its line reads // `amount`: it never joins the Amount line, and its line reads
// "Up to" and the amount. A take with a `recipient` is the ETH of a // "Up to" and the amount. A take with a `recipient`, the ETH of a
// later WRAP_ETH. If that recipient is not the router, the take never // later WRAP_ETH that pays anyone but the router, never joins the
// joins the Amount line either: its Moved to line names that recipient. // Amount line either: its Moved to line names that recipient. The
// take of a later WRAP_ETH of a stated amount has that step's index as
// `wrapStep`.
const userPaid = []; const userPaid = [];
// A swap step the user pays for (`payerIsUser` true) takes `amount` // A swap step the user pays for (`payerIsUser` true) takes `amount`
// of `token` from the sender, or at most that when `atMost` is set. A // of `token` from the sender, or at most that when `atMost` is set. A
@@ -1454,17 +1462,20 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
const r = recipient.toLowerCase(); const r = recipient.toLowerCase();
return r === MSG_SENDER || r === sender; return r === MSG_SENDER || r === sender;
}; };
// A swap step or V4 SETTLE pays `amount` of `token` from the router's // A swap step or V4 SETTLE at `step` pays `amount` of `token` from the
// own balance. CONTRACT_BALANCE pays all of it. Any other quantity // router's own balance. CONTRACT_BALANCE pays all of it. Any other
// counts against the amount a WRAP_ETH stated, and the steps pay all // quantity counts against the amount a WRAP_ETH stated, and the steps
// of it once their amounts reach that one: a swap paid in ETH wraps // pay all of it once their amounts reach that one: a swap paid in ETH
// what its swap steps then spend. An amount that is not a quantity, // wraps what its swap steps then spend. An amount that is not a
// such as the null decodeV4Swap() gives a SETTLE whose payment it // quantity, such as the null decodeV4Swap() gives a SETTLE whose
// cannot tell, or one of a token whose amount is not known, pays out // payment it cannot tell, or one of a token whose amount is not known,
// nothing this decoder can see. // pays out nothing this decoder can see.
const spendFromRouter = (token, amount) => { const spendFromRouter = (step, token, amount) => {
const t = token.toLowerCase(); const t = token.toLowerCase();
const held = heldByRouter.get(t); const held = heldByRouter.get(t);
if (t === WETH.get(chainId) && heldByRouter.has(t)) {
lastRouterWethSwapStep = step;
}
if (amount === CONTRACT_BALANCE) { if (amount === CONTRACT_BALANCE) {
heldByRouter.delete(t); heldByRouter.delete(t);
} else if (typeof held === "bigint" && typeof amount === "bigint") { } else if (typeof held === "bigint" && typeof amount === "bigint") {
@@ -1750,7 +1761,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
[s.recipient], [s.recipient],
); );
if (!s.payerIsUser) { if (!s.payerIsUser) {
spendFromRouter(s.tokenIn, s.amountIn); spendFromRouter(i, s.tokenIn, s.amountIn);
} }
if (!isHop(i, s.tokenOut, s.recipient)) { if (!isHop(i, s.tokenOut, s.recipient)) {
addPayee(s.recipient, s.tokenOut); addPayee(s.recipient, s.tokenOut);
@@ -1779,7 +1790,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
[s.recipient], [s.recipient],
); );
if (!s.payerIsUser) { if (!s.payerIsUser) {
spendFromRouter(s.tokenIn, s.amountIn); spendFromRouter(i, s.tokenIn, s.amountIn);
} }
if (!isHop(i, s.tokenOut, s.recipient)) { if (!isHop(i, s.tokenOut, s.recipient)) {
addPayee(s.recipient, s.tokenOut); addPayee(s.recipient, s.tokenOut);
@@ -1795,8 +1806,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
) { ) {
// Buys exactly amountOut and spends at most amountInMax. // Buys exactly amountOut and spends at most amountInMax.
// Paid from the router, it spends an amount the calldata // Paid from the router, it spends an amount the calldata
// does not state, so it spends nothing spendFromRouter() // does not state, so spendFromRouter() has none to count.
// could count.
hasV2OrV3ExactOut = true; hasV2OrV3ExactOut = true;
const s = const s =
command === "V2_SWAP_EXACT_OUT" command === "V2_SWAP_EXACT_OUT"
@@ -1818,6 +1828,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
]), ]),
[s.recipient], [s.recipient],
); );
if (!s.payerIsUser) {
spendFromRouter(i, s.tokenIn, null);
}
if (!isHop(i, s.tokenOut, s.recipient)) { if (!isHop(i, s.tokenOut, s.recipient)) {
addPayee(s.recipient, s.tokenOut); addPayee(s.recipient, s.tokenOut);
} }
@@ -1915,7 +1928,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
w.amount, w.amount,
); );
inputPaidByUser = true; inputPaidByUser = true;
wrapEthRecipient = w.recipient; wrapEthStep = i;
if (!isRouter(w.recipient)) {
wrapEthRecipient = w.recipient;
}
} else if ( } else if (
w.amount > 0n && w.amount > 0n &&
w.amount !== CONTRACT_BALANCE w.amount !== CONTRACT_BALANCE
@@ -1930,7 +1946,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
atMost: heldByRouter.has( atMost: heldByRouter.has(
"0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000",
), ),
recipient: w.recipient, recipient: isRouter(w.recipient)
? null
: w.recipient,
wrapStep: i,
}); });
} else if ( } else if (
w.amount === CONTRACT_BALANCE && w.amount === CONTRACT_BALANCE &&
@@ -1941,11 +1960,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
) )
) { ) {
// It pays the recipient all the ETH the transaction // It pays the recipient all the ETH the transaction
// carries. It gets no lines after an UNWRAP_WETH // carries. After an earlier step paid ETH to the
// into the router, or while the router holds ETH // router there is no line: Uniswap's routes pay back
// from a V4 step: Uniswap's routes pay back what // what their swap did not spend this way, and carry
// their swap did not spend this way, and carry no // no ETH.
// ETH.
userPaid.push({ userPaid.push({
token: "0x0000000000000000000000000000000000000000", token: "0x0000000000000000000000000000000000000000",
amount: TRANSACTION_ETH, amount: TRANSACTION_ETH,
@@ -2019,7 +2037,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// Each SETTLE paid from the router spends the amount // Each SETTLE paid from the router spends the amount
// decodeV4Swap() says it pays. // decodeV4Swap() says it pays.
for (const s of v4.settledFromRouter) { for (const s of v4.settledFromRouter) {
spendFromRouter(s.currency, s.amount); spendFromRouter(i, s.currency, s.amount);
} }
for (const t of v4.takes) { for (const t of v4.takes) {
if (!isHop(i, t.currency, t.recipient)) { if (!isHop(i, t.currency, t.recipient)) {
@@ -2290,10 +2308,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
permit.beforeEveryTake && permit.beforeEveryTake &&
!stepsUnread.some((step) => step > permit.step); !stepsUnread.some((step) => step > permit.step);
// What the steps take that the Amount line does not give, as // What the steps take that the Amount line does not give, as
// { token, amount, atMost, recipient }. A null token is a step this // { token, amount, atMost, recipient, wrapStep }. A null token is a
// decoder did not read (see spentUnread()), or a token it does not // step this decoder did not read (see spentUnread()), or a token it
// know. `atMost` is as in `userPaid`. Only the take of a WRAP_ETH has // does not know. `atMost` is as in `userPaid`. Only the take of a
// a recipient (see wrapEthRecipient and userPaid). // WRAP_ETH that pays elsewhere has a recipient (see wrapEthRecipient
// and userPaid). Only the take of a WRAP_ETH has a `wrapStep`.
const spentFromSender = []; const spentFromSender = [];
let total = null; let total = null;
// The takes from the sender that `total` adds up, each a quantity, in // The takes from the sender that `total` adds up, each a quantity, in
@@ -2318,6 +2337,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
token: inputToken, token: inputToken,
amount: inputAmount, amount: inputAmount,
recipient: wrapEthRecipient, recipient: wrapEthRecipient,
wrapStep: wrapEthStep,
}); });
} }
if (hasFirstTake && !present(inputAmount)) { if (hasFirstTake && !present(inputAmount)) {
@@ -2330,6 +2350,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
token: inputToken, token: inputToken,
amount: inputAmount, amount: inputAmount,
recipient: wrapEthRecipient, recipient: wrapEthRecipient,
wrapStep: wrapEthStep,
}); });
} }
if (permitStands) inputToken = permit.token; if (permitStands) inputToken = permit.token;
@@ -2341,7 +2362,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
typeof total === "bigint" && typeof total === "bigint" &&
typeof t.amount === "bigint" && typeof t.amount === "bigint" &&
!t.atMost && !t.atMost &&
(!present(t.recipient) || isRouter(t.recipient)) !present(t.recipient)
) { ) {
total += t.amount; total += t.amount;
inTotal.push(t); inTotal.push(t);
@@ -2522,8 +2543,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// of the lines below for a PERMIT2_TRANSFER_FROM. An amount this // of the lines below for a PERMIT2_TRANSFER_FROM. An amount this
// decoder does not know is said so, and a step it did not read gets // decoder does not know is said so, and a step it did not read gets
// that one line alone. A take with no recipient goes to the swap step // that one line alone. A take with no recipient goes to the swap step
// that spends it. A take with a recipient goes to that recipient: // that spends it. The take of a WRAP_ETH into the router goes to the
// the router, the sender as `MSG_SENDER`, or an address. // router, unless a later swap step spends its WETH. A take with a
// recipient goes to that recipient; `MSG_SENDER` means the sender.
for (const s of spentFromSender) { for (const s of spentFromSender) {
details.push({ details.push({
label: "Moved from the sender", label: "Moved from the sender",
@@ -2545,10 +2567,12 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
if (!present(s.recipient)) { if (!present(s.recipient)) {
details.push({ details.push({
label: "Moved to", label: "Moved to",
value: "The swap step that spends it", value:
present(s.wrapStep) &&
s.wrapStep > lastRouterWethSwapStep
? "The router"
: "The swap step that spends it",
}); });
} else if (isRouter(s.recipient)) {
details.push({ label: "Moved to", value: "The router" });
} else if (s.recipient.toLowerCase() === MSG_SENDER) { } else if (s.recipient.toLowerCase() === MSG_SENDER) {
details.push({ label: "Moved to", value: "The sender" }); details.push({ label: "Moved to", value: "The sender" });
} else { } else {
-61
View File
@@ -19,17 +19,12 @@
// its ETH line reads the same, and Send states it as unknown // its ETH line reads the same, and Send states it as unknown
// (https://git.eeqj.de/sneak/AutistMask/issues/736). // (https://git.eeqj.de/sneak/AutistMask/issues/736).
// //
// A refresh that returns while Send is up draws Send's balance line again, and
// leaves what the user entered as it is
// (https://git.eeqj.de/sneak/AutistMask/issues/748).
//
// Driven through the real popup over a stored profile on Ethereum Mainnet, // Driven through the real popup over a stored profile on Ethereum Mainnet,
// with "Show tracked tokens with zero balance" on, as it is by default. // with "Show tracked tokens with zero balance" on, as it is by default.
const { HDNodeWallet, Mnemonic } = require("ethers"); const { HDNodeWallet, Mnemonic } = require("ethers");
const { const {
ADDRESS,
bootPopup, bootPopup,
cleanupPopup, cleanupPopup,
unversionedValidProfile, unversionedValidProfile,
@@ -436,62 +431,6 @@ test("Send for the new token reads its balance as unknown before any refresh has
await finish([MILLION]); await finish([MILLION]);
}); });
test("Send for the new token reads the fetched balance once the refresh returns, without being reopened", async () => {
const page = await bootOnAddress();
const finish = holdNextRefresh(page);
await addNewToken(page);
await openToken(page, NEW_TOKEN.toLowerCase());
await page.click("btn-address-token-send");
expect(page.text("send-balance")).toBe("Current balance: unknown (NEW)");
await finish([MILLION]);
expect(page.visibleViews()).toEqual(["send"]);
expect(page.text("send-balance")).toBe("Current balance: 1000000.0000 NEW");
});
test("a refresh that returns while Send is up draws an unread ETH balance again and leaves the inputs alone", async () => {
const page = await bootPopup(profileNotRead());
// The dropdown's first option, which a browser selects by itself.
page.node("send-token").value = "ETH";
await page.click("btn-main-send");
expect(page.text("send-balance")).toBe("Current balance: unknown (ETH)");
const recipient = "0x" + "dd".repeat(20);
page.node("send-to").value = recipient;
page.node("send-amount").value = "0.5";
// Stands for the options on the screen, which a refresh leaves as they are.
page.node("send-token").innerHTML = "options on the screen";
// Stands for the From address from before the refresh.
page.node("send-from").innerHTML = "From address before the refresh";
await refreshOnce(page, []);
expect(page.visibleViews()).toEqual(["send"]);
expect(page.text("send-balance")).toBe("Current balance: 2.0000 ETH");
expect(page.node("send-from").innerHTML).toContain(ADDRESS);
expect(page.value("send-to")).toBe(recipient);
expect(page.value("send-amount")).toBe("0.5");
expect(page.value("send-token")).toBe("ETH");
expect(page.node("send-token").innerHTML).toBe("options on the screen");
});
test("a refresh that returns while Send is up keeps the token picked in the dropdown and draws its new balance", async () => {
const page = await bootOnDerivingWallet();
const dropdown = page.node("send-token");
dropdown.value = "ETH";
await page.click("btn-main-send");
dropdown.value = NEW_TOKEN;
for (const fn of dropdown.listeners.change) await fn();
expect(page.text("send-balance")).toBe("Current balance: 1000000.0000 NEW");
page.node("send-amount").value = "0.5";
await refreshOnce(page, [{ ...MILLION, balance: "3000000.0" }]);
expect(page.text("send-balance")).toBe("Current balance: 3000000.0000 NEW");
expect(page.value("send-token")).toBe(NEW_TOKEN);
expect(page.value("send-amount")).toBe("0.5");
});
test("a token added by contract address on SettingsAddToken reads quantity unknown until the refresh returns", async () => { test("a token added by contract address on SettingsAddToken reads quantity unknown until the refresh returns", async () => {
const page = await bootPopup(unversionedValidProfile()); const page = await bootPopup(unversionedValidProfile());
expect(page.visibleViews()).toEqual(["main"]); expect(page.visibleViews()).toEqual(["main"]);
+21 -43
View File
@@ -2475,10 +2475,8 @@ describe("uniswap decoder", () => {
// After an earlier step that may leave ETH in the router, the ETH that // After an earlier step that may leave ETH in the router, the ETH that
// the user pays a V4 step reads as a maximum, on lines of its own. // the user pays a V4 step reads as a maximum, on lines of its own.
// Every row reads at least the ETH that can leave the wallet. The row // Every row reads at least the ETH that can leave the wallet. The row
// marked "reads as on next" reads the same on next. The row with a // marked "reads as on next" reads the same on next. On next every other
// SETTLE_ALL of at most 3 ETH read that ETH as not known on next. On // row added that ETH to the Amount line, or read it as an exact amount.
// next every other row added that ETH to the Amount line, or read it as
// an exact amount.
// //
// ADDRESS_THIS is the router on every release. // ADDRESS_THIS is the router on every release.
const ADDRESS_THIS = "0x0000000000000000000000000000000000000002"; const ADDRESS_THIS = "0x0000000000000000000000000000000000000002";
@@ -2917,10 +2915,10 @@ describe("uniswap decoder", () => {
expect(linesBetweenTokens(result)).toEqual(lines); expect(linesBetweenTokens(result)).toEqual(lines);
}); });
// https://git.eeqj.de/sneak/AutistMask/issues/747 — a WRAP_ETH into the // https://git.eeqj.de/sneak/AutistMask/issues/747 — the WETH of a
// router pays its WETH to the router, whichever step then takes it. On // WRAP_ETH into the router goes to a swap step only when a later swap
// next that Moved to line read The swap step that spends it in every // step paid from the router spends it. On next every row not marked
// row. // "reads as on next" read The swap step that spends it.
const SWAP_STEP = "The swap step that spends it"; const SWAP_STEP = "The swap step that spends it";
// Each pays 1 WETH from the router, for USDC, to the user. // Each pays 1 WETH from the router, for USDC, to the user.
const V3_WETH_FROM_ROUTER = coder.encode( const V3_WETH_FROM_ROUTER = coder.encode(
@@ -3005,30 +3003,24 @@ describe("uniswap decoder", () => {
], ],
lines: [ lines: [
["Amount", "600.0000 USDT"], ["Amount", "600.0000 USDT"],
...wrapLines("The router"), ...wrapLines(SWAP_STEP),
...wrapLines("The router"), ...wrapLines("The router"),
], ],
}, },
{ {
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, then a V3 exact-in paid from the router's WETH", kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, then a V3 exact-in paid from the router's WETH, reads as on next",
commands: [0x00, 0x0b, 0x00], commands: [0x00, 0x0b, 0x00],
inputs: [V3_600, WRAP_INTO_ROUTER, V3_WETH_FROM_ROUTER], inputs: [V3_600, WRAP_INTO_ROUTER, V3_WETH_FROM_ROUTER],
lines: [ lines: [["Amount", "600.0000 USDT"], ...wrapLines(SWAP_STEP)],
["Amount", "600.0000 USDT"],
...wrapLines("The router"),
],
}, },
{ {
kind: "the same with a V2 exact-in paid from the router's WETH", kind: "the same with a V2 exact-in paid from the router's WETH, reads as on next",
commands: [0x00, 0x0b, 0x08], commands: [0x00, 0x0b, 0x08],
inputs: [V3_600, WRAP_INTO_ROUTER, V2_WETH_FROM_ROUTER], inputs: [V3_600, WRAP_INTO_ROUTER, V2_WETH_FROM_ROUTER],
lines: [ lines: [["Amount", "600.0000 USDT"], ...wrapLines(SWAP_STEP)],
["Amount", "600.0000 USDT"],
...wrapLines("The router"),
],
}, },
{ {
kind: "the same with a V3 exact-out paid from the router's WETH", kind: "the same with a V3 exact-out paid from the router's WETH, reads as on next",
commands: [0x00, 0x0b, 0x01], commands: [0x00, 0x0b, 0x01],
inputs: [ inputs: [
V3_600, V3_600,
@@ -3045,17 +3037,14 @@ describe("uniswap decoder", () => {
["Amount", "Up to 600.0000 USDT"], ["Amount", "Up to 600.0000 USDT"],
["Moved from the sender", "Up to 1.0000 ETH"], ["Moved from the sender", "Up to 1.0000 ETH"],
["Token moved", "ETH"], ["Token moved", "ETH"],
["Moved to", "The router"], ["Moved to", SWAP_STEP],
], ],
}, },
{ {
kind: "the same with a V4 swap that the router's WETH settles", kind: "the same with a V4 swap that the router's WETH settles, reads as on next",
commands: [0x00, 0x0b, 0x10], commands: [0x00, 0x0b, 0x10],
inputs: [V3_600, WRAP_INTO_ROUTER, V4_WETH_FROM_ROUTER], inputs: [V3_600, WRAP_INTO_ROUTER, V4_WETH_FROM_ROUTER],
lines: [ lines: [["Amount", "600.0000 USDT"], ...wrapLines(SWAP_STEP)],
["Amount", "600.0000 USDT"],
...wrapLines("The router"),
],
}, },
{ {
kind: "a WRAP_ETH of 1 ETH into the router that sets the input side, a SWEEP of that WETH to another address, then an EXECUTE_SUB_PLAN", kind: "a WRAP_ETH of 1 ETH into the router that sets the input side, a SWEEP of that WETH to another address, then an EXECUTE_SUB_PLAN",
@@ -3067,11 +3056,11 @@ describe("uniswap decoder", () => {
], ],
}, },
{ {
kind: "a WRAP_ETH of 1 ETH into the router that sets the input side, a V3 exact-in paid from the router's WETH, then an EXECUTE_SUB_PLAN", kind: "a WRAP_ETH of 1 ETH into the router that sets the input side, a V3 exact-in paid from the router's WETH, then an EXECUTE_SUB_PLAN, reads as on next",
commands: [0x0b, 0x00, 0x21], commands: [0x0b, 0x00, 0x21],
inputs: [WRAP_INTO_ROUTER, V3_WETH_FROM_ROUTER, SUB_PLAN], inputs: [WRAP_INTO_ROUTER, V3_WETH_FROM_ROUTER, SUB_PLAN],
lines: [ lines: [
...wrapLines("The router"), ...wrapLines(SWAP_STEP),
["Moved from the sender", UNDECODED], ["Moved from the sender", UNDECODED],
], ],
}, },
@@ -3089,7 +3078,7 @@ describe("uniswap decoder", () => {
], ],
}, },
{ {
kind: "the same with a V3 exact-in paid from the router's WETH after the WRAP_ETH", kind: "the same with a V3 exact-in paid from the router's WETH after the WRAP_ETH, reads as on next",
commands: [0x0a, 0x0b, 0x00, 0x00], commands: [0x0a, 0x0b, 0x00, 0x00],
inputs: [ inputs: [
encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR), encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR),
@@ -3097,10 +3086,7 @@ describe("uniswap decoder", () => {
V3_WETH_FROM_ROUTER, V3_WETH_FROM_ROUTER,
V3_600, V3_600,
], ],
lines: [ lines: [["Amount", "5000.0000 USDT"], ...wrapLines(SWAP_STEP)],
["Amount", "5000.0000 USDT"],
...wrapLines("The router"),
],
}, },
])("$kind", ({ commands, inputs, lines }) => { ])("$kind", ({ commands, inputs, lines }) => {
const result = decodeSteps(commands, inputs); const result = decodeSteps(commands, inputs);
@@ -3110,10 +3096,8 @@ describe("uniswap decoder", () => {
// https://git.eeqj.de/sneak/AutistMask/issues/747 — a later WRAP_ETH // https://git.eeqj.de/sneak/AutistMask/issues/747 — a later WRAP_ETH
// of CONTRACT_BALANCE wraps all the ETH the router holds. Unless an // of CONTRACT_BALANCE wraps all the ETH the router holds. Unless an
// earlier step paid ETH to the router, that is the ETH the transaction // earlier step paid ETH to the router, that is the ETH the transaction
// carries. On next no line gave it. Into the router it gets no lines. // carries. On next no line gave it. Uniswap's own routes pay back what
// It gets none after an UNWRAP_WETH into the router, or while the // their swap did not spend this way, and carry no ETH, so in the rows
// router holds ETH from a V4 step: Uniswap's own routes pay back what
// their swap did not spend this way, and carry no ETH. In the rows
// marked "reads as on next" that WRAP_ETH gets no lines. // marked "reads as on next" that WRAP_ETH gets no lines.
const WRAP_ALL_TO_OTHER = encodeWrapEth(OTHER_ADDR, CONTRACT_BALANCE); const WRAP_ALL_TO_OTHER = encodeWrapEth(OTHER_ADDR, CONTRACT_BALANCE);
const WRAP_ALL_TO_SENDER = encodeWrapEth(MSG_SENDER, CONTRACT_BALANCE); const WRAP_ALL_TO_SENDER = encodeWrapEth(MSG_SENDER, CONTRACT_BALANCE);
@@ -3197,12 +3181,6 @@ describe("uniswap decoder", () => {
inputs: [WRAP_INTO_ROUTER, WRAP_ALL_TO_OTHER], inputs: [WRAP_INTO_ROUTER, WRAP_ALL_TO_OTHER],
lines: [["Amount", "1.0000 ETH"], ...allEthLines(OTHER_ADDR)], lines: [["Amount", "1.0000 ETH"], ...allEthLines(OTHER_ADDR)],
}, },
{
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of CONTRACT_BALANCE into the router, reads as on next",
commands: [0x00, 0x0b],
inputs: [V3_600, encodeWrapEth(ADDRESS_THIS, CONTRACT_BALANCE)],
lines: [["Amount", "600.0000 USDT"]],
},
{ {
kind: "a V3 exact-in of 600 USDT, an UNWRAP_WETH to the sender, then a WRAP_ETH of CONTRACT_BALANCE to another address", kind: "a V3 exact-in of 600 USDT, an UNWRAP_WETH to the sender, then a WRAP_ETH of CONTRACT_BALANCE to another address",
commands: [0x00, 0x0c, 0x0b], commands: [0x00, 0x0c, 0x0b],