Compare commits
2
Commits
b07f250926
...
56014a264b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56014a264b | ||
|
|
2c882c75d6 |
@@ -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**
|
||||||
@@ -2355,7 +2361,7 @@ view would leave a wallet one click from deletion.
|
|||||||
transaction"
|
transaction"
|
||||||
- Decoded action (if calldata is recognized): action name, token details,
|
- Decoded action (if calldata is recognized): action name, token details,
|
||||||
amounts, steps, deadline, and unless a swap pays only the From address, a
|
amounts, steps, deadline, and unless a swap pays only the From address, a
|
||||||
`Paid to` line for each other address it pays, or saying that its output
|
`Paid to` line for each other address it pays, or saying that a token
|
||||||
stays in the router or who it pays is unknown (see Transaction Decoding)
|
stays in the router or who it pays is unknown (see Transaction Decoding)
|
||||||
- From: color dot + full address + etherscan link
|
- From: color dot + full address + etherscan link
|
||||||
- Contract: color dot + full address + etherscan link, token symbol label if
|
- Contract: color dot + full address + etherscan link, token symbol label if
|
||||||
@@ -3048,49 +3054,68 @@ Currently supported:
|
|||||||
such as one whose owner word has a nonzero top byte (the router reads the
|
such as one whose owner word has a nonzero top byte (the router reads the
|
||||||
owner from the word's last 20 bytes), likewise sets neither, and it is listed
|
owner from the word's last 20 bytes), likewise sets neither, and it is listed
|
||||||
as `Balance Check (could not be decoded)`. When a swap step, `TAKE` or
|
as `Balance Check (could not be decoded)`. When a swap step, `TAKE` or
|
||||||
`TAKE_PORTION` pays the router the swap's output token, the output stays in
|
`TAKE_PORTION` pays the router any token, the swap's output or another, a
|
||||||
the router unless a later step pays all the router holds of it to anyone but
|
`WRAP_ETH` pays it WETH, or an `UNWRAP_WETH` into the router turns WETH a step
|
||||||
the router: a `SWEEP` of that token, an `UNWRAP_WETH` when it is WETH, a
|
paid it into ETH, that token stays in the router unless a later step pays out
|
||||||
`TRANSFER` of `CONTRACT_BALANCE`, a `PAY_PORTION` of 10,000 bips or a
|
or spends all the router holds of it: a `SWEEP` of that token to anyone but
|
||||||
`PAY_PORTION_FULL_PRECISION` of 1e18 of that token, or an
|
the router, an `UNWRAP_WETH` when it is WETH, a `WRAP_ETH` of
|
||||||
`ACROSS_V4_DEPOSIT_V3` of `CONTRACT_BALANCE` of that token, or of ETH when it
|
`CONTRACT_BALANCE` when it is ETH, a `TRANSFER` of `CONTRACT_BALANCE`, a
|
||||||
sets `useNative`. The router sends ETH as written, so a `TRANSFER` of
|
`PAY_PORTION` of 10,000 bips or a `PAY_PORTION_FULL_PRECISION` of 1e18 of that
|
||||||
`CONTRACT_BALANCE` of ETH always reverts and pays out nothing. A step paying a
|
token to anyone but the router, an `ACROSS_V4_DEPOSIT_V3` of
|
||||||
token to the V2 pair that the very next step spends, as a V2 exact-in of that
|
`CONTRACT_BALANCE` of that token, or of ETH when it sets `useNative`, or a V2
|
||||||
token whose `amountIn` is `ALREADY_PAID` (zero) or `CONTRACT_BALANCE`, is a
|
or V3 exact-in swap step or V4 `SETTLE` that pays `CONTRACT_BALANCE` of that
|
||||||
hop, not a payout: Uniswap's mixed routes pay the next V2 pair directly, and
|
token from the router (`payerIsUser` false). Such steps also spend all the
|
||||||
the router enforces that swap's own minimum. The decoder checks that the
|
WETH a `WRAP_ETH` paid the router once together they spend the amount the
|
||||||
address paid is that pair, deriving it from the V2 factory and pair init code
|
`WRAP_ETH` states, as a swap paid in ETH does. The router pays a `SETTLE` of
|
||||||
hash the network's routers were deployed with and the first two tokens of that
|
`OPEN_DELTA` (zero) only what its step still owes of that token when the
|
||||||
step's path; a payment to any other address is named. (Sepolia's
|
`SETTLE` runs, so one from the router counts as spending the amounts the
|
||||||
`UniversalRouterV1_2_V2Support` was deployed with placeholders for both
|
step's swap actions state they spend of that token only when every action in
|
||||||
values, so no V2 swap step can succeed on that router.) Every other payment
|
the step could be read, every swap action names the token it spends and the
|
||||||
counts, in whichever step and of whatever token, a share of the output
|
token it buys (a swap action with an empty path swaps nothing), the `SETTLE`
|
||||||
included: an interface fee paid to another address by `PAY_PORTION`,
|
is the step's only `SETTLE` or `SETTLE_ALL` of that token and comes after
|
||||||
`TRANSFER` or `TAKE_PORTION` is named, and the minimum reads `Min. paid out`,
|
every swap action, no swap action buys that token, and the swap actions that
|
||||||
because the swap's minimum includes the fee. The minimum reads `Min. received`
|
spend it state a quantity together; otherwise it counts as spending nothing.
|
||||||
only when some step pays the transaction's sender (the From address on
|
So the rest of a V4 `SETTLE` of more than its swap spends, taken to the
|
||||||
TxApproval, the sender the explorer reports on TransactionDetail) and no
|
router, the WETH of a `WRAP_ETH`, and a token a swap pays the router on the
|
||||||
`Paid to` line is shown. `Paid to` lines after `Token Out` name each other
|
way to the output, each stays in the router when later steps do not spend all
|
||||||
address paid; one reads
|
of it, where anyone can take it in a later transaction. The router sends ETH
|
||||||
|
as written, so a `TRANSFER` of `CONTRACT_BALANCE` of ETH always reverts and
|
||||||
|
pays out nothing. A step paying a token to the V2 pair that the very next step
|
||||||
|
spends, as a V2 exact-in of that token whose `amountIn` is `ALREADY_PAID`
|
||||||
|
(zero) or `CONTRACT_BALANCE`, is a hop, not a payout: Uniswap's mixed routes
|
||||||
|
pay the next V2 pair directly, and the router enforces that swap's own
|
||||||
|
minimum. The decoder checks that the address paid is that pair, deriving it
|
||||||
|
from the V2 factory and pair init code hash the network's routers were
|
||||||
|
deployed with and the first two tokens of that step's path; a payment to any
|
||||||
|
other address is named. (Sepolia's `UniversalRouterV1_2_V2Support` was
|
||||||
|
deployed with placeholders for both values, so no V2 swap step can succeed on
|
||||||
|
that router.) Every other payment counts, in whichever step and of whatever
|
||||||
|
token, a share of the output included: an interface fee paid to another
|
||||||
|
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
|
||||||
|
`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
|
||||||
bridge deposit; one reads, for example,
|
bridge deposit; one reads, for example,
|
||||||
`The router, 2000.0000 USDT from the sender (later steps may not spend all of it)`
|
`The router, 2000.0000 USDT from the sender (later steps may not spend all of it)`
|
||||||
for each move into the router, followed by a `Token moved into the router`
|
for each move into the router, followed by a `Token moved into the router`
|
||||||
line that gives the moved token's address as the `Token In` line does; one
|
line that gives the moved token's address as the `Token In` line does; one
|
||||||
reads `The router, where it stays (no later step pays it out)` when the output
|
reads `The router, where it stays (no later step pays it out)` for each token
|
||||||
stays in the router; and one more reads `Unknown (could not be decoded)` when
|
that stays in the router, followed, for any token but the output, by a
|
||||||
a step that may pay an address went unread. When no step pays anyone, a single
|
`Token moved into the router` line giving that token in the same way (ETH by
|
||||||
line reads `Unknown (not named in the calldata)`. Wherever there is a
|
name); and one more reads `Unknown (could not be decoded)` when a step that
|
||||||
`Paid to` line, the minimum reads `Min. paid out`. A step goes unread when its
|
may pay an address went unread. When no step pays anyone, a single line reads
|
||||||
recipient does not decode as an address, or when it is a command the decoder
|
`Unknown (not named in the calldata)`. Wherever there is a `Paid to` line, the
|
||||||
does not read: every command other than `PERMIT2_PERMIT`,
|
minimum reads `Min. paid out`. A step goes unread when its recipient does not
|
||||||
`PERMIT2_TRANSFER_FROM`, `V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`,
|
decode as an address, or when it is a command the decoder does not read: every
|
||||||
`V3_SWAP_EXACT_IN`, `V4_SWAP`, `SWEEP`, `TRANSFER`, `PAY_PORTION`,
|
command other than `PERMIT2_PERMIT`, `PERMIT2_TRANSFER_FROM`,
|
||||||
`PAY_PORTION_FULL_PRECISION`, `ACROSS_V4_DEPOSIT_V3`, `WRAP_ETH`,
|
`V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`, `V3_SWAP_EXACT_IN`, `V4_SWAP`,
|
||||||
`UNWRAP_WETH` and `BALANCE_CHECK_ERC20` (`V3_SWAP_EXACT_OUT`,
|
`SWEEP`, `TRANSFER`, `PAY_PORTION`, `PAY_PORTION_FULL_PRECISION`,
|
||||||
`PERMIT2_TRANSFER_FROM_BATCH`, `EXECUTE_SUB_PLAN` and the NFT commands among
|
`ACROSS_V4_DEPOSIT_V3`, `WRAP_ETH`, `UNWRAP_WETH` and `BALANCE_CHECK_ERC20`
|
||||||
them), or a type its release names no command for.
|
(`V3_SWAP_EXACT_OUT`, `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
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+196
-37
@@ -5,6 +5,7 @@ const {
|
|||||||
Interface,
|
Interface,
|
||||||
AbiCoder,
|
AbiCoder,
|
||||||
getBytes,
|
getBytes,
|
||||||
|
getAddress,
|
||||||
formatUnits,
|
formatUnits,
|
||||||
getCreate2Address,
|
getCreate2Address,
|
||||||
keccak256,
|
keccak256,
|
||||||
@@ -337,8 +338,8 @@ const UNNAMED_CURRENCY = "Unknown (not named in the calldata)";
|
|||||||
// address this decoder did not read.
|
// address this decoder did not read.
|
||||||
const UNDECODED_CURRENCY = "Unknown (could not be decoded)";
|
const UNDECODED_CURRENCY = "Unknown (could not be decoded)";
|
||||||
|
|
||||||
// The Paid to line when the swap's output is paid to the router and no later
|
// The Paid to line for each token paid to the router, the swap's output or
|
||||||
// step pays it out of the router.
|
// another, that no later step pays out of the router.
|
||||||
const STAYS_IN_ROUTER =
|
const STAYS_IN_ROUTER =
|
||||||
"The router, where it stays (no later step pays it out)";
|
"The router, where it stays (no later step pays it out)";
|
||||||
|
|
||||||
@@ -413,12 +414,16 @@ const NO_MINIMUM = "None (no minimum guaranteed)";
|
|||||||
// Permit2 amounts are uint160; the maximum is Permit2's "unbounded".
|
// Permit2 amounts are uint160; the maximum is Permit2's "unbounded".
|
||||||
const MAX_UINT160 = BigInt("0xffffffffffffffffffffffffffffffffffffffff");
|
const MAX_UINT160 = BigInt("0xffffffffffffffffffffffffffffffffffffffff");
|
||||||
|
|
||||||
// WETH, the token UNWRAP_WETH turns into ETH: on mainnet, then on Sepolia.
|
// WETH, the token WRAP_ETH turns ETH into and UNWRAP_WETH turns into ETH, on
|
||||||
// Either one counts on either network.
|
// each network, by chain id: `weth9` in the deployment parameters
|
||||||
const WETH_ADDRESSES = [
|
// V2_FACTORIES cites, which every router in ROUTER_ADDRESSES was deployed
|
||||||
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
// with (their constructor arguments on the explorer).
|
||||||
"0xfff9976782d46cc05630d1f6ebab18b2324d6b14",
|
const WETH = new Map([
|
||||||
];
|
[NETWORKS.mainnet.chainId, "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"],
|
||||||
|
[NETWORKS.sepolia.chainId, "0xfff9976782d46cc05630d1f6ebab18b2324d6b14"],
|
||||||
|
]);
|
||||||
|
// Either one counts on either network as the WETH UNWRAP_WETH turns into ETH.
|
||||||
|
const WETH_ADDRESSES = [...WETH.values()];
|
||||||
|
|
||||||
// Two recipient values the router reads as placeholders, not as addresses:
|
// Two recipient values the router reads as placeholders, not as addresses:
|
||||||
// MSG_SENDER pays the caller of execute(), which is the transaction's sender,
|
// MSG_SENDER pays the caller of execute(), which is the transaction's sender,
|
||||||
@@ -522,6 +527,7 @@ function decodeBalanceCheck(input) {
|
|||||||
// ABI: (address recipient, uint256 amountIn, uint256 amountOutMin,
|
// ABI: (address recipient, uint256 amountIn, uint256 amountOutMin,
|
||||||
// address[] path, bool payerIsUser)
|
// address[] path, bool payerIsUser)
|
||||||
// A zero `amountIn` is read the way the router reads it, as ALREADY_PAID.
|
// A zero `amountIn` is read the way the router reads it, as ALREADY_PAID.
|
||||||
|
// With `payerIsUser` false the router pays the input from its own balance.
|
||||||
function decodeV2SwapExactIn(input) {
|
function decodeV2SwapExactIn(input) {
|
||||||
try {
|
try {
|
||||||
const d = coder.decode(
|
const d = coder.decode(
|
||||||
@@ -535,6 +541,7 @@ function decodeV2SwapExactIn(input) {
|
|||||||
path: d[3],
|
path: d[3],
|
||||||
tokenIn: d[3][0],
|
tokenIn: d[3][0],
|
||||||
tokenOut: d[3][d[3].length - 1],
|
tokenOut: d[3][d[3].length - 1],
|
||||||
|
payerIsUser: d[4],
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -574,6 +581,7 @@ function decodeV3Path(pathHex) {
|
|||||||
// Decode V3_SWAP_EXACT_IN (command 0x00) input bytes.
|
// Decode V3_SWAP_EXACT_IN (command 0x00) input bytes.
|
||||||
// ABI: (address recipient, uint256 amountIn, uint256 amountOutMin,
|
// ABI: (address recipient, uint256 amountIn, uint256 amountOutMin,
|
||||||
// bytes path, bool payerIsUser)
|
// bytes path, bool payerIsUser)
|
||||||
|
// With `payerIsUser` false the router pays the input from its own balance.
|
||||||
function decodeV3SwapExactIn(input) {
|
function decodeV3SwapExactIn(input) {
|
||||||
try {
|
try {
|
||||||
const d = coder.decode(
|
const d = coder.decode(
|
||||||
@@ -588,6 +596,7 @@ function decodeV3SwapExactIn(input) {
|
|||||||
amountOutMin: d[2],
|
amountOutMin: d[2],
|
||||||
tokenIn: path.tokenIn,
|
tokenIn: path.tokenIn,
|
||||||
tokenOut: path.tokenOut,
|
tokenOut: path.tokenOut,
|
||||||
|
payerIsUser: d[4],
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -877,7 +886,10 @@ function totalAmount(amounts) {
|
|||||||
// and its recipient as written, MSG_SENDER for a TAKE_ALL. TAKE_PORTION takes
|
// and its recipient as written, MSG_SENDER for a TAKE_ALL. TAKE_PORTION takes
|
||||||
// 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.
|
// read. `settledFromRouter` is every SETTLE that the router pays from its own
|
||||||
|
// balance (`payerIsUser` false), as its currency and the amount it pays: a
|
||||||
|
// quantity, CONTRACT_BALANCE (all the router holds), or null when this
|
||||||
|
// 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);
|
||||||
@@ -887,6 +899,15 @@ function decodeV4Swap(input) {
|
|||||||
let settleToken = null;
|
let settleToken = null;
|
||||||
let takeToken = null;
|
let takeToken = null;
|
||||||
const takes = [];
|
const takes = [];
|
||||||
|
// Every SETTLE paid from the router, as its currency, its amount as
|
||||||
|
// written and its index among the step's actions.
|
||||||
|
const routerSettles = [];
|
||||||
|
// The currency of every SETTLE and SETTLE_ALL.
|
||||||
|
const settledCurrencies = [];
|
||||||
|
// The index of the step's last swap action, read or not, or -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.
|
||||||
@@ -906,10 +927,19 @@ function decodeV4Swap(input) {
|
|||||||
params[i],
|
params[i],
|
||||||
);
|
);
|
||||||
settleToken = s[0];
|
settleToken = s[0];
|
||||||
|
settledCurrencies.push(s[0]);
|
||||||
|
if (!s[2]) {
|
||||||
|
routerSettles.push({
|
||||||
|
currency: s[0],
|
||||||
|
amount: s[1],
|
||||||
|
index: i,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (actionId === V4_SETTLE_ALL) {
|
} else if (actionId === V4_SETTLE_ALL) {
|
||||||
// SETTLE_ALL: (address currency, uint256 maxAmount)
|
// SETTLE_ALL: (address currency, uint256 maxAmount)
|
||||||
const s = coder.decode(["address", "uint256"], params[i]);
|
const s = coder.decode(["address", "uint256"], params[i]);
|
||||||
settleToken = s[0];
|
settleToken = s[0];
|
||||||
|
settledCurrencies.push(s[0]);
|
||||||
} else if (actionId === V4_TAKE) {
|
} else if (actionId === V4_TAKE) {
|
||||||
// TAKE: (address currency, address recipient, uint256 amount)
|
// TAKE: (address currency, address recipient, uint256 amount)
|
||||||
const t = coder.decode(
|
const t = coder.decode(
|
||||||
@@ -936,18 +966,23 @@ function decodeV4Swap(input) {
|
|||||||
actionId === V4_SWAP_EXACT_OUT_SINGLE ||
|
actionId === V4_SWAP_EXACT_OUT_SINGLE ||
|
||||||
actionId === V4_SWAP_EXACT_OUT
|
actionId === V4_SWAP_EXACT_OUT
|
||||||
) {
|
) {
|
||||||
|
lastSwapAction = i;
|
||||||
try {
|
try {
|
||||||
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 ||
|
||||||
@@ -958,6 +993,41 @@ function decodeV4Swap(input) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What each SETTLE paid from the router pays. A quantity or
|
||||||
|
// CONTRACT_BALANCE pays as written. A SETTLE of OPEN_DELTA (zero)
|
||||||
|
// pays whatever the step still owes of its currency when it runs.
|
||||||
|
// That is what the step's swap actions state they spend of it only
|
||||||
|
// in the plain shape of Uniswap's own routes, so only then is it
|
||||||
|
// given that amount: every action in the step was read; every swap
|
||||||
|
// 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 =
|
||||||
|
!actionUnread &&
|
||||||
|
swaps.every((w) => present(w.tokenIn) && present(w.tokenOut)) &&
|
||||||
|
settledCurrencies.filter((c) => c === s.currency).length ===
|
||||||
|
1 &&
|
||||||
|
s.index > lastSwapAction &&
|
||||||
|
!swaps.some((w) => w.tokenOut === s.currency) &&
|
||||||
|
typeof stated === "bigint";
|
||||||
|
return {
|
||||||
|
currency: s.currency,
|
||||||
|
amount: paysWhatSwapsSpend ? stated : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Rule 3.
|
// Rule 3.
|
||||||
if (swapUndecoded) {
|
if (swapUndecoded) {
|
||||||
return {
|
return {
|
||||||
@@ -966,6 +1036,7 @@ function decodeV4Swap(input) {
|
|||||||
amountIn: null,
|
amountIn: null,
|
||||||
amountOutMin: null,
|
amountOutMin: null,
|
||||||
takes,
|
takes,
|
||||||
|
settledFromRouter,
|
||||||
recipientUnread,
|
recipientUnread,
|
||||||
swapUndecoded,
|
swapUndecoded,
|
||||||
hasSwapAction: true,
|
hasSwapAction: true,
|
||||||
@@ -1007,6 +1078,7 @@ function decodeV4Swap(input) {
|
|||||||
amountIn,
|
amountIn,
|
||||||
amountOutMin,
|
amountOutMin,
|
||||||
takes,
|
takes,
|
||||||
|
settledFromRouter,
|
||||||
recipientUnread,
|
recipientUnread,
|
||||||
swapUndecoded,
|
swapUndecoded,
|
||||||
hasSwapAction: swaps.length > 0,
|
hasSwapAction: swaps.length > 0,
|
||||||
@@ -1099,16 +1171,44 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// router, as { token, amount }. Later steps may leave part of it
|
// router, as { token, amount }. Later steps may leave part of it
|
||||||
// there: nothing here checks that they spend all of it.
|
// there: nothing here checks that they spend all of it.
|
||||||
const movedIntoRouter = [];
|
const movedIntoRouter = [];
|
||||||
// The tokens, lowercased, that a swap step or a V4 TAKE or
|
// Each token a step paid to the router that no later step paid out
|
||||||
// TAKE_PORTION paid to the router and no later step paid out of it in
|
// or spent in full, by its address, lowercased, ETH as the zero
|
||||||
// full: a SWEEP of that token, an UNWRAP_WETH of WETH, or a TRANSFER,
|
// address. The router is paid what a swap step, V4 TAKE or
|
||||||
// PAY_PORTION, PAY_PORTION_FULL_PRECISION or ACROSS_V4_DEPOSIT_V3 of
|
// TAKE_PORTION pays it, the WETH of a WRAP_ETH into it, and the ETH
|
||||||
// all the router holds of that token.
|
// of an UNWRAP_WETH into it, in place of the WETH it held. All the
|
||||||
const heldByRouter = new Set();
|
// router holds of a token is paid out or spent by a SWEEP of that
|
||||||
|
// token, an UNWRAP_WETH of WETH, a WRAP_ETH of CONTRACT_BALANCE of
|
||||||
|
// ETH, a TRANSFER, PAY_PORTION, PAY_PORTION_FULL_PRECISION or
|
||||||
|
// ACROSS_V4_DEPOSIT_V3 of all of it, or a swap step or V4 SETTLE paid
|
||||||
|
// from the router (see spendFromRouter()). The value is the amount the
|
||||||
|
// router holds, in base units, when a WRAP_ETH stated it, otherwise
|
||||||
|
// null.
|
||||||
|
const heldByRouter = new Map();
|
||||||
const isRouter = (recipient) => {
|
const isRouter = (recipient) => {
|
||||||
const r = recipient.toLowerCase();
|
const r = recipient.toLowerCase();
|
||||||
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
|
||||||
|
// own balance. CONTRACT_BALANCE pays all of it. Any other quantity
|
||||||
|
// counts against the amount a WRAP_ETH stated, and the steps pay all
|
||||||
|
// of it once their amounts reach that one: a swap paid in ETH wraps
|
||||||
|
// what its swap steps then spend. An amount that is not a quantity,
|
||||||
|
// such as the null decodeV4Swap() gives a SETTLE whose payment it
|
||||||
|
// cannot tell, or one of a token whose amount is not known, pays out
|
||||||
|
// nothing this decoder can see.
|
||||||
|
const spendFromRouter = (token, amount) => {
|
||||||
|
const t = token.toLowerCase();
|
||||||
|
const held = heldByRouter.get(t);
|
||||||
|
if (amount === CONTRACT_BALANCE) {
|
||||||
|
heldByRouter.delete(t);
|
||||||
|
} else if (typeof held === "bigint" && typeof amount === "bigint") {
|
||||||
|
if (amount >= held) {
|
||||||
|
heldByRouter.delete(t);
|
||||||
|
} else {
|
||||||
|
heldByRouter.set(t, held - amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
// A step pays `recipient`, an address other than the sender's or the
|
// A step pays `recipient`, an address other than the sender's or the
|
||||||
// router's, as written.
|
// router's, as written.
|
||||||
const addPaidElsewhere = (recipient) => {
|
const addPaidElsewhere = (recipient) => {
|
||||||
@@ -1122,7 +1222,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
const addPayee = (recipient, output) => {
|
const addPayee = (recipient, output) => {
|
||||||
const r = recipient.toLowerCase();
|
const r = recipient.toLowerCase();
|
||||||
if (isRouter(recipient)) {
|
if (isRouter(recipient)) {
|
||||||
if (present(output)) heldByRouter.add(output.toLowerCase());
|
if (present(output)) {
|
||||||
|
heldByRouter.set(output.toLowerCase(), null);
|
||||||
|
}
|
||||||
} else if (r === MSG_SENDER || r === sender) {
|
} else if (r === MSG_SENDER || r === sender) {
|
||||||
paidToSender = true;
|
paidToSender = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -1243,6 +1345,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// the last swap step determines the final output token
|
// the last swap step determines the final output token
|
||||||
// and minimum received amount.
|
// and minimum received amount.
|
||||||
setOutput(s.tokenOut, s.amountOutMin);
|
setOutput(s.tokenOut, s.amountOutMin);
|
||||||
|
if (!s.payerIsUser) {
|
||||||
|
spendFromRouter(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);
|
||||||
}
|
}
|
||||||
@@ -1254,6 +1359,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
if (s) {
|
if (s) {
|
||||||
setInputOnce(s.tokenIn, s.amountIn);
|
setInputOnce(s.tokenIn, s.amountIn);
|
||||||
setOutput(s.tokenOut, s.amountOutMin);
|
setOutput(s.tokenOut, s.amountOutMin);
|
||||||
|
if (!s.payerIsUser) {
|
||||||
|
spendFromRouter(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);
|
||||||
}
|
}
|
||||||
@@ -1350,12 +1458,36 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
w.amount,
|
w.amount,
|
||||||
);
|
);
|
||||||
addPayee(w.recipient);
|
addPayee(w.recipient);
|
||||||
|
// CONTRACT_BALANCE wraps all the router's ETH.
|
||||||
|
if (w.amount === CONTRACT_BALANCE) {
|
||||||
|
heldByRouter.delete(
|
||||||
|
"0x0000000000000000000000000000000000000000",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Into the router, the router holds the WETH. Its
|
||||||
|
// amount is known when the step states it and the
|
||||||
|
// router held no WETH before.
|
||||||
|
if (isRouter(w.recipient) && w.amount > 0n) {
|
||||||
|
const weth = WETH.get(chainId);
|
||||||
|
heldByRouter.set(
|
||||||
|
weth,
|
||||||
|
w.amount === CONTRACT_BALANCE ||
|
||||||
|
heldByRouter.has(weth)
|
||||||
|
? null
|
||||||
|
: w.amount,
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
payeeUnread = true;
|
payeeUnread = true;
|
||||||
}
|
}
|
||||||
} else if (command === "V4_SWAP") {
|
} else if (command === "V4_SWAP") {
|
||||||
const v4 = decodeV4Swap(inputs[i]);
|
const v4 = decodeV4Swap(inputs[i]);
|
||||||
if (v4) {
|
if (v4) {
|
||||||
|
// Each SETTLE paid from the router spends the amount
|
||||||
|
// decodeV4Swap() says it pays.
|
||||||
|
for (const s of v4.settledFromRouter) {
|
||||||
|
spendFromRouter(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)) {
|
||||||
addPayee(t.recipient, t.currency);
|
addPayee(t.recipient, t.currency);
|
||||||
@@ -1400,9 +1532,17 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
const u = decodeUnwrapWeth(inputs[i]);
|
const u = decodeUnwrapWeth(inputs[i]);
|
||||||
if (u) {
|
if (u) {
|
||||||
addPayee(u.recipient);
|
addPayee(u.recipient);
|
||||||
if (!isRouter(u.recipient)) {
|
// It pays out all the router's WETH, as ETH. Into the
|
||||||
WETH_ADDRESSES.forEach((w) =>
|
// router, the router then holds as ETH the WETH a
|
||||||
heldByRouter.delete(w),
|
// step paid it.
|
||||||
|
const heldWeth = WETH_ADDRESSES.some((w) =>
|
||||||
|
heldByRouter.has(w),
|
||||||
|
);
|
||||||
|
WETH_ADDRESSES.forEach((w) => heldByRouter.delete(w));
|
||||||
|
if (isRouter(u.recipient) && heldWeth) {
|
||||||
|
heldByRouter.set(
|
||||||
|
"0x0000000000000000000000000000000000000000",
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1568,11 +1708,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
|
|
||||||
// A Paid to line names each other address paid, and says when a
|
// A Paid to line names each other address paid, and says when a
|
||||||
// step pays the bridge, when one moves the sender's tokens into the
|
// step pays the bridge, when one moves the sender's tokens into the
|
||||||
// router, when the output stays in the router, and when a payout
|
// router, when a token stays in the router, and when a payout could
|
||||||
// could not be read. The output is received by the sender only when
|
// not be read. The output is received by the sender only when some
|
||||||
// some step pays the sender and there is no such line. When no step
|
// step pays the sender and there is no such line. When no step names
|
||||||
// names anyone paid, one line says so. `paidTo` holds these lines and
|
// anyone paid, one line says so. `paidTo` holds these lines and the
|
||||||
// the token line after each move into the router.
|
// token line after each line about a token in the router.
|
||||||
const outputHeld =
|
const outputHeld =
|
||||||
present(outputToken) && heldByRouter.has(outputToken.toLowerCase());
|
present(outputToken) && heldByRouter.has(outputToken.toLowerCase());
|
||||||
const paidTo = paidElsewhere.map((address) => ({
|
const paidTo = paidElsewhere.map((address) => ({
|
||||||
@@ -1583,9 +1723,24 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
if (bridgeDeposit) {
|
if (bridgeDeposit) {
|
||||||
paidTo.push({ label: "Paid to", value: PAID_TO_BRIDGE });
|
paidTo.push({ label: "Paid to", value: PAID_TO_BRIDGE });
|
||||||
}
|
}
|
||||||
// Each move into the router is followed by a line giving the moved
|
// The line giving a token in the router, in the shape of the Token In
|
||||||
// token's address, in the shape of the Token In line, so the screens
|
// line, so the screens show it as they show that one. ETH has no
|
||||||
// show it as they show that one.
|
// address.
|
||||||
|
const tokenMovedLine = (token) => {
|
||||||
|
const info = tokenInfo(token, sources);
|
||||||
|
if (!present(info.address)) {
|
||||||
|
return {
|
||||||
|
label: "Token moved into the router",
|
||||||
|
value: info.symbol,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
label: "Token moved into the router",
|
||||||
|
value: info.symbol ? info.symbol + " (" + token + ")" : token,
|
||||||
|
address: token,
|
||||||
|
isToken: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
for (const m of movedIntoRouter) {
|
for (const m of movedIntoRouter) {
|
||||||
const info = tokenInfo(m.token, sources);
|
const info = tokenInfo(m.token, sources);
|
||||||
paidTo.push({
|
paidTo.push({
|
||||||
@@ -1595,18 +1750,22 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
amountText(m.amount, info).display +
|
amountText(m.amount, info).display +
|
||||||
" from the sender (later steps may not spend all of it)",
|
" from the sender (later steps may not spend all of it)",
|
||||||
});
|
});
|
||||||
paidTo.push({
|
paidTo.push(tokenMovedLine(m.token));
|
||||||
label: "Token moved into the router",
|
|
||||||
value: info.symbol
|
|
||||||
? info.symbol + " (" + m.token + ")"
|
|
||||||
: m.token,
|
|
||||||
address: m.token,
|
|
||||||
isToken: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (outputHeld) {
|
if (outputHeld) {
|
||||||
paidTo.push({ label: "Paid to", value: STAYS_IN_ROUTER });
|
paidTo.push({ label: "Paid to", value: STAYS_IN_ROUTER });
|
||||||
}
|
}
|
||||||
|
// Any other token the router still holds after the last step, such
|
||||||
|
// as one a swap step bought on the way to the output, what a V4
|
||||||
|
// SETTLE took beyond what the step's swap spent, or the WETH of a
|
||||||
|
// WRAP_ETH, stays there too, and its line is followed by the token's.
|
||||||
|
for (const token of heldByRouter.keys()) {
|
||||||
|
if (present(outputToken) && token === outputToken.toLowerCase()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
paidTo.push({ label: "Paid to", value: STAYS_IN_ROUTER });
|
||||||
|
paidTo.push(tokenMovedLine(getAddress(token)));
|
||||||
|
}
|
||||||
if (payeeUnread) {
|
if (payeeUnread) {
|
||||||
paidTo.push({ label: "Paid to", value: UNDECODED_CURRENCY });
|
paidTo.push({ label: "Paid to", value: UNDECODED_CURRENCY });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -739,7 +739,9 @@ describe("uniswap decoder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Paid in ETH, with an exact-out step, the last swap step buys WETH, so
|
// Paid in ETH, with an exact-out step, the last swap step buys WETH, so
|
||||||
// UNWRAP_WETH makes the output ETH.
|
// UNWRAP_WETH makes the output ETH. That step is paid by the user, so the
|
||||||
|
// USDC the exact-out step bought stays in the router, and the minimum is
|
||||||
|
// not received (https://git.eeqj.de/sneak/AutistMask/issues/606).
|
||||||
test("an ETH-paid swap whose last step buys WETH, then UNWRAP_WETH, shows ETH", () => {
|
test("an ETH-paid swap whose last step buys WETH, then UNWRAP_WETH, shows ETH", () => {
|
||||||
const data = buildExecute(
|
const data = buildExecute(
|
||||||
solidityPacked(
|
solidityPacked(
|
||||||
@@ -767,7 +769,7 @@ describe("uniswap decoder", () => {
|
|||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||||
expect(detail(result, "Token Out").value).toBe("ETH");
|
expect(detail(result, "Token Out").value).toBe("ETH");
|
||||||
expect(detail(result, "Min. received").value).toBe("0.4000 ETH");
|
expect(detail(result, "Min. paid out").value).toBe("0.4000 ETH");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sepolia's WETH is a different contract; UNWRAP_WETH makes it ETH too.
|
// Sepolia's WETH is a different contract; UNWRAP_WETH makes it ETH too.
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
// on the steps that pay it
|
// on the steps that pay it
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/602), the command each router
|
// (https://git.eeqj.de/sneak/AutistMask/issues/602), the command each router
|
||||||
// release runs for a command type
|
// release runs for a command type
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/592), and the address whose
|
// (https://git.eeqj.de/sneak/AutistMask/issues/592), the address whose
|
||||||
// balance a BALANCE_CHECK_ERC20 checks
|
// balance a BALANCE_CHECK_ERC20 checks
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/600).
|
// (https://git.eeqj.de/sneak/AutistMask/issues/600), and any token a swap
|
||||||
|
// leaves in the router (https://git.eeqj.de/sneak/AutistMask/issues/606).
|
||||||
//
|
//
|
||||||
// Every swap command, SWEEP, WRAP_ETH, UNWRAP_WETH, TRANSFER, PAY_PORTION,
|
// Every swap command, SWEEP, WRAP_ETH, UNWRAP_WETH, TRANSFER, PAY_PORTION,
|
||||||
// PAY_PORTION_FULL_PRECISION, PERMIT2_TRANSFER_FROM, ACROSS_V4_DEPOSIT_V3 and
|
// PAY_PORTION_FULL_PRECISION, PERMIT2_TRANSFER_FROM, ACROSS_V4_DEPOSIT_V3 and
|
||||||
@@ -546,6 +547,486 @@ describe("an output left in the router is not received", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/606: any token a step pays the
|
||||||
|
// router stays there unless a later step pays out or spends all of it, not
|
||||||
|
// only the swap's output. A V4 SETTLE can take more of the user's tokens than
|
||||||
|
// the step's swap spends, a WRAP_ETH wraps the user's ETH into the router, and
|
||||||
|
// a swap can leave there the token it bought on the way to the output.
|
||||||
|
describe("any token left in the router is not received", () => {
|
||||||
|
// The WETH/USDC pair on Ethereum Mainnet, as its V2 factory reports it.
|
||||||
|
const PAIR = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc";
|
||||||
|
const USDT_5000 = 5000000000n;
|
||||||
|
|
||||||
|
// V4 SETTLE of `amount` of `currency` (zero: what the step's swap
|
||||||
|
// spends), paid by the user, or by the router when `payerIsUser` is false.
|
||||||
|
function settle(currency, amount, payerIsUser) {
|
||||||
|
return [
|
||||||
|
0x0b,
|
||||||
|
coder.encode(
|
||||||
|
["address", "uint256", "bool"],
|
||||||
|
[currency, amount, payerIsUser],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// V4 SWAP_EXACT_IN_SINGLE of `amountIn` of `currencyIn` (zero: the open
|
||||||
|
// delta) for at least `amountOutMin` of `currencyOut`.
|
||||||
|
function swapExactInSingle(
|
||||||
|
currencyIn,
|
||||||
|
currencyOut,
|
||||||
|
amountIn,
|
||||||
|
amountOutMin,
|
||||||
|
) {
|
||||||
|
return [
|
||||||
|
0x06,
|
||||||
|
coder.encode(
|
||||||
|
[
|
||||||
|
"tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
[currencyIn, currencyOut, 500, 10, ZERO],
|
||||||
|
true,
|
||||||
|
amountIn,
|
||||||
|
amountOutMin,
|
||||||
|
"0x",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// 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.
|
||||||
|
const tokenLine = (token, symbol) => ({
|
||||||
|
label: "Token moved into the router",
|
||||||
|
value: symbol + " (" + token + ")",
|
||||||
|
address: token,
|
||||||
|
isToken: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
kind: "a V4 SETTLE of 5,000 USDT for a swap of 2,000, the rest taken to the router by TAKE",
|
||||||
|
data: execute(
|
||||||
|
v4Swap(
|
||||||
|
settle(USDT, USDT_5000, true),
|
||||||
|
SWAP_USDT_FOR_USDC,
|
||||||
|
take(USDT, ADDRESS_THIS),
|
||||||
|
takeAll(USDC),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
left: tokenLine(USDT, "USDT"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a V4 SETTLE of 5,000 USDT for a swap of 2,000, the rest taken to the router by TAKE_PORTION",
|
||||||
|
data: execute(
|
||||||
|
v4Swap(
|
||||||
|
settle(USDT, USDT_5000, true),
|
||||||
|
SWAP_USDT_FOR_USDC,
|
||||||
|
takeAll(USDC),
|
||||||
|
takePortion(USDT, ADDRESS_THIS, 10000n),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
left: tokenLine(USDT, "USDT"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a WRAP_ETH into the router that nothing spends",
|
||||||
|
data: execute(
|
||||||
|
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||||
|
v3SwapExactIn(MSG_SENDER, USDT_2000, USDC_1990, USDT, USDC),
|
||||||
|
),
|
||||||
|
left: tokenLine(WETH, "WETH"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a WRAP_ETH of 1 ETH into the router, then a swap of 0.5 WETH from the router",
|
||||||
|
data: execute(
|
||||||
|
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||||
|
v3SwapExactIn(
|
||||||
|
MSG_SENDER,
|
||||||
|
ONE_WETH / 2n,
|
||||||
|
USDC_1990,
|
||||||
|
WETH,
|
||||||
|
USDC,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
left: tokenLine(WETH, "WETH"),
|
||||||
|
},
|
||||||
|
// A SETTLE of OPEN_DELTA from the router pays only what the step
|
||||||
|
// still owes of its currency when it runs.
|
||||||
|
{
|
||||||
|
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 0.5 WETH with two SETTLEs of OPEN_DELTA from the router",
|
||||||
|
data: execute(
|
||||||
|
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||||
|
v4Swap(
|
||||||
|
swapExactInSingle(WETH, USDC, ONE_WETH / 2n, USDC_1990),
|
||||||
|
settle(WETH, 0n, false),
|
||||||
|
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 0.75 WETH with a SETTLE of 0.25 WETH and one of OPEN_DELTA from the router",
|
||||||
|
data: execute(
|
||||||
|
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||||
|
v4Swap(
|
||||||
|
swapExactInSingle(
|
||||||
|
WETH,
|
||||||
|
USDC,
|
||||||
|
(ONE_WETH * 3n) / 4n,
|
||||||
|
USDC_1990,
|
||||||
|
),
|
||||||
|
settle(WETH, ONE_WETH / 4n, false),
|
||||||
|
settle(WETH, 0n, false),
|
||||||
|
takeAll(USDC),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
left: tokenLine(WETH, "WETH"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 step that also buys WETH back, with a SETTLE of OPEN_DELTA from the router",
|
||||||
|
data: execute(
|
||||||
|
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||||
|
v4Swap(
|
||||||
|
swapExactInSingle(WETH, USDC, ONE_WETH, USDC_1990),
|
||||||
|
swapExactInSingle(USDC, WETH, 1000000n, 1n),
|
||||||
|
settle(WETH, 0n, false),
|
||||||
|
takeAll(USDC),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
left: tokenLine(WETH, "WETH"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 SETTLE of OPEN_DELTA from the router before the swap, which the user settles",
|
||||||
|
data: execute(
|
||||||
|
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||||
|
v4Swap(
|
||||||
|
settle(WETH, 0n, false),
|
||||||
|
swapExactInSingle(WETH, USDC, ONE_WETH, USDC_1990),
|
||||||
|
settle(WETH, 0n, true),
|
||||||
|
takeAll(USDC),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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",
|
||||||
|
data: execute(
|
||||||
|
swapToWeth(ADDRESS_THIS),
|
||||||
|
transfer(0x05, WETH, PAIR, 1n),
|
||||||
|
v2Swap(0x08, MSG_SENDER, 0n, USDC_1990, [WETH, USDC], false),
|
||||||
|
),
|
||||||
|
left: tokenLine(WETH, "WETH"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a swap paying WETH to the router, then a swap of 1 wei of it from the router",
|
||||||
|
data: execute(
|
||||||
|
swapToWeth(ADDRESS_THIS),
|
||||||
|
v2Swap(0x08, MSG_SENDER, 1n, USDC_1990, [WETH, USDC], false),
|
||||||
|
),
|
||||||
|
left: tokenLine(WETH, "WETH"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a swap paying WETH to the router, an UNWRAP_WETH into the router, then another swap",
|
||||||
|
data: execute(
|
||||||
|
swapToWeth(ADDRESS_THIS),
|
||||||
|
unwrapWeth(ADDRESS_THIS),
|
||||||
|
v3SwapExactIn(MSG_SENDER, USDT_2000, USDC_1990, USDT, USDC),
|
||||||
|
),
|
||||||
|
left: { label: "Token moved into the router", value: "ETH" },
|
||||||
|
},
|
||||||
|
])("$kind", ({ data, left }) => {
|
||||||
|
const result = decode(data);
|
||||||
|
expect(labels(result)).toEqual([
|
||||||
|
"Protocol",
|
||||||
|
"Token In",
|
||||||
|
"Amount",
|
||||||
|
"Token Out",
|
||||||
|
"Paid to",
|
||||||
|
"Token moved into the router",
|
||||||
|
"Min. paid out",
|
||||||
|
"Steps",
|
||||||
|
"Deadline",
|
||||||
|
]);
|
||||||
|
expect(paidTo(result)).toEqual([STAYS_IN_ROUTER]);
|
||||||
|
expect(detail(result, "Token moved into the router")).toEqual(left);
|
||||||
|
expect(detail(result, "Min. paid out").value).toBe("1990.0000 USDC");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Uniswap's own routes: a swap paid in ETH wraps into the router what its
|
||||||
|
// swap steps then spend from it, and a route through several pools pays
|
||||||
|
// each token it buys on the way to the router and spends all of it in the
|
||||||
|
// next step (CONTRACT_BALANCE), turning ETH into WETH or back where the
|
||||||
|
// pools need it.
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
kind: "a V4 SETTLE of what its swap spends",
|
||||||
|
data: execute(
|
||||||
|
v4Swap(
|
||||||
|
settle(USDT, USDT_2000, true),
|
||||||
|
SWAP_USDT_FOR_USDC,
|
||||||
|
takeAll(USDC),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
min: "1990.0000 USDC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a V4 SETTLE of more than its swap spends, the rest taken to the user",
|
||||||
|
data: execute(
|
||||||
|
v4Swap(
|
||||||
|
settle(USDT, USDT_5000, true),
|
||||||
|
SWAP_USDT_FOR_USDC,
|
||||||
|
take(USDT, MSG_SENDER),
|
||||||
|
takeAll(USDC),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
min: "1990.0000 USDC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a WRAP_ETH into the router, then a swap of all of it from the router",
|
||||||
|
data: execute(
|
||||||
|
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||||
|
v3SwapExactIn(
|
||||||
|
MSG_SENDER,
|
||||||
|
ONE_WETH,
|
||||||
|
USDC_1990,
|
||||||
|
WETH,
|
||||||
|
USDC,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
min: "1990.0000 USDC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a WRAP_ETH into the router, then two swaps from the router that together spend all of it",
|
||||||
|
data: execute(
|
||||||
|
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||||
|
v3SwapExactIn(
|
||||||
|
MSG_SENDER,
|
||||||
|
(ONE_WETH * 3n) / 5n,
|
||||||
|
1n,
|
||||||
|
WETH,
|
||||||
|
USDC,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
v2Swap(
|
||||||
|
0x08,
|
||||||
|
MSG_SENDER,
|
||||||
|
(ONE_WETH * 2n) / 5n,
|
||||||
|
USDC_1990,
|
||||||
|
[WETH, USDC],
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
min: "1990.0000 USDC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a WRAP_ETH into the router, then a V4 swap of all of it settled from the router",
|
||||||
|
data: execute(
|
||||||
|
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||||
|
v4Swap(
|
||||||
|
swapExactInSingle(WETH, USDC, ONE_WETH, USDC_1990),
|
||||||
|
settle(WETH, 0n, false),
|
||||||
|
takeAll(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",
|
||||||
|
data: execute(
|
||||||
|
wrapEth(ADDRESS_THIS, CONTRACT_BALANCE),
|
||||||
|
v3SwapExactIn(
|
||||||
|
MSG_SENDER,
|
||||||
|
CONTRACT_BALANCE,
|
||||||
|
USDC_1990,
|
||||||
|
WETH,
|
||||||
|
USDC,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
min: "1990.0000 USDC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a swap paying WETH to the router, then a V2 swap of all of it",
|
||||||
|
data: execute(
|
||||||
|
swapToWeth(ADDRESS_THIS),
|
||||||
|
v2Swap(
|
||||||
|
0x08,
|
||||||
|
MSG_SENDER,
|
||||||
|
CONTRACT_BALANCE,
|
||||||
|
USDC_1990,
|
||||||
|
[WETH, USDC],
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
min: "1990.0000 USDC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a swap paying WETH to the router, then a V4 swap settling all of it from the router",
|
||||||
|
data: execute(
|
||||||
|
swapToWeth(ADDRESS_THIS),
|
||||||
|
v4Swap(
|
||||||
|
settle(WETH, CONTRACT_BALANCE, false),
|
||||||
|
swapExactInSingle(WETH, USDC, 0n, USDC_1990),
|
||||||
|
takeAll(USDC),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
min: "1990.0000 USDC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a V4 swap taking USDC to the router, then a V3 swap of all of it",
|
||||||
|
data: execute(
|
||||||
|
v4Swap(
|
||||||
|
SWAP_USDT_FOR_USDC,
|
||||||
|
SETTLE_USDT,
|
||||||
|
take(USDC, ADDRESS_THIS),
|
||||||
|
),
|
||||||
|
v3SwapExactIn(
|
||||||
|
MSG_SENDER,
|
||||||
|
CONTRACT_BALANCE,
|
||||||
|
ONE_WETH,
|
||||||
|
USDC,
|
||||||
|
WETH,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
min: "1.0000 WETH",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a V4 swap taking ETH to the router, a WRAP_ETH of all of it, then a V3 swap of all the WETH",
|
||||||
|
data: execute(
|
||||||
|
v4Swap(
|
||||||
|
SWAP_USDT_FOR_ETH,
|
||||||
|
SETTLE_USDT,
|
||||||
|
take(ZERO, ADDRESS_THIS),
|
||||||
|
),
|
||||||
|
wrapEth(ADDRESS_THIS, CONTRACT_BALANCE),
|
||||||
|
v3SwapExactIn(
|
||||||
|
MSG_SENDER,
|
||||||
|
CONTRACT_BALANCE,
|
||||||
|
USDC_1990,
|
||||||
|
WETH,
|
||||||
|
USDC,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
min: "1990.0000 USDC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a swap paying WETH to the router, an UNWRAP_WETH into the router, then a V4 swap settling all the ETH from the router",
|
||||||
|
data: execute(
|
||||||
|
swapToWeth(ADDRESS_THIS),
|
||||||
|
unwrapWeth(ADDRESS_THIS),
|
||||||
|
v4Swap(
|
||||||
|
settle(ZERO, CONTRACT_BALANCE, false),
|
||||||
|
swapExactInSingle(ZERO, USDC, 0n, USDC_1990),
|
||||||
|
takeAll(USDC),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
min: "1990.0000 USDC",
|
||||||
|
},
|
||||||
|
])("$kind reads as a swap paid to the user", ({ data, min }) => {
|
||||||
|
const result = decode(data);
|
||||||
|
expect(labels(result)).toEqual(PAID_TO_USER);
|
||||||
|
expect(detail(result, "Min. received").value).toBe(min);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// https://git.eeqj.de/sneak/AutistMask/issues/599: TRANSFER, PAY_PORTION,
|
// https://git.eeqj.de/sneak/AutistMask/issues/599: TRANSFER, PAY_PORTION,
|
||||||
// PAY_PORTION_FULL_PRECISION and V4 TAKE_PORTION pay a token to an address,
|
// PAY_PORTION_FULL_PRECISION and V4 TAKE_PORTION pay a token to an address,
|
||||||
// and PERMIT2_TRANSFER_FROM moves the user's own tokens to one.
|
// and PERMIT2_TRANSFER_FROM moves the user's own tokens to one.
|
||||||
|
|||||||
Reference in New Issue
Block a user