Author SHA1 Message Date
sneak 427d99e0a0 fix: a permit on the zero address never reads as ETH (closes #660)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
Permit2 has no allowance of ETH, but the swap decoder read a permit's
zero-address token as ETH. Its Allowance token line read ETH. A permit of
1 wei of the zero address before a V4 step of 5 ETH also stood as the
Amount figure, which then read 0.000000000000000001 ETH.

Such a permit now never stands as the Amount figure. Its lines give the
amount in base units and name the zero address, as for a token nothing
knows.

Model: opus-5-5
2026-10-27 02:56:35 +00:00
clawbot 53551c6382 fix: the StateRecovery erase warning names every kind of wallet backup (closes #593)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
The StateRecovery erase warning told every user to rely on each wallet's recovery phrase. A wallet imported from a private key or an extended private key has none; its backup is that key. The warning now names all three, in the words the delete-wallet screens use, and README's StateRecovery entry says what a wallet can be added again from. A test reads the warning.

Model: opus-5-5
2026-10-27 03:46:17 +01:00
5 changed files with 279 additions and 519 deletions
+86 -89
View File
@@ -1265,9 +1265,7 @@ see Transaction Decoding), do not always read as a number. They can read:
the same words. `Unlimited`, `All available (V4 open delta)` and the same words. `Unlimited`, `All available (V4 open delta)` and
`Whatever an earlier step sent to the pair (V2 already paid)` keep their `Whatever an earlier step sent to the pair (V2 already paid)` keep their
wording. When a V2 or V3 exact-out step sets `Min. received`, that line shows wording. When a V2 or V3 exact-out step sets `Min. received`, that line shows
its `amountOut`, the exact amount it buys. When the router may hold ETH from its `amountOut`, the exact amount it buys.
an earlier step, the `Moved from the sender` line of ETH that the user pays a
V4 step also reads `Up to <amount>` (see Transaction Decoding).
- `All available (V4 open delta)`: the swap's `Amount` line, when the amount it - `All available (V4 open delta)`: the swap's `Amount` line, when the amount it
shows is a V4 exact-in `amountIn` of zero. V4 reads that zero as "use the shows is a V4 exact-in `amountIn` of zero. V4 reads that zero as "use the
whole open delta", so the calldata states no quantity. Transaction Decoding whole open delta", so the calldata states no quantity. Transaction Decoding
@@ -2822,6 +2820,9 @@ view would leave a wallet one click from deletion.
- "Export Saved Data" button, and the read-only text box it fills - "Export Saved Data" button, and the read-only text box it fills
- What erasing does and does not do, in bold: every wallet stored in this - What erasing does and does not do, in bold: every wallet stored in this
browser is deleted; nothing on chain changes and no money is moved browser is deleted; nothing on chain changes and no money is moved
- What a wallet can be added again from: a kept copy of its recovery phrase,
private key or extended private key. Without that copy or the exported
copy, everything the wallet holds is gone forever.
- A text input asking for `ERASE MY WALLET` to be typed back - A text input asking for `ERASE MY WALLET` to be typed back
- Error line - Error line
- "Erase Saved Data" button - "Erase Saved Data" button
@@ -3479,48 +3480,46 @@ Currently supported:
of any amount but `OPEN_DELTA` pays that amount, and a `SETTLE` of of any amount but `OPEN_DELTA` pays that amount, and a `SETTLE` of
`OPEN_DELTA` or a `SETTLE_ALL` pays what the step owes, as above. So a V4 step `OPEN_DELTA` or a `SETTLE_ALL` pays what the step owes, as above. So a V4 step
that swaps 400 USDT, with a `SETTLE` of 5,000 USDT paid by the user and the that swaps 400 USDT, with a `SETTLE` of 5,000 USDT paid by the user and the
rest taken to another address, reads `5000.0000 USDT`. V4 pays a `SETTLE` of rest taken to another address, reads `5000.0000 USDT`. A later step paid from
ETH from the router's own ETH, whichever payer it names. So when the router the router spends what the router holds and takes nothing, and so does a V2
may hold ETH from an earlier step (see above), the ETH that the user pays a V4 exact-in whose `amountIn` is `ALREADY_PAID`. A later take of `Token In` is
step gets lines of its own (see below). A later step paid from the router added to the total while both are quantities: a V3 exact-in of 600 USDT and a
spends what the router holds and takes nothing, and so does a V2 exact-in V2 exact-in of 400 USDT, both paid by the user, read `1000.0000 USDT`. When
whose `amountIn` is `ALREADY_PAID`. A later take of `Token In` is added to the the decoder does not know a take's amount, the total is not known and there is
total while both are quantities: a V3 exact-in of 600 USDT and a V2 exact-in no `Amount` line. A `PERMIT2_PERMIT` names `Token In` only when its amount,
of 400 USDT, both paid by the user, read `1000.0000 USDT`. When the decoder `Unlimited` included, stands as the `Amount` figure in place of the total,
does not know a take's amount, the total is not known and there is no `Amount` which it does only in the plain shape of Uniswap's own routes: it comes before
line. A `PERMIT2_PERMIT` names `Token In` only when its amount, `Unlimited` the step that sets the input side, it is the only `PERMIT2_PERMIT` in the
included, stands as the `Amount` figure in place of the total, which it does transaction and there is no `PERMIT2_PERMIT_BATCH`, its spender is the router,
only in the plain shape of Uniswap's own routes: it comes before the step that its token is not the zero address, the router may not let it fail, no step
sets the input side, it is the only `PERMIT2_PERMIT` in the transaction and before it takes its token, a `PERMIT2_TRANSFER_FROM` of it included, or a
there is no `PERMIT2_PERMIT_BATCH`, its spender is the router, the router may token the decoder does not know, and the decoder reads every step after it in
not let it fail, no step before it takes its token, a `PERMIT2_TRANSFER_FROM` full. Permit2 then lets the router take no more of the token than that amount,
of it included, or a token the decoder does not know, and the decoder reads so the permit covers every take of it. Permit2 has no allowance of ETH, so a
every step after it in full. Permit2 then lets the router take no more of the permit on the zero address covers no take of ETH. A step before the permit
token than that amount, so the permit covers every take of it. A step before takes under whatever allowance the router already had, so it may take more
the permit takes under whatever allowance the router already had, so it may than the permit's amount, which would then read lower than what leaves the
take more than the permit's amount, which would then read lower than what user's wallet: a `PERMIT2_TRANSFER_FROM` of 2,000 USDT into the router, then a
leaves the user's wallet: a `PERMIT2_TRANSFER_FROM` of 2,000 USDT into the permit of 600 USDT to the router and a V3 exact-in of 2,000 USDT paid from the
router, then a permit of 600 USDT to the router and a V3 exact-in of 2,000 router, read `Amount 2000.0000 USDT`. A later permit or batch may raise what
USDT paid from the router, read `Amount 2000.0000 USDT`. A later permit or Permit2 lets the router take, and so may a step the decoder does not read in
batch may raise what Permit2 lets the router take, and so may a step the full: a command the decoder does not read (listed at the end of this section),
decoder does not read in full: a command the decoder does not read (listed at a step whose input it cannot read, in whole or in part, and a step it fails
the end of this section), a step whose input it cannot read, in whole or in on. Every other `PERMIT2_PERMIT`, before the step that sets the input side or
part, and a step it fails on. Every other `PERMIT2_PERMIT`, before the step after it, leaves `Token In` and the `Amount` line as they read with no permit,
that sets the input side or after it, leaves `Token In` and the `Amount` line unless it lets a spender other than the router take `Token In` (see below): a
as they read with no permit, unless it lets a spender other than the router permit of 5,000 DAI to another address and then a V3 exact-in of 600 USDT read
take `Token In` (see below): a permit of 5,000 DAI to another address and then `Swap USDT → USDC` with `Amount 600.0000 USDT`, and so do that exact-in and
a V3 exact-in of 600 USDT read `Swap USDT → USDC` with `Amount 600.0000 USDT`, then a permit of 5,000 DAI to the router. When a permit's amount stands and
and so do that exact-in and then a permit of 5,000 DAI to the router. When a the step that sets the input side spends a token other than the permit's, or
permit's amount stands and the step that sets the input side spends a token one the decoder does not know, that step's amount gets lines of its own,
other than the permit's, or one the decoder does not know, that step's amount unless it states no amount and the user pays it none of its input token. Each
gets lines of its own, unless it states no amount and the user pays it none of take the total does not count gives three lines after the `Amount` line (after
its input token. Each take the total does not count gives three lines after `Token In` when there is none): one of a token other than `Token In`; one of
the `Amount` line (after `Token In` when there is none): one of a token other `Token In` after an amount that is not a quantity (a V4 open delta,
than `Token In`; one of `Token In` after an amount that is not a quantity (a `ALREADY_PAID`, which keep their wording on the `Amount` line, or no stated
V4 open delta, `ALREADY_PAID`, which keep their wording on the `Amount` line, amount, as of a V4 exact-out step); and one whose amount the decoder does not
or no stated amount, as of a V4 exact-out step); one whose amount the decoder know. When there is no `Amount` line, each take the total counts gets them
does not know; and one of ETH that the user pays a V4 step when the router may
hold ETH. When there is no `Amount` line, each take the total counts gets them
too, so that every amount the decoder reads is shown: the amount the step that too, so that every amount the decoder reads is shown: the amount the step that
sets the input side states, when it is a quantity and the user pays that step sets the input side states, when it is a quantity and the user pays that step
(a V2 or V3 step whose `payerIsUser` is true, a V4 step the user pays some of (a V2 or V3 step whose `payerIsUser` is true, a V4 step the user pays some of
@@ -3542,26 +3541,22 @@ Currently supported:
its amount, worded as the `Amount` line words one, or its amount, worded as the `Amount` line words one, or
`Unknown (could not be decoded)` when the decoder does not know it; `Unknown (could not be decoded)` when the decoder does not know it;
`Token moved`, giving the token's address as the `Token In` line does; and `Token moved`, giving the token's address as the `Token In` line does; and
`Moved to`, which reads `The swap step that spends it`. For ETH that the user `Moved to`, which reads `The swap step that spends it`. A token the decoder
pays a V4 step when the router may hold ETH, `Moved from the sender` reads does not know gives the `Moved from the sender` line alone. A step may also
`Up to <amount>` for a known amount. For a `WRAP_ETH` that pays its WETH to take from the user's wallet what the decoder did not read: a V4 step whose
anyone but the router, `Moved to` names that recipient instead: `The sender` input or one of whose `SETTLE`s or `SETTLE_ALL`s it cannot read, which the
for `address(1)`, and otherwise the address. A token the decoder does not know router still runs; a V2 or V3 exact-in or exact-out step or a
gives the `Moved from the sender` line alone. A step may also take from the `PERMIT2_TRANSFER_FROM` whose input it cannot read, such as a V3 step whose
user's wallet what the decoder did not read: a V4 step whose input or one of path is shorter than one token address or has bytes after its last hop; every
whose `SETTLE`s or `SETTLE_ALL`s it cannot read, which the router still runs; `PERMIT2_TRANSFER_FROM_BATCH`; every `EXECUTE_SUB_PLAN`, wherever it stands;
a V2 or V3 exact-in or exact-out step or a `PERMIT2_TRANSFER_FROM` whose input and every step it fails on. Each such step gives one `Moved from the sender`
it cannot read, such as a V3 step whose path is shorter than one token address line among those, reading `Unknown (could not be decoded)`. That line, or any
or has bytes after its last hop; every `PERMIT2_TRANSFER_FROM_BATCH`; every line for a token the decoder does not know, leaves no `Amount` line, whose
`EXECUTE_SUB_PLAN`, wherever it stands; and every step it fails on. Each such figure might be too low, and such a step before a `PERMIT2_PERMIT` keeps the
step gives one `Moved from the sender` line among those, reading permit's amount from standing. Then each `PERMIT2_TRANSFER_FROM` but one into
`Unknown (could not be decoded)`. That line, or any line for a token the the router, in step order, gives three lines: `Moved from the sender`, the
decoder does not know, leaves no `Amount` line, whose figure might be too low, amount it moves, for example `1000000.0000 USDT`; `Token moved`, giving the
and such a step before a `PERMIT2_PERMIT` keeps the permit's amount from token's address as the `Token In` line does; and `Moved to`, which reads
standing. Then each `PERMIT2_TRANSFER_FROM` but one into the router, in step
order, gives three lines: `Moved from the sender`, the amount it moves, for
example `1000000.0000 USDT`; `Token moved`, giving the token's address as the
`Token In` line does; and `Moved to`, which reads
`The V2 pair the next swap spends` for a hop and `The sender` for `address(1)` `The V2 pair the next swap spends` for a hop and `The sender` for `address(1)`
on every router but `UniversalRouterV1`, and otherwise gives the address. A on every router but `UniversalRouterV1`, and otherwise gives the address. A
move into the router needs none of them, because its `Paid to` line and move into the router needs none of them, because its `Paid to` line and
@@ -3576,30 +3571,32 @@ Currently supported:
amount stands gives none: the `Amount` line gives its amount, `Token In` its amount stands gives none: the `Amount` line gives its amount, `Token In` its
token, and its spender is the router. Each allowance a `PERMIT2_PERMIT_BATCH` token, and its spender is the router. Each allowance a `PERMIT2_PERMIT_BATCH`
grants, one for each token it names, all for its one spender, gives the same grants, one for each token it names, all for its one spender, gives the same
three lines, in step order with those of each `PERMIT2_PERMIT`. A three lines, in step order with those of each `PERMIT2_PERMIT`. For an
`PERMIT2_PERMIT` or `PERMIT2_PERMIT_BATCH` whose input the decoder cannot allowance of the zero address, `Permit2 allowance` gives the amount in base
read, such as one whose token or spender word has a nonzero top byte, may let units and `Allowance token` gives the zero address. A `PERMIT2_PERMIT` or
any spender take any amount of any token: it gives one `Permit2 allowance` `PERMIT2_PERMIT_BATCH` whose input the decoder cannot read, such as one whose
line alone, reading `Unknown (could not be decoded)`. These lines change no token or spender word has a nonzero top byte, may let any spender take any
`Paid to` line: a permit or batch pays nobody. But code that a step runs, such amount of any token: it gives one `Permit2 allowance` line alone, reading
as a V4 pool's hook or a token's own code, can call a spender other than the `Unknown (could not be decoded)`. These lines change no `Paid to` line: a
router, which can then take up to its allowance from the user's wallet before permit or batch pays nobody. But code that a step runs, such as a V4 pool's
the transaction ends. So an allowance to any spender but the router, or a hook or a token's own code, can call a spender other than the router, which
permit or batch the decoder cannot read, makes the minimum read can then take up to its allowance from the user's wallet before the
`Min. paid out`, and when its token is `Token In`, or the decoder does not transaction ends. So an allowance to any spender but the router, or a permit
know its token or `Token In`, it leaves no `Amount` line, whose figure might or batch the decoder cannot read, makes the minimum read `Min. paid out`, and
be too low: a permit of 5,000 USDT to another address and then a V3 exact-in when its token is `Token In`, or the decoder does not know its token or
of 600 USDT read with no `Amount` line, and the 600 USDT gets lines of its `Token In`, it leaves no `Amount` line, whose figure might be too low: a
own. A step goes unread when its recipient does not decode as an address, when permit of 5,000 USDT to another address and then a V3 exact-in of 600 USDT
it is a V3 swap step whose path is shorter than one token address or has bytes read with no `Amount` line, and the 600 USDT gets lines of its own. A step
after its last hop, or when it is a command the decoder does not read: every goes unread when its recipient does not decode as an address, when it is a V3
command other than `PERMIT2_PERMIT`, `PERMIT2_PERMIT_BATCH`, swap step whose path is shorter than one token address or has bytes after its
`PERMIT2_TRANSFER_FROM`, `V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`, last hop, or when it is a command the decoder does not read: every command
`V3_SWAP_EXACT_IN`, `V3_SWAP_EXACT_OUT`, `V4_SWAP`, `SWEEP`, `TRANSFER`, other than `PERMIT2_PERMIT`, `PERMIT2_PERMIT_BATCH`, `PERMIT2_TRANSFER_FROM`,
`PAY_PORTION`, `PAY_PORTION_FULL_PRECISION`, `ACROSS_V4_DEPOSIT_V3`, `V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`, `V3_SWAP_EXACT_IN`,
`WRAP_ETH`, `UNWRAP_WETH` and `BALANCE_CHECK_ERC20` `V3_SWAP_EXACT_OUT`, `V4_SWAP`, `SWEEP`, `TRANSFER`, `PAY_PORTION`,
(`PERMIT2_TRANSFER_FROM_BATCH`, `EXECUTE_SUB_PLAN` and the NFT commands among `PAY_PORTION_FULL_PRECISION`, `ACROSS_V4_DEPOSIT_V3`, `WRAP_ETH`,
them), or a type its release names no command for. `UNWRAP_WETH` and `BALANCE_CHECK_ERC20` (`PERMIT2_TRANSFER_FROM_BATCH`,
`EXECUTE_SUB_PLAN` and the NFT commands among them), or a type its release
names no command for.
### Non-Goals Forever ### Non-Goals Forever
+5 -4
View File
@@ -1994,10 +1994,11 @@
>Erasing the saved data deletes every wallet stored in >Erasing the saved data deletes every wallet stored in
this browser.</strong this browser.</strong
> >
Nothing on the blockchain changes and no money is moved, but Nothing on the blockchain changes and no money is moved. If
without the exported copy above, or the recovery phrase for you kept a copy of a wallet's recovery phrase, private key
each wallet written down, everything they hold is gone or extended private key, you can add that wallet again.
forever. Without that copy or the exported copy above, everything the
wallet holds is gone forever.
</p> </p>
<p class="text-xs mb-1"> <p class="text-xs mb-1">
To confirm, type To confirm, type
+67 -94
View File
@@ -1244,10 +1244,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// comes from the transaction's value. A step paid from the router // comes from the transaction's value. A step paid from the router
// takes nothing from the sender. // takes nothing from the sender.
let inputPaidByUser = false; let inputPaidByUser = false;
// The recipient of a WRAP_ETH that set the input side, as written,
// when it is not the router; otherwise null. That WRAP_ETH pays its
// WETH there, not to a swap step (see the Moved to lines).
let wrapEthRecipient = 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;
@@ -1340,18 +1336,15 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
}; };
// 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
// that sets the input side, as { token, amount, atMost }, in step // that sets the input side, as { token, amount }, in step order.
// order. After the loop each joins the Amount line or gets lines of // After the loop each joins the Amount line or gets lines of its own
// its own (see permitStands). A take with `atMost` set takes at most // (see permitStands).
// its amount, and always gets lines of its own.
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. A V2 exact-in of ALREADY_PAID takes
// V2 exact-in of ALREADY_PAID takes nothing. // nothing.
const addUserPaid = (token, amount, atMost = false) => { const addUserPaid = (token, amount) => {
if (amount !== ALREADY_PAID) { if (amount !== ALREADY_PAID) userPaid.push({ token, amount });
userPaid.push({ token, amount, atMost });
}
}; };
// A step may take from the sender what this decoder did not read: a // A step may take from the sender what this decoder did not read: a
// V4 step whose input or one of whose SETTLEs or SETTLE_ALLs it // V4 step whose input or one of whose SETTLEs or SETTLE_ALLs it
@@ -1794,9 +1787,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
w.amount, w.amount,
); );
inputPaidByUser = true; inputPaidByUser = true;
if (!isRouter(w.recipient)) {
wrapEthRecipient = w.recipient;
}
} }
addPayee(w.recipient); addPayee(w.recipient);
// CONTRACT_BALANCE wraps all the router's ETH. // CONTRACT_BALANCE wraps all the router's ETH.
@@ -1825,14 +1815,6 @@ 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]);
const inputSetBefore = inputEstablished; const inputSetBefore = inputEstablished;
// V4Router pays a SETTLE of ETH from the router's own
// ETH, whichever payer it names. When the router may hold
// ETH from an earlier step, that ETH may pay some of the
// ETH that the user pays this step. The sender then pays
// at most the step's amount.
const routerMayHoldEth = heldByRouter.has(
"0x0000000000000000000000000000000000000000",
);
if (v4) { if (v4) {
// 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.
@@ -1923,13 +1905,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
if (v4) { if (v4) {
for (const p of v4.paidByUser) { for (const p of v4.paidByUser) {
if (setInputHere && p === paidIn) continue; if (setInputHere && p === paidIn) continue;
addUserPaid( addUserPaid(p.currency, p.amount);
p.currency,
p.amount,
routerMayHoldEth &&
p.currency ===
"0x0000000000000000000000000000000000000000",
);
} }
} }
if (!v4 || v4.settleUnread) { if (!v4 || v4.settleUnread) {
@@ -2040,22 +2016,23 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// being of another token or coming after an amount that is not a // being of another token or coming after an amount that is not a
// quantity (a V4 open delta, ALREADY_PAID, or none stated), gets // quantity (a V4 open delta, ALREADY_PAID, or none stated), gets
// lines of its own. One whose amount is not known also leaves the // lines of its own. One whose amount is not known also leaves the
// total unknown, with no figure. A take of at most its amount (see // total unknown, with no figure.
// userPaid) is never added, and gets lines of its own too.
// //
// A PERMIT2_PERMIT names Token In only when its amount stands as the // A PERMIT2_PERMIT names Token In only when its amount stands as the
// Amount figure in place of the total, which it does only in the // Amount figure in place of the total, which it does only in the
// plain shape of Uniswap's own routes: it comes before the step that // plain shape of Uniswap's own routes: it comes before the step that
// set the input side, it is the only PERMIT2_PERMIT in the // set the input side, it is the only PERMIT2_PERMIT in the
// transaction and there is no PERMIT2_PERMIT_BATCH, its spender is // transaction and there is no PERMIT2_PERMIT_BATCH, its spender is
// the router, the router may not let it fail, no step before it took // the router, its token is not the zero address, the router may not
// its token or one not known (a PERMIT2_TRANSFER_FROM of its token // let it fail, no step before it took its token or one not known (a
// included), and this decoder read every step after it in full (see // PERMIT2_TRANSFER_FROM of its token included), and this decoder read
// stepsUnread). Permit2 then lets the router take no more of the token // every step after it in full (see stepsUnread). Permit2 then lets the
// than that amount, so the permit covers every take of it. A step // router take no more of the token than that amount, so the permit
// before it takes under whatever allowance the router already had, so // covers every take of it. Permit2 has no allowance of ETH, so a
// it may take more. A later permit or batch, or a later step this // permit on the zero address covers no take of ETH. A step before it
// decoder did not read, may raise what Permit2 lets the router take. // takes under whatever allowance the router already had, so it may
// take more. A later permit or batch, or a later step this decoder
// did not read, may raise what Permit2 lets the router take.
// Every other PERMIT2_PERMIT leaves Token In and the Amount line as // Every other PERMIT2_PERMIT leaves Token In and the Amount line as
// they read with no permit, and gets lines of its own below. A step // they read with no permit, and gets lines of its own below. A step
// that may take from the sender what this decoder did not read leaves // that may take from the sender what this decoder did not read leaves
@@ -2072,21 +2049,19 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
stepCommands.filter((c) => c === "PERMIT2_PERMIT").length === 1 && stepCommands.filter((c) => c === "PERMIT2_PERMIT").length === 1 &&
!stepCommands.includes("PERMIT2_PERMIT_BATCH") && !stepCommands.includes("PERMIT2_PERMIT_BATCH") &&
sameAddress(permit.spender, toAddress) && sameAddress(permit.spender, toAddress) &&
permit.token !== "0x0000000000000000000000000000000000000000" &&
!permit.allowedToFail && !permit.allowedToFail &&
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 }. A null token is a step this decoder did not read
// decoder did not read (see spentUnread()), or a token it does not // (see spentUnread()), or a token it does not know.
// know. `atMost` is as in `userPaid`. Only the take of a WRAP_ETH that
// pays elsewhere has a recipient (see wrapEthRecipient).
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, as
// the shape of `spentFromSender`. When there is no Amount line, each // { token, amount }. When there is no Amount line, each gets lines of
// gets lines of its own (see below). The amount of a step that set // its own (see below). The amount of a step that set the input side
// the input side and is paid from the router counts in `total` but is // and is paid from the router counts in `total` but is not among them.
// not among them.
const inTotal = []; const inTotal = [];
// Whether the step that set the input side counts as a take from the // Whether the step that set the input side counts as a take from the
// sender, which may get lines of its own. The amount it stated counts // sender, which may get lines of its own. The amount it stated counts
@@ -2101,11 +2076,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
if (!permitStands || sameAddress(inputToken, permit.token)) { if (!permitStands || sameAddress(inputToken, permit.token)) {
total = inputAmount; total = inputAmount;
if (typeof inputAmount === "bigint" && inputPaidByUser) { if (typeof inputAmount === "bigint" && inputPaidByUser) {
inTotal.push({ inTotal.push({ token: inputToken, amount: inputAmount });
token: inputToken,
amount: inputAmount,
recipient: wrapEthRecipient,
});
} }
if (hasFirstTake && !present(inputAmount)) { if (hasFirstTake && !present(inputAmount)) {
firstTakeUnstated = { token: inputToken, amount: null }; firstTakeUnstated = { token: inputToken, amount: null };
@@ -2113,11 +2084,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} else if (hasFirstTake) { } else if (hasFirstTake) {
// The step that set the input side spent a token other than the // The step that set the input side spent a token other than the
// permit's, or one not known, so its amount gets lines of its own. // permit's, or one not known, so its amount gets lines of its own.
spentFromSender.push({ spentFromSender.push({ token: inputToken, amount: inputAmount });
token: inputToken,
amount: inputAmount,
recipient: wrapEthRecipient,
});
} }
if (permitStands) inputToken = permit.token; if (permitStands) inputToken = permit.token;
for (const t of userPaid) { for (const t of userPaid) {
@@ -2125,7 +2092,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
if (ofTokenIn && permitStands) continue; if (ofTokenIn && permitStands) continue;
if ( if (
ofTokenIn && ofTokenIn &&
!t.atMost &&
typeof total === "bigint" && typeof total === "bigint" &&
typeof t.amount === "bigint" typeof t.amount === "bigint"
) { ) {
@@ -2230,9 +2196,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// V2's already-paid zero, neither of which is a quantity at all (see // V2's already-paid zero, neither of which is a quantity at all (see
// OPEN_DELTA and ALREADY_PAID), and an unbounded permit. Those two // OPEN_DELTA and ALREADY_PAID), and an unbounded permit. Those two
// tests come first — the sentinels are not bigints and cannot be // tests come first — the sentinels are not bigints and cannot be
// compared with one. `atMost` says the amount is a maximum (see // compared with one.
// userPaid). const spentText = (amount, info) => {
const spentText = (amount, info, atMost = false) => {
if (amount === OPEN_DELTA) { if (amount === OPEN_DELTA) {
return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT }; return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
} }
@@ -2245,7 +2210,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
if (amount >= MAX_UINT160) { if (amount >= MAX_UINT160) {
return { raw: "Unlimited", display: "Unlimited" }; return { raw: "Unlimited", display: "Unlimited" };
} }
if (hasV2OrV3ExactOut || atMost) { if (hasV2OrV3ExactOut) {
// A V2 or V3 exact-out swap spends at most this figure, // A V2 or V3 exact-out swap spends at most this figure,
// whichever steps gave it (an amountInMax, the WRAP_ETH of a // whichever steps gave it (an amountInMax, the WRAP_ETH of a
// swap paid in ETH, a permit, or several steps' total), so it // swap paid in ETH, a permit, or several steps' total), so it
@@ -2298,33 +2263,20 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// What steps take from the sender beyond the Amount line, in the shape // What steps take from the sender beyond the Amount line, in the shape
// 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.
// that spends it. 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",
value: present(s.amount) value: present(s.amount)
? spentText(s.amount, tokenInfo(s.token, sources), s.atMost) ? spentText(s.amount, tokenInfo(s.token, sources)).display
.display
: UNDECODED_CURRENCY, : UNDECODED_CURRENCY,
}); });
if (present(s.token)) { if (present(s.token)) {
details.push(tokenMovedLine("Token moved", s.token, sources)); details.push(tokenMovedLine("Token moved", s.token, sources));
if (!present(s.recipient)) { details.push({
details.push({ label: "Moved to",
label: "Moved to", value: "The swap step that spends it",
value: "The swap step that spends it", });
});
} else if (s.recipient.toLowerCase() === MSG_SENDER) {
details.push({ label: "Moved to", value: "The sender" });
} else {
details.push({
label: "Moved to",
value: s.recipient,
address: s.recipient,
});
}
} }
} }
@@ -2366,15 +2318,36 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
}); });
continue; continue;
} }
details.push({ // Permit2 has no allowance of ETH. An allowance of the zero
label: "Permit2 allowance", // address reads as one of a token nothing knows: its amount in
value: // base units, and the zero address as its token.
p.amount >= MAX_UINT160 if (p.token === "0x0000000000000000000000000000000000000000") {
? "Unlimited" details.push({
: amountText(p.amount, tokenInfo(p.token, sources)) label: "Permit2 allowance",
.display, value:
}); p.amount >= MAX_UINT160
details.push(tokenMovedLine("Allowance token", p.token, sources)); ? "Unlimited"
: unknownDecimalsAmount(p.amount),
});
details.push({
label: "Allowance token",
value: p.token,
address: p.token,
isToken: true,
});
} else {
details.push({
label: "Permit2 allowance",
value:
p.amount >= MAX_UINT160
? "Unlimited"
: amountText(p.amount, tokenInfo(p.token, sources))
.display,
});
details.push(
tokenMovedLine("Allowance token", p.token, sources),
);
}
details.push({ details.push({
label: "Allowance spender", label: "Allowance spender",
value: p.spender, value: p.spender,
+26
View File
@@ -23,6 +23,9 @@
// version field, and showing those users a wipe prompt would be a worse defect // version field, and showing those users a wipe prompt would be a worse defect
// than the one being fixed. It is migrated in place and keeps working. // than the one being fixed. It is migrated in place and keeps working.
const fs = require("fs");
const path = require("path");
const { const {
bootPopup, bootPopup,
cleanupPopup, cleanupPopup,
@@ -148,6 +151,29 @@ describe("the destructive reset on the recovery screen", () => {
}); });
}); });
// A key or xprv wallet has no recovery phrase, so the erase warning names all
// three backups (https://git.eeqj.de/sneak/AutistMask/issues/593). The boot
// harness holds no static text, so this reads the markup itself.
describe("the erase warning on the recovery screen", () => {
test("names the recovery phrase, the private key and the extended private key", () => {
const html = fs.readFileSync(
path.join(__dirname, "..", "src", "popup", "index.html"),
"utf8",
);
const start = html.indexOf('id="view-state-recovery"');
const end = html.indexOf('id="btn-state-recovery-reset"', start);
const screenText = html
.slice(start, end)
.replace(/<[^>]*>/g, " ")
.replace(/\s+/g, " ");
expect(screenText).toContain(
"If you kept a copy of a wallet's recovery phrase, private key or " +
"extended private key, you can add that wallet again.",
);
});
});
describe("a popup already open when the stored profile becomes unreadable", () => { describe("a popup already open when the stored profile becomes unreadable", () => {
// https://git.eeqj.de/sneak/AutistMask/issues/373. The popup used to stay // https://git.eeqj.de/sneak/AutistMask/issues/373. The popup used to stay
// on the wallet list with the last good balances, and only a reopen // on the wallet list with the last good balances, and only a reopen
+95 -332
View File
@@ -1428,8 +1428,8 @@ describe("uniswap decoder", () => {
]; ];
// execute() of these command bytes and inputs, decoded as sent by // execute() of these command bytes and inputs, decoded as sent by
// the user to `router`. // the user.
function decodeSteps(commands, inputs, router = ROUTER_ADDR) { function decodeSteps(commands, inputs) {
const data = buildExecute( const data = buildExecute(
solidityPacked( solidityPacked(
commands.map(() => "uint8"), commands.map(() => "uint8"),
@@ -1438,7 +1438,7 @@ describe("uniswap decoder", () => {
inputs, inputs,
9999999999n, 9999999999n,
); );
return uniswap.decode(data, router, "0x1", {}, USER_ADDR); return uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
} }
// The first step: a V3 exact-in of 600 USDT for USDC, paid by the // The first step: a V3 exact-in of 600 USDT for USDC, paid by the
@@ -2424,335 +2424,6 @@ describe("uniswap decoder", () => {
expect(linesBetweenTokens(result)).toEqual(lines); expect(linesBetweenTokens(result)).toEqual(lines);
}); });
// https://git.eeqj.de/sneak/AutistMask/issues/665 — a WRAP_ETH that
// sets the input side and pays its WETH to anyone but the router
// names that recipient on its Moved to line. On next each row read
// The swap step that spends it.
const ONE_ETH = 1000000000000000000n;
const MSG_SENDER = "0x0000000000000000000000000000000000000001";
// The lines of a WRAP_ETH of 1 ETH whose Moved to line reads `to`.
const wrapLines = (to) => [
["Moved from the sender", "1.0000 ETH"],
["Token moved", "ETH"],
["Moved to", to],
];
test.each([
{
kind: "a WRAP_ETH of 1 ETH to another address, then an EXECUTE_SUB_PLAN",
commands: [0x0b, 0x21],
inputs: [encodeWrapEth(OTHER_ADDR, ONE_ETH), SUB_PLAN],
lines: [
...wrapLines(OTHER_ADDR),
["Moved from the sender", UNDECODED],
],
},
{
kind: "a WRAP_ETH of 1 ETH to address(1), then an EXECUTE_SUB_PLAN",
commands: [0x0b, 0x21],
inputs: [encodeWrapEth(MSG_SENDER, ONE_ETH), SUB_PLAN],
lines: [
...wrapLines("The sender"),
["Moved from the sender", UNDECODED],
],
},
{
kind: "a PERMIT2_PERMIT of 5,000 USDT to the router, a WRAP_ETH of 1 ETH to another address, then a V3 exact-in of 600 USDT",
commands: [0x0a, 0x0b, 0x00],
inputs: [
encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR),
encodeWrapEth(OTHER_ADDR, ONE_ETH),
V3_600,
],
lines: [["Amount", "5000.0000 USDT"], ...wrapLines(OTHER_ADDR)],
},
])("$kind", ({ commands, inputs, lines }) => {
const result = decodeSteps(commands, inputs);
expect(linesBetweenTokens(result)).toEqual(lines);
});
// https://git.eeqj.de/sneak/AutistMask/issues/665 — V4Router pays a
// SETTLE of ETH from the router's own ETH, whichever payer it names.
// 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.
// Every row reads at least the ETH that can leave the wallet. The two
// rows marked "reads as on next" read the same on next. On 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.
const ADDRESS_THIS = "0x0000000000000000000000000000000000000002";
const WRAP_INTO_ROUTER = encodeWrapEth(ADDRESS_THIS, ONE_ETH);
// UNWRAP_WETH has the layout of WRAP_ETH.
const UNWRAP_INTO_ROUTER = encodeWrapEth(ADDRESS_THIS, 0n);
// A V4 step swapping `amount` of ETH for USDC, with a SETTLE of
// OPEN_DELTA by the user.
const v4EthForUsdc = (amount) =>
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_SETTLE_ID,
V4_TAKE_ALL_ID,
]),
[
encodeV4ExactInSingle(ZERO_ADDR, USDC_ADDR, amount, 1n),
settle(ZERO_ADDR, 0n, true),
encodeV4TakeAll(USDC_ADDR, 0n),
],
);
const V4_ONE_ETH = v4EthForUsdc(ONE_ETH);
// A TRANSFER or PAY_PORTION of `amount` of `token` to another
// address.
const payOut = (token, amount) =>
coder.encode(
["address", "address", "uint256"],
[token, OTHER_ADDR, amount],
);
// An ACROSS_V4_DEPOSIT_V3 of `amount` of ETH (`useNative` set), from
// the sender to the sender on chain id 8453.
const acrossEth = (amount) =>
coder.encode(
[
"tuple(address,address,address,address,uint256,uint256,uint256,address,uint32,uint32,uint32,bytes,bool)",
],
[
[
USER_ADDR,
USER_ADDR,
WETH_ADDR,
WETH_ADDR,
amount,
1n,
8453n,
ZERO_ADDR,
0,
0,
0,
"0x",
true,
],
],
);
// The lines of ETH the sender pays a V4 step, of `amount`.
const ethLines = (amount) => [
["Moved from the sender", amount],
["Token moved", "ETH"],
["Moved to", "The swap step that spends it"],
];
test.each([
{
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 1 ETH that the user settles, reads as on next",
commands: [0x0b, 0x10],
inputs: [WRAP_INTO_ROUTER, V4_ONE_ETH],
lines: [["Amount", "2.0000 ETH"]],
},
{
kind: "a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles",
commands: [0x0b, 0x0c, 0x10],
inputs: [WRAP_INTO_ROUTER, UNWRAP_INTO_ROUTER, V4_ONE_ETH],
lines: [
["Amount", "1.0000 ETH"],
...ethLines("Up to 1.0000 ETH"),
],
},
{
kind: "the same with a TRANSFER of 1 ETH to another address after the UNWRAP_WETH",
commands: [0x0b, 0x0c, 0x05, 0x10],
inputs: [
WRAP_INTO_ROUTER,
UNWRAP_INTO_ROUTER,
payOut(ZERO_ADDR, ONE_ETH),
V4_ONE_ETH,
],
lines: [
["Amount", "1.0000 ETH"],
...ethLines("Up to 1.0000 ETH"),
],
},
{
kind: "the same with a PAY_PORTION of 5,000 bips of WETH to another address before the UNWRAP_WETH",
commands: [0x0b, 0x06, 0x0c, 0x10],
inputs: [
WRAP_INTO_ROUTER,
payOut(WETH_ADDR, 5000n),
UNWRAP_INTO_ROUTER,
V4_ONE_ETH,
],
lines: [
["Amount", "1.0000 ETH"],
...ethLines("Up to 1.0000 ETH"),
],
},
{
kind: "the same with a WRAP_ETH of 1 ETH to another address after the UNWRAP_WETH",
commands: [0x0b, 0x0c, 0x0b, 0x10],
inputs: [
WRAP_INTO_ROUTER,
UNWRAP_INTO_ROUTER,
encodeWrapEth(OTHER_ADDR, ONE_ETH),
V4_ONE_ETH,
],
lines: [
["Amount", "1.0000 ETH"],
...ethLines("Up to 1.0000 ETH"),
],
},
{
kind: "the same on UniversalRouterV2_1_2 with an ACROSS_V4_DEPOSIT_V3 of 1 ETH after the UNWRAP_WETH",
router: ROUTER_V2_1_2_ADDR,
commands: [0x0b, 0x0c, 0x40, 0x10],
inputs: [
WRAP_INTO_ROUTER,
UNWRAP_INTO_ROUTER,
acrossEth(ONE_ETH),
V4_ONE_ETH,
],
lines: [
["Amount", "1.0000 ETH"],
...ethLines("Up to 1.0000 ETH"),
],
},
{
kind: "a V3 exact-in of 600 USDT that the user pays, a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles",
commands: [0x00, 0x0b, 0x0c, 0x10],
inputs: [
V3_600,
WRAP_INTO_ROUTER,
UNWRAP_INTO_ROUTER,
V4_ONE_ETH,
],
lines: [
["Amount", "600.0000 USDT"],
...ethLines("Up to 1.0000 ETH"),
],
},
{
kind: "a V3 exact-in of 600 USDT for WETH paid to the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles",
commands: [0x00, 0x0c, 0x10],
inputs: [
encodeV3SwapExactIn(ADDRESS_THIS, USDT_600, 1n, [
USDT_ADDR,
WETH_ADDR,
]),
UNWRAP_INTO_ROUTER,
V4_ONE_ETH,
],
lines: [
["Amount", "600.0000 USDT"],
...ethLines("Up to 1.0000 ETH"),
],
},
{
kind: "a V4 swap of 600 USDT that takes ETH into the router, then a V4 swap of 5 ETH that the user settles",
commands: [0x10, 0x10],
inputs: [
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_SETTLE_ID,
V4_TAKE_ID,
]),
[
encodeV4ExactInSingle(
USDT_ADDR,
ZERO_ADDR,
USDT_600,
1n,
),
settle(USDT_ADDR, 0n, true),
take(ZERO_ADDR, ADDRESS_THIS, 0n),
],
),
v4EthForUsdc(5n * ONE_ETH),
],
lines: [
["Amount", "600.0000 USDT"],
...ethLines("Up to 5.0000 ETH"),
],
},
{
kind: "a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 exact-out paid in ETH with a SETTLE_ALL of at most 3 ETH, reads as on next",
commands: [0x0b, 0x0c, 0x10],
inputs: [
WRAP_INTO_ROUTER,
UNWRAP_INTO_ROUTER,
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_OUT_SINGLE_ID,
V4_SETTLE_ALL_ID,
V4_TAKE_ALL_ID,
]),
[
encodeV4ExactOutSingle(
ZERO_ADDR,
USDC_ADDR,
390000000n,
3n * ONE_ETH,
),
encodeV4SettleAll(ZERO_ADDR, 3n * ONE_ETH),
encodeV4TakeAll(USDC_ADDR, 0n),
],
),
],
lines: [
...wrapLines("The swap step that spends it"),
...ethLines(UNDECODED),
],
},
{
kind: "a WRAP_ETH of 1 ETH into the router, a V3 exact-out of at most 1 WETH paid from the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles",
commands: [0x0b, 0x01, 0x0c, 0x10],
inputs: [
WRAP_INTO_ROUTER,
encodeV3SwapExactOut(
USER_ADDR,
390000000n,
ONE_ETH,
[USDC_ADDR, WETH_ADDR],
false,
),
UNWRAP_INTO_ROUTER,
V4_ONE_ETH,
],
lines: [
["Amount", "Up to 1.0000 ETH"],
...ethLines("Up to 1.0000 ETH"),
],
},
{
kind: "a WRAP_ETH of 1 ETH into the router, a V4 exact-out paid from the router's WETH, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles",
commands: [0x0b, 0x10, 0x0c, 0x10],
inputs: [
WRAP_INTO_ROUTER,
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_OUT_SINGLE_ID,
V4_SETTLE_ID,
V4_TAKE_ALL_ID,
]),
[
encodeV4ExactOutSingle(
WETH_ADDR,
USDC_ADDR,
390000000n,
ONE_ETH,
),
settle(WETH_ADDR, 0n, false),
encodeV4TakeAll(USDC_ADDR, 0n),
],
),
UNWRAP_INTO_ROUTER,
V4_ONE_ETH,
],
lines: [
["Amount", "1.0000 ETH"],
...ethLines("Up to 1.0000 ETH"),
],
},
])("$kind", ({ router, commands, inputs, lines }) => {
const result = decodeSteps(commands, inputs, router);
expect(linesBetweenTokens(result)).toEqual(lines);
});
// A V3_SWAP_EXACT_OUT the user pays for, of at most `amountInMax` // A V3_SWAP_EXACT_OUT the user pays for, of at most `amountInMax`
// USDT for USDC. // USDT for USDC.
const v3ExactOut = (amountInMax) => const v3ExactOut = (amountInMax) =>
@@ -3639,6 +3310,98 @@ describe("uniswap decoder", () => {
expect(detail(result, "Amount").value).toBe("Up to 1000.0000 USDT"); expect(detail(result, "Amount").value).toBe("Up to 1000.0000 USDT");
}); });
// https://git.eeqj.de/sneak/AutistMask/issues/660 — Permit2 has no
// allowance of ETH, so a permit on the zero address reads as one on a
// token nothing knows. On next at 53551c6 such a permit's Allowance
// token line read ETH. A permit of 1 wei of the zero address before a
// V4 step of 5 ETH made the Amount line read 0.000000000000000001 ETH.
describe("a permit on the zero address", () => {
const FIVE_ETH = 5000000000000000000n;
// The lines of a permit of `amount` base units of the zero
// address for the router.
const zeroAddressLines = (amount) => [
[
"Permit2 allowance",
amount + " base units (decimals unknown)",
],
["Allowance token", ZERO_ADDR],
["Allowance spender", getAddress(ROUTER_ADDR)],
];
test("a PERMIT2_PERMIT on the zero address names the address", () => {
const result = decodeSteps(
[0x00, 0x0a],
[
V3_600,
encodePermit2(
ZERO_ADDR,
1000000000000000000n,
ROUTER_ADDR,
),
],
);
expect(linesBetweenTokens(result)).toEqual([
["Amount", "600.0000 USDT"],
...zeroAddressLines("1000000000000000000"),
]);
expect(detail(result, "Allowance token").address).toBe(
ZERO_ADDR,
);
});
test("a PERMIT2_PERMIT_BATCH entry on the zero address names the address", () => {
const result = decodeSteps(
[0x03, 0x00],
[
encodePermit2Batch(
[
[USDT_ADDR, USDT_600],
[ZERO_ADDR, 1000000000000000000n],
],
ROUTER_ADDR,
),
V3_600,
],
);
expect(linesBetweenTokens(result)).toEqual([
["Amount", "600.0000 USDT"],
...USDT_600_LINES,
...zeroAddressLines("1000000000000000000"),
]);
});
test("a PERMIT2_PERMIT of 1 wei of the zero address, then a V4 step of 5 ETH the user settles, shows the 5 ETH", () => {
const result = decodeSteps(
[0x0a, 0x10],
[
encodePermit2(ZERO_ADDR, 1n, ROUTER_ADDR),
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_SETTLE_ID,
V4_TAKE_ALL_ID,
]),
[
encodeV4ExactInSingle(
ZERO_ADDR,
USDC_ADDR,
FIVE_ETH,
1n,
),
settle(ZERO_ADDR, FIVE_ETH, true),
encodeV4TakeAll(USDC_ADDR, 1n),
],
),
],
);
expect(detail(result, "Token In").value).toBe("ETH (native)");
expect(linesBetweenTokens(result)).toEqual([
["Amount", "5.0000 ETH"],
...zeroAddressLines("1"),
]);
});
});
}); });
// https://git.eeqj.de/sneak/AutistMask/issues/359 — the output half, in // https://git.eeqj.de/sneak/AutistMask/issues/359 — the output half, in