2 Commits
Author SHA1 Message Date
sneak 56014a264b fix: any token a swap leaves in the router keeps it from reading as received (closes #606)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
The decoder checked only the swap's output for being left in the router.
It now records every token a step pays the router, the WETH of a
WRAP_ETH and the rest of a V4 SETTLE taken to the router among them, and
any token still there after the last step gets a Paid to line naming it,
so the minimum reads Min. paid out. Steps that spend all the router holds
of a token, and swap steps from the router that together spend what a
WRAP_ETH stated, count as spending it. A SETTLE of OPEN_DELTA from the
router counts only in the plain shape of Uniswap's own routes.

Model: opus-5-5
2026-10-19 18:58:27 +00:00
clawbot 2c882c75d6 fix: refusing a From address the wallet does not hold moves nothing (closes #609)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
Sign & Send wrote its refusal of a From address the selected wallet does not
hold onto the password error line, which reserves one line. The sentence runs
to about three, so it pushed Sign & Send down.

The refusal now has its own box above the password field, its sentence fixed
in index.html, holding its space from the first paint and only made visible,
as #583 did for the other
refusals. Drawing the screen and each press hide it again. A Chrome test
writes the other wallet into the stored selection, reopens the popup onto the
screen, presses Sign & Send and checks that nothing was broadcast and nothing
moved.

Model: opus-5-5
2026-10-19 20:50:02 +02:00
7 changed files with 341 additions and 82 deletions
+41 -32
View File
@@ -433,7 +433,11 @@ previously correct by reading only.
It also covers a send that Sign & Send refuses for its network fee: the stub It also covers a send that Sign & Send refuses for its network fee: the stub
node quotes an ordinary fee while the confirmation screen is drawn and one over node quotes an ordinary fee while the confirmation screen is drawn and one over
the bound when Sign & Send asks again, and the refusal must broadcast nothing the bound when Sign & Send asks again, and the refusal must broadcast nothing
and leave the password field and Sign & Send where they were. and leave the password field and Sign & Send where they were. It checks the same
for a send refused because the selected wallet does not hold its From address:
with the confirmation screen showing a send from the HD wallet, the key wallet
is selected by writing it straight into extension storage, as another popup
selecting it would save it, and the popup is reopened onto that screen.
It also covers both ways the wait for a sent transaction's receipt ends on the It also covers both ways the wait for a sent transaction's receipt ends on the
error screen: lookups that still find no receipt 60 seconds after the broadcast, error screen: lookups that still find no receipt 60 seconds after the broadcast,
@@ -1814,9 +1818,10 @@ view would leave a wallet one click from deletion.
estimated, and it says the wallet does not know how many decimal places estimated, and it says the wallet does not know how many decimal places
the token uses and that the transaction cannot be sent; for any other the token uses and that the transaction cannot be sent; for any other
failure it asks the user to go back and try again failure it asks the user to go back and try again
- Two more reserved error boxes, made visible when "Sign & Send" refuses a - Three more reserved error boxes, made visible when "Sign & Send" refuses a
send because the active network changed or because of its gas limit or fee send because the active network changed, because the selected wallet does
(see Transitions), so the refusal moves nothing not hold the From address, or because of its gas limit or fee (see
Transitions), so the refusal moves nothing
- Password: an inline field on this screen, not a modal, with its own error - Password: an inline field on this screen, not a modal, with its own error
line line
- "Sign & Send" button (disabled if errors, while the balance is being read, - "Sign & Send" button (disabled if errors, while the balance is being read,
@@ -1860,8 +1865,9 @@ view would leave a wallet one click from deletion.
- "Sign & Send" signs as the From address above, not as whichever address is - "Sign & Send" signs as the From address above, not as whichever address is
selected: the two are saved separately, so the popup can reopen onto this selected: the two are saved separately, so the popup can reopen onto this
screen with another address selected. When the selected wallet does not screen with another address selected. When the selected wallet does not
hold the From address → full-sentence error on the password error line, hold the From address → nothing is signed; "The selected wallet does not
nothing sent (no screen change) hold the From address above, so nothing was sent. Please go back and send
again." in its own box, no screen change
- "Sign & Send" (correct password, but the From address's key is refused, - "Sign & Send" (correct password, but the From address's key is refused,
see Data Model) → nothing is signed → **ErrorTx** saying why see Data Model) → nothing is signed → **ErrorTx** saying why
- "Back" → **Send** - "Back" → **Send**
@@ -3060,32 +3066,35 @@ Currently supported:
or V3 exact-in swap step or V4 `SETTLE` that pays `CONTRACT_BALANCE` of that or V3 exact-in swap step or V4 `SETTLE` that pays `CONTRACT_BALANCE` of that
token from the router (`payerIsUser` false). Such steps also spend all the token from the router (`payerIsUser` false). Such steps also spend all the
WETH a `WRAP_ETH` paid the router once together they spend the amount the WETH a `WRAP_ETH` paid the router once together they spend the amount the
`WRAP_ETH` states, as a swap paid in ETH does; a `SETTLE` of `OPEN_DELTA` `WRAP_ETH` states, as a swap paid in ETH does. The router pays a `SETTLE` of
(zero) from the router counts as spending what its step's swap spends only `OPEN_DELTA` (zero) only what its step still owes of that token when the
when it is the step's only `SETTLE` or `SETTLE_ALL` of that token, comes after `SETTLE` runs, so one from the router counts as spending the amounts the
every swap action in the step, and no swap action in the step buys that token, step's swap actions state they spend of that token only when every action in
because the router pays only what the step still owes of that token when the the step could be read, every swap action names the token it spends and the
`SETTLE` runs; otherwise it counts as spending nothing. So the rest of a V4 token it buys (a swap action with an empty path swaps nothing), the `SETTLE`
`SETTLE` of more than its swap spends, taken to the router, the WETH of a is the step's only `SETTLE` or `SETTLE_ALL` of that token and comes after
`WRAP_ETH`, and a token a swap pays the router on the way to the output, each every swap action, no swap action buys that token, and the swap actions that
stays in the router when later steps do not spend all of it, where anyone can spend it state a quantity together; otherwise it counts as spending nothing.
take it in a later transaction. The router sends ETH as written, so a So the rest of a V4 `SETTLE` of more than its swap spends, taken to the
`TRANSFER` of `CONTRACT_BALANCE` of ETH always reverts and pays out nothing. A router, the WETH of a `WRAP_ETH`, and a token a swap pays the router on the
step paying a token to the V2 pair that the very next step spends, as a V2 way to the output, each stays in the router when later steps do not spend all
exact-in of that token whose `amountIn` is `ALREADY_PAID` (zero) or of it, where anyone can take it in a later transaction. The router sends ETH
`CONTRACT_BALANCE`, is a hop, not a payout: Uniswap's mixed routes pay the as written, so a `TRANSFER` of `CONTRACT_BALANCE` of ETH always reverts and
next V2 pair directly, and the router enforces that swap's own minimum. The pays out nothing. A step paying a token to the V2 pair that the very next step
decoder checks that the address paid is that pair, deriving it from the V2 spends, as a V2 exact-in of that token whose `amountIn` is `ALREADY_PAID`
factory and pair init code hash the network's routers were deployed with and (zero) or `CONTRACT_BALANCE`, is a hop, not a payout: Uniswap's mixed routes
the first two tokens of that step's path; a payment to any other address is pay the next V2 pair directly, and the router enforces that swap's own
named. (Sepolia's `UniversalRouterV1_2_V2Support` was deployed with minimum. The decoder checks that the address paid is that pair, deriving it
placeholders for both values, so no V2 swap step can succeed on that router.) from the V2 factory and pair init code hash the network's routers were
Every other payment counts, in whichever step and of whatever token, a share deployed with and the first two tokens of that step's path; a payment to any
of the output included: an interface fee paid to another address by other address is named. (Sepolia's `UniversalRouterV1_2_V2Support` was
`PAY_PORTION`, `TRANSFER` or `TAKE_PORTION` is named, and the minimum reads deployed with placeholders for both values, so no V2 swap step can succeed on
`Min. paid out`, because the swap's minimum includes the fee. The minimum that router.) Every other payment counts, in whichever step and of whatever
reads `Min. received` only when some step pays the transaction's sender (the token, a share of the output included: an interface fee paid to another
From address on TxApproval, the sender the explorer reports on address by `PAY_PORTION`, `TRANSFER` or `TAKE_PORTION` is named, and the
minimum reads `Min. paid out`, because the swap's minimum includes the fee.
The minimum reads `Min. received` only when some step pays the transaction's
sender (the From address on TxApproval, the sender the explorer reports on
TransactionDetail) and no `Paid to` line is shown. `Paid to` lines after TransactionDetail) and no `Paid to` line is shown. `Paid to` lines after
`Token Out` name each other address paid; one reads `Token Out` name each other address paid; one reads
`The bridge, which pays the deposit's recipient on another network` for a `The bridge, which pays the deposit's recipient on another network` for a
+11 -3
View File
@@ -697,9 +697,10 @@
id="confirm-fee-unknown-error" id="confirm-fee-unknown-error"
class="mb-2 border border-border border-dashed p-2 text-xs invisible" class="mb-2 border border-border border-dashed p-2 text-xs invisible"
></div> ></div>
<!-- Shown when Sign & Send refuses a send for its network, or <!-- Shown when Sign & Send refuses a send for its network, for
for its gas limit or fee. Each holds its space from the first a From address the selected wallet does not hold, or for its
paint, so the refusal moves nothing. --> gas limit or fee. Each holds its space from the first paint, so
the refusal moves nothing. -->
<div <div
id="confirm-network-changed-error" id="confirm-network-changed-error"
class="mb-2 border border-border border-dashed p-2 text-xs invisible" class="mb-2 border border-border border-dashed p-2 text-xs invisible"
@@ -708,6 +709,13 @@
reviewed, so it was not sent. Please go back and review it reviewed, so it was not sent. Please go back and review it
again. again.
</div> </div>
<div
id="confirm-from-address-error"
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
>
The selected wallet does not hold the From address above, so
nothing was sent. Please go back and send again.
</div>
<div <div
id="confirm-fee-bound-error" id="confirm-fee-bound-error"
class="mb-2 border border-border border-dashed p-2 text-xs invisible" class="mb-2 border border-border border-dashed p-2 text-xs invisible"
+3 -5
View File
@@ -320,6 +320,7 @@ function show(txInfo) {
$("confirm-tx-password").value = ""; $("confirm-tx-password").value = "";
hideError("confirm-tx-password-error"); hideError("confirm-tx-password-error");
setVisible("confirm-network-changed-error", false); setVisible("confirm-network-changed-error", false);
setVisible("confirm-from-address-error", false);
setVisible("confirm-fee-bound-error", false); setVisible("confirm-fee-bound-error", false);
// Gas estimate — show placeholder then fetch async // Gas estimate — show placeholder then fetch async
@@ -661,6 +662,7 @@ function init(_ctx) {
$("btn-confirm-send").addEventListener("click", async () => { $("btn-confirm-send").addEventListener("click", async () => {
// A press shows only its own refusal, if any. // A press shows only its own refusal, if any.
setVisible("confirm-network-changed-error", false); setVisible("confirm-network-changed-error", false);
setVisible("confirm-from-address-error", false);
setVisible("confirm-fee-bound-error", false); setVisible("confirm-fee-bound-error", false);
const wallet = state.wallets[state.selectedWallet]; const wallet = state.wallets[state.selectedWallet];
@@ -686,11 +688,7 @@ function init(_ctx) {
// address selected. // address selected.
const addr = wallet.addresses.find((a) => a.address === pendingTx.from); const addr = wallet.addresses.find((a) => a.address === pendingTx.from);
if (!addr) { if (!addr) {
showError( setVisible("confirm-from-address-error", true);
"confirm-tx-password-error",
"The selected wallet does not hold the From address above, " +
"so nothing was sent. Please go back and send again.",
);
return; return;
} }
+56 -39
View File
@@ -887,9 +887,9 @@ function totalAmount(amounts) {
// a share of the currency, so it does not name the output side as TAKE does. // a share of the currency, so it does not name the output side as TAKE does.
// `recipientUnread` is set when the step has a payout this decoder did not // `recipientUnread` is set when the step has a payout this decoder did not
// read. `settledFromRouter` is every SETTLE that the router pays from its own // read. `settledFromRouter` is every SETTLE that the router pays from its own
// balance (`payerIsUser` false), as the currency, the amount as written and // balance (`payerIsUser` false), as its currency and the amount it pays: a
// its index among the step's actions, except that a SETTLE of OPEN_DELTA // quantity, CONTRACT_BALANCE (all the router holds), or null when this
// whose payment this decoder cannot follow has no amount (null; see below). // decoder cannot tell (see below).
function decodeV4Swap(input) { function decodeV4Swap(input) {
try { try {
const d = coder.decode(["bytes", "bytes[]"], input); const d = coder.decode(["bytes", "bytes[]"], input);
@@ -899,11 +899,15 @@ function decodeV4Swap(input) {
let settleToken = null; let settleToken = null;
let takeToken = null; let takeToken = null;
const takes = []; const takes = [];
const settledFromRouter = []; // Every SETTLE paid from the router, as its currency, its amount as
// The currency of every SETTLE and SETTLE_ALL, in order. // written and its index among the step's actions.
const routerSettles = [];
// The currency of every SETTLE and SETTLE_ALL.
const settledCurrencies = []; const settledCurrencies = [];
// The index of the step's last swap action, read or not, or -1. // The index of the step's last swap action, read or not, or -1.
let lastSwapAction = -1; let lastSwapAction = -1;
// Set when an action of the step could not be read.
let actionUnread = false;
let recipientUnread = false; let recipientUnread = false;
// The step's swap actions that could be read, as decodeV4SwapAction() // The step's swap actions that could be read, as decodeV4SwapAction()
// reads them. // reads them.
@@ -925,7 +929,7 @@ function decodeV4Swap(input) {
settleToken = s[0]; settleToken = s[0];
settledCurrencies.push(s[0]); settledCurrencies.push(s[0]);
if (!s[2]) { if (!s[2]) {
settledFromRouter.push({ routerSettles.push({
currency: s[0], currency: s[0],
amount: s[1], amount: s[1],
index: i, index: i,
@@ -967,14 +971,18 @@ function decodeV4Swap(input) {
swaps.push(decodeV4SwapAction(actionId, params[i])); swaps.push(decodeV4SwapAction(actionId, params[i]));
} catch { } catch {
swapUndecoded = true; swapUndecoded = true;
actionUnread = true;
} }
} else {
// V4Router reverts on any action not named above, so
// none of them pays anyone. This decoder reads none.
actionUnread = true;
} }
// V4Router reverts on any action not named above, so none
// of them pays anyone.
} catch { } catch {
// Skip sub-actions we can't decode. A TAKE, TAKE_ALL or // Skip sub-actions we can't decode. A TAKE, TAKE_ALL or
// TAKE_PORTION among them is a payout this decoder did not // TAKE_PORTION among them is a payout this decoder did not
// read. // read.
actionUnread = true;
if ( if (
actionId === V4_TAKE || actionId === V4_TAKE ||
actionId === V4_TAKE_ALL || actionId === V4_TAKE_ALL ||
@@ -985,23 +993,40 @@ function decodeV4Swap(input) {
} }
} }
// A SETTLE of OPEN_DELTA (zero) paid from the router pays what the // What each SETTLE paid from the router pays. A quantity or
// step still owes of its currency when it runs. That is what the // CONTRACT_BALANCE pays as written. A SETTLE of OPEN_DELTA (zero)
// step's swap actions spend of it only when the SETTLE is the step's // pays whatever the step still owes of its currency when it runs.
// one SETTLE or SETTLE_ALL of that currency, comes after every swap // That is what the step's swap actions state they spend of it only
// action, and no swap action buys that currency (an unread one may). // in the plain shape of Uniswap's own routes, so only then is it
// Any other such SETTLE pays an amount this decoder cannot tell. // given that amount: every action in the step was read; every swap
for (const s of settledFromRouter) { // action names the currency it spends and the one it buys (an
// exact-in action with an empty path swaps nothing); the SETTLE is
// the step's only SETTLE or SETTLE_ALL of its currency and comes
// after every swap action; no swap action buys that currency; and
// the swap actions that spend it state a quantity together (see
// totalAmount()). Otherwise it is given null.
const settledFromRouter = routerSettles.map((s) => {
if (s.amount !== 0n) {
return { currency: s.currency, amount: s.amount };
}
const stated = totalAmount(
swaps
.filter((w) => w.tokenIn === s.currency)
.map((w) => w.amountIn),
);
const paysWhatSwapsSpend = const paysWhatSwapsSpend =
!actionUnread &&
swaps.every((w) => present(w.tokenIn) && present(w.tokenOut)) &&
settledCurrencies.filter((c) => c === s.currency).length === settledCurrencies.filter((c) => c === s.currency).length ===
1 && 1 &&
s.index > lastSwapAction && s.index > lastSwapAction &&
!swapUndecoded && !swaps.some((w) => w.tokenOut === s.currency) &&
!swaps.some((w) => w.tokenOut === s.currency); typeof stated === "bigint";
if (s.amount === 0n && !paysWhatSwapsSpend) { return {
s.amount = null; currency: s.currency,
} amount: paysWhatSwapsSpend ? stated : null,
} };
});
// Rule 3. // Rule 3.
if (swapUndecoded) { if (swapUndecoded) {
@@ -1164,12 +1189,13 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
return r === ADDRESS_THIS || r === toAddress.toLowerCase(); return r === ADDRESS_THIS || r === toAddress.toLowerCase();
}; };
// A swap step or V4 SETTLE pays `amount` of `token` from the router's // A swap step or V4 SETTLE pays `amount` of `token` from the router's
// own balance. CONTRACT_BALANCE pays all of it. Another amount the // own balance. CONTRACT_BALANCE pays all of it. Any other quantity
// step states counts against the amount a WRAP_ETH stated, and the // counts against the amount a WRAP_ETH stated, and the steps pay all
// steps pay all of it once their amounts reach that one: a swap paid // of it once their amounts reach that one: a swap paid in ETH wraps
// in ETH wraps what its swap steps then spend. An amount the step // what its swap steps then spend. An amount that is not a quantity,
// does not state, or one of a token whose amount is not known, pays // such as the null decodeV4Swap() gives a SETTLE whose payment it
// out nothing this decoder can see. // cannot tell, or one of a token whose amount is not known, pays out
// nothing this decoder can see.
const spendFromRouter = (token, amount) => { const spendFromRouter = (token, amount) => {
const t = token.toLowerCase(); const t = token.toLowerCase();
const held = heldByRouter.get(t); const held = heldByRouter.get(t);
@@ -1457,19 +1483,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} else if (command === "V4_SWAP") { } else if (command === "V4_SWAP") {
const v4 = decodeV4Swap(inputs[i]); const v4 = decodeV4Swap(inputs[i]);
if (v4) { if (v4) {
// A SETTLE of OPEN_DELTA (zero) that decodeV4Swap() // Each SETTLE paid from the router spends the amount
// left at zero pays what the step's swap actions // decodeV4Swap() says it pays.
// spend of its currency, which the step states as its
// amount when that currency is its Token In.
for (const s of v4.settledFromRouter) { for (const s of v4.settledFromRouter) {
let amount = s.amount; spendFromRouter(s.currency, s.amount);
if (amount === 0n) {
amount =
s.currency === v4.tokenIn
? v4.amountIn
: null;
}
spendFromRouter(s.currency, 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)) {
+17 -3
View File
@@ -442,11 +442,25 @@ describe("a confirmation screen whose From is not the selected address", () => {
expect(Transaction.from(signed).from).toBe(derived(1).address); expect(Transaction.from(signed).from).toBe(derived(1).address);
}); });
// The refusal's sentence is fixed in index.html, in a box of its own that
// holds its space from the first paint, so showing it moves nothing.
test("nothing is sent when the selected wallet does not hold it", async () => { test("nothing is sent when the selected wallet does not hold it", async () => {
expect(await confirmSend(derived(5).address, 0)).toBeUndefined(); expect(await confirmSend(derived(5).address, 0)).toBeUndefined();
expect(node("confirm-tx-password-error").textContent).toBe( expect(node("confirm-from-address-error").style.visibility).toBe(
"The selected wallet does not hold the From address above, " + "visible",
"so nothing was sent. Please go back and send again.", );
expect(node("confirm-tx-password-error").textContent).toBe("");
// Reviewing a send again draws the screen again, which hides it.
confirmTx.show({
from: derived(0).address,
to: RECIPIENT,
amount: "0.5",
token: "ETH",
});
await settle();
expect(node("confirm-from-address-error").style.visibility).toBe(
"hidden",
); );
}); });
}); });
+119
View File
@@ -3153,6 +3153,125 @@ test("a send refused for its fee leaves the password field and Sign & Send where
} }
}); });
// --------- a send from an address the selected wallet does not hold (#609)
//
// The selection is saved apart from the confirmation screen, so the popup can
// reopen onto the screen with a wallet selected that does not hold its From
// address, and Sign & Send then refuses the send. Another popup selecting the
// key wallet would save that selection; here it is written into storage, and
// the popup reopened.
test("a send from an address the selected wallet does not hold leaves the password field and Sign & Send where they were (#609)", async (env) => {
try {
// From the HD wallet's address.
await goToConfirm(env.page, {
token: "ETH",
balance: FUNDED_ETH_TEXT + " ETH",
amount: COMFORTABLE_AMOUNT,
});
// The screen's own save lands first, so it cannot write over the
// selection below.
await waitForPersisted(
env.page,
"currentView",
"confirm-tx",
"before selecting the key wallet",
);
await env.page.evaluate(
() =>
new Promise((resolve) => {
chrome.storage.local.get("autistmask", (r) => {
const record = {
...r.autistmask,
selectedWallet: 1,
selectedAddress: 0,
};
chrome.storage.local.set(
{ autistmask: record },
resolve,
);
});
}),
);
await reopenPopup(env, "confirm-tx");
await waitForEstimate(env.page);
// The refusal shown anywhere on the screen, not only in its own box:
// shown on the password error line instead, the test fails on the
// moves checked below rather than on the wait for it.
const refusal = env.page
.locator("#view-confirm-tx")
.getByText("does not hold the From address above")
.filter({ visible: true });
const before = await confirmState(env.page);
assert(
!before.sendDisabled && (await refusal.count()) === 0,
"the screen is not ready to send: " + JSON.stringify(before),
);
const sent = env.routeOpts.broadcastTransactions.length;
await fillPasswordAndSend(env.page);
await refusal.waitFor({ timeout: 60000 });
const after = await confirmState(env.page);
// Printed pass or fail, as the dust threshold test does.
console.log(
"# refused From address: password field top " +
before.passwordTop +
" -> " +
after.passwordTop +
"px, Sign & Send top " +
before.sendTop +
" -> " +
after.sendTop +
"px",
);
assert(
env.routeOpts.broadcastTransactions.length === sent,
"a send from an address the selected wallet does not hold" +
" reached the RPC",
);
assert(
after.passwordTop === before.passwordTop,
"the password field moved " +
(after.passwordTop - before.passwordTop) +
"px when the send was refused",
);
assert(
after.sendTop === before.sendTop,
"Sign & Send moved " +
(after.sendTop - before.sendTop) +
"px when the send was refused",
);
// Only now, where the refusal is shown. Its sentence is wrapped across
// lines in index.html.
const shownIn = await refusal.evaluate((el) => ({
id: el.id,
text: el.textContent.replace(/\s+/g, " ").trim(),
}));
assert(
shownIn.id === "confirm-from-address-error" &&
shownIn.text ===
"The selected wallet does not hold the From address above," +
" so nothing was sent. Please go back and send again.",
"wrong From address refusal: " + JSON.stringify(shownIn),
);
} finally {
// A reopened popup restores no Send screen beneath this one, so Back
// lands on the selected address. Then Home, and the HD wallet's
// address, which the later tests send from.
if (await env.page.isVisible("#view-confirm-tx")) {
await env.page.click("#btn-confirm-back");
}
if (await env.page.isVisible("#view-send")) {
await env.page.click("#btn-send-back");
}
await visible(env.page, "#view-address");
await env.page.click("#btn-address-back");
await visible(env.page, "#view-main");
await openAddressDetail(env.page);
}
});
// ------------------------------ the wait for a receipt ending in error (#315) // ------------------------------ the wait for a receipt ending in error (#315)
// //
// README.md (WaitTx) documents two ways the wait ends on the error screen: a // README.md (WaitTx) documents two ways the wait ends on the error screen: a
+94
View File
@@ -594,6 +594,34 @@ describe("any token left in the router is not received", () => {
), ),
]; ];
} }
// V4 SWAP_EXACT_IN of `amountIn` of `currencyIn` through one pool for
// each currency of `path` in turn, for at least `amountOutMin` of the
// last. An empty path swaps nothing.
function swapExactIn(currencyIn, path, amountIn, amountOutMin) {
return [
0x07,
coder.encode(
[
"tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)",
],
[
[
currencyIn,
path.map((c) => [c, 500, 10, ZERO, "0x"]),
amountIn,
amountOutMin,
],
],
),
];
}
// V4 SETTLE_ALL of `currency`, paid by the user.
function settleAll(currency, maxAmount) {
return [
0x0c,
coder.encode(["address", "uint256"], [currency, maxAmount]),
];
}
// The line giving a token left in the router. // The line giving a token left in the router.
const tokenLine = (token, symbol) => ({ const tokenLine = (token, symbol) => ({
label: "Token moved into the router", label: "Token moved into the router",
@@ -709,6 +737,60 @@ describe("any token left in the router is not received", () => {
), ),
left: tokenLine(WETH, "WETH"), left: tokenLine(WETH, "WETH"),
}, },
// The user pays for the swap by a SETTLE_ALL or SETTLE whose currency
// the decoder cannot read, so the SETTLE from the router pays nothing.
{
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 1 WETH with a SETTLE_ALL that cannot be read and a SETTLE of OPEN_DELTA from the router",
data: execute(
wrapEth(ADDRESS_THIS, ONE_WETH),
v4Swap(
swapExactInSingle(WETH, USDC, ONE_WETH, USDC_1990),
withDirtyWord(settleAll(WETH, ONE_WETH), 0),
settle(WETH, 0n, false),
takeAll(USDC),
),
),
left: tokenLine(WETH, "WETH"),
},
{
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 1 WETH with a SETTLE by the user that cannot be read and a SETTLE of OPEN_DELTA from the router",
data: execute(
wrapEth(ADDRESS_THIS, ONE_WETH),
v4Swap(
swapExactInSingle(WETH, USDC, ONE_WETH, USDC_1990),
withDirtyWord(settle(WETH, ONE_WETH, true), 0),
settle(WETH, 0n, false),
takeAll(USDC),
),
),
left: tokenLine(WETH, "WETH"),
},
{
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 step with an exact-in of 1 WETH less 1,000 wei and an empty path, a swap of 1,000 wei, and a SETTLE of OPEN_DELTA from the router",
data: execute(
wrapEth(ADDRESS_THIS, ONE_WETH),
v4Swap(
swapExactIn(WETH, [], ONE_WETH - 1000n, 0n),
swapExactInSingle(WETH, USDC, 1000n, USDC_1990),
settle(WETH, 0n, false),
takeAll(USDC),
),
),
left: tokenLine(WETH, "WETH"),
},
{
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of all of it settled from the router, with an action the decoder does not read",
data: execute(
wrapEth(ADDRESS_THIS, ONE_WETH),
v4Swap(
swapExactInSingle(WETH, USDC, ONE_WETH, USDC_1990),
settle(WETH, 0n, false),
takeAll(USDC),
[0x14, coder.encode(["address"], [WETH])],
),
),
left: tokenLine(WETH, "WETH"),
},
{ {
kind: "a swap paying WETH to the router, a TRANSFER of 1 wei of it to the WETH/USDC pair, then a V2 swap of what the pair holds", kind: "a swap paying WETH to the router, a TRANSFER of 1 wei of it to the WETH/USDC pair, then a V2 swap of what the pair holds",
data: execute( data: execute(
@@ -832,6 +914,18 @@ describe("any token left in the router is not received", () => {
), ),
min: "1990.0000 USDC", min: "1990.0000 USDC",
}, },
{
kind: "a WRAP_ETH into the router, then a V4 swap of all of it through two pools, settled from the router",
data: execute(
wrapEth(ADDRESS_THIS, ONE_WETH),
v4Swap(
swapExactIn(WETH, [USDT, USDC], ONE_WETH, USDC_1990),
settle(WETH, 0n, false),
takeAll(USDC),
),
),
min: "1990.0000 USDC",
},
{ {
kind: "a WRAP_ETH of all the router's ETH, then a swap of all its WETH", kind: "a WRAP_ETH of all the router's ETH, then a swap of all its WETH",
data: execute( data: execute(