Compare commits
2
Commits
d842d000e8
...
d34fb1976f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d34fb1976f | ||
|
|
4109c4d44a |
No files matched your search
@@ -1253,14 +1253,24 @@ Decoding). So a step whose USDT-for-USDC swap action could not be read, with a
|
|||||||
A V4 step with no swap action sets only the sides its `SETTLE`, `SETTLE_ALL`,
|
A V4 step with no swap action sets only the sides its `SETTLE`, `SETTLE_ALL`,
|
||||||
`TAKE` or `TAKE_ALL` names.
|
`TAKE` or `TAKE_ALL` names.
|
||||||
|
|
||||||
An `UNWRAP_WETH` step makes `Token Out` ETH only when the output side is WETH,
|
An `UNWRAP_WETH` step makes `Token Out` ETH only when no step set the output
|
||||||
on mainnet or Sepolia, or when no step set the output side; a WETH
|
side, or when the output side is WETH, on mainnet or Sepolia, that the step that
|
||||||
`Min. received` figure then reads in ETH. Otherwise `Token Out` and
|
set it paid to the router and that the router still holds when the `UNWRAP_WETH`
|
||||||
`Min. received` keep the output side's own token and figure, whether the swap
|
runs (Transaction Decoding below gives what the router holds). The router
|
||||||
was paid in ETH or in a token: a V2 exact-out swap that buys USDC and then
|
unwraps only the WETH it holds, so WETH that step paid to the user, or to anyone
|
||||||
unwraps the WETH it did not spend shows USDC. The token permission warning on
|
else, stays WETH: a swap that pays the router WETH, then a swap that pays the
|
||||||
the signature screen has its own amount wording, including `Unknown`; the
|
user WETH, then an unwrap, shows WETH with the second swap's minimum. The
|
||||||
SignApproval section below describes it.
|
minimum reads in ETH when that step paid all its WETH to the router. When it
|
||||||
|
paid only part of it to the router, as a V4 step does that `TAKE`s some of its
|
||||||
|
WETH to the user and the rest to the router, or paid the router WETH beside a
|
||||||
|
payout the decoder cannot read, the swap shows ETH with no minimum: the decoder
|
||||||
|
does not count how much of that step's minimum reaches the router, nor read the
|
||||||
|
`UNWRAP_WETH`'s own minimum. A later step that sets the output side sets it
|
||||||
|
anew. Otherwise `Token Out` and `Min. received` keep the output side's own token
|
||||||
|
and figure, whether the swap was paid in ETH or in a token: a V2 exact-out swap
|
||||||
|
that buys USDC and then unwraps the WETH it did not spend shows USDC. The token
|
||||||
|
permission warning on the signature screen has its own amount wording, including
|
||||||
|
`Unknown`; the SignApproval section below describes it.
|
||||||
|
|
||||||
The swap's `Deadline` line is the router's deadline as a UTC date and time, e.g.
|
The swap's `Deadline` line is the router's deadline as a UTC date and time, e.g.
|
||||||
`2026-02-27 08:25:51`. A JavaScript date reaches only to 275760-09-13 00:00:00
|
`2026-02-27 08:25:51`. A JavaScript date reaches only to 275760-09-13 00:00:00
|
||||||
@@ -1989,7 +1999,11 @@ view would leave a wallet one click from deletion.
|
|||||||
- Type: transaction classification — one of: Native ETH Transfer, ERC-20
|
- Type: transaction classification — one of: Native ETH Transfer, ERC-20
|
||||||
Token Transfer, Swap, Token Approval, Contract Call, Contract Creation. A
|
Token Transfer, Swap, Token Approval, Contract Call, Contract Creation. A
|
||||||
transfer with a token contract is an ERC-20 Token Transfer whatever symbol
|
transfer with a token contract is an ERC-20 Token Transfer whatever symbol
|
||||||
the token reports.
|
the token reports. A call named `execute` or `multicall` is a Swap, here
|
||||||
|
and on its history row, only when it was sent to one of the Universal
|
||||||
|
Router addresses Uniswap publishes for the network it was sent on (see
|
||||||
|
Transaction Decoding); sent to any other contract, its history row shows
|
||||||
|
the method name and this line reads Contract Call.
|
||||||
- Status: "Success" or "Failed"
|
- Status: "Success" or "Failed"
|
||||||
- From: blockie + color dot + full address (tap to copy) + etherscan link;
|
- From: blockie + color dot + full address (tap to copy) + etherscan link;
|
||||||
ENS name if available
|
ENS name if available
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const { toDecimals } = require("./transferAmount");
|
|||||||
// truncation without the approval screens' nonzero floor: the transaction
|
// truncation without the approval screens' nonzero floor: the transaction
|
||||||
// detail view is the authoritative record and already shows exact precision.
|
// detail view is the authoritative record and already shows exact precision.
|
||||||
const { truncateAmount: formatTxValue } = require("./amountDisplay");
|
const { truncateAmount: formatTxValue } = require("./amountDisplay");
|
||||||
|
const { routerRelease } = require("./uniswap");
|
||||||
|
|
||||||
// Ethereum addresses are case-insensitive: EIP-55 mixed case is a checksum
|
// Ethereum addresses are case-insensitive: EIP-55 mixed case is a checksum
|
||||||
// over the address, not part of its identity. Every address comparison in
|
// over the address, not part of its identity. Every address comparison in
|
||||||
@@ -51,7 +52,6 @@ function parseTx(tx, addrLower, chainId) {
|
|||||||
}
|
}
|
||||||
// Map known DEX methods to "Swap" for cleaner display
|
// Map known DEX methods to "Swap" for cleaner display
|
||||||
const SWAP_METHODS = new Set([
|
const SWAP_METHODS = new Set([
|
||||||
"execute",
|
|
||||||
"swap",
|
"swap",
|
||||||
"swapExactTokensForTokens",
|
"swapExactTokensForTokens",
|
||||||
"swapTokensForExactTokens",
|
"swapTokensForExactTokens",
|
||||||
@@ -59,9 +59,16 @@ function parseTx(tx, addrLower, chainId) {
|
|||||||
"swapTokensForExactETH",
|
"swapTokensForExactETH",
|
||||||
"swapExactTokensForETH",
|
"swapExactTokensForETH",
|
||||||
"swapETHForExactTokens",
|
"swapETHForExactTokens",
|
||||||
"multicall",
|
|
||||||
]);
|
]);
|
||||||
const label = SWAP_METHODS.has(method)
|
// Multisigs, smart accounts and many other contracts also have an
|
||||||
|
// `execute` or `multicall` method, so either is a swap only when sent
|
||||||
|
// to a Universal Router on this transaction's network: the same
|
||||||
|
// addresses the approval screen decodes a swap for.
|
||||||
|
const isSwap =
|
||||||
|
SWAP_METHODS.has(method) ||
|
||||||
|
((method === "execute" || method === "multicall") &&
|
||||||
|
routerRelease(to, chainId) !== null);
|
||||||
|
const label = isSwap
|
||||||
? "Swap"
|
? "Swap"
|
||||||
: method.charAt(0).toUpperCase() + method.slice(1);
|
: method.charAt(0).toUpperCase() + method.slice(1);
|
||||||
direction = "contract";
|
direction = "contract";
|
||||||
|
|||||||
+60
-16
@@ -1219,6 +1219,16 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
let minOutput = null;
|
let minOutput = null;
|
||||||
let outputEstablished = false;
|
let outputEstablished = false;
|
||||||
let hasUnwrapWeth = false;
|
let hasUnwrapWeth = false;
|
||||||
|
// Set when an UNWRAP_WETH pays out, as ETH, the output side's WETH,
|
||||||
|
// which the router holds; cleared whenever the output side is set
|
||||||
|
// again.
|
||||||
|
let outputUnwrapped = false;
|
||||||
|
// Where the step that set the output side paid its output: the
|
||||||
|
// recipient of a swap step, as written, or of each TAKE, TAKE_ALL and
|
||||||
|
// TAKE_PORTION of it in a V4 step, a TAKE_ALL as paid to the sender,
|
||||||
|
// with null for a payout of that step this decoder did not read. A
|
||||||
|
// BALANCE_CHECK_ERC20 pays nobody.
|
||||||
|
let outputRecipients = [];
|
||||||
let hasV2OrV3ExactOut = false;
|
let hasV2OrV3ExactOut = false;
|
||||||
let swapUndecoded = false;
|
let swapUndecoded = false;
|
||||||
let hasSubPlan = false;
|
let hasSubPlan = false;
|
||||||
@@ -1255,11 +1265,13 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
if (!present(token) && !present(amount)) return;
|
if (!present(token) && !present(amount)) return;
|
||||||
setInput(token, amount);
|
setInput(token, amount);
|
||||||
};
|
};
|
||||||
const setOutput = (token, amount) => {
|
const setOutput = (token, amount, recipients) => {
|
||||||
if (!present(token) && !present(amount)) return;
|
if (!present(token) && !present(amount)) return;
|
||||||
outputToken = present(token) ? token : null;
|
outputToken = present(token) ? token : null;
|
||||||
minOutput = present(amount) ? amount : null;
|
minOutput = present(amount) ? amount : null;
|
||||||
outputEstablished = true;
|
outputEstablished = true;
|
||||||
|
outputUnwrapped = false;
|
||||||
|
outputRecipients = recipients;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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
|
||||||
@@ -1505,7 +1517,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// does. It pays nobody.
|
// does. It pays nobody.
|
||||||
const minBalance = allowedToFail ? 0n : b.minBalance;
|
const minBalance = allowedToFail ? 0n : b.minBalance;
|
||||||
if (!(minBalance === 0n && present(minOutput))) {
|
if (!(minBalance === 0n && present(minOutput))) {
|
||||||
setOutput(b.token, minBalance);
|
setOutput(b.token, minBalance, []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (command === "V3_SWAP_EXACT_IN") {
|
} else if (command === "V3_SWAP_EXACT_IN") {
|
||||||
@@ -1519,7 +1531,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// Always update output: in multi-step swaps (V3 → V4),
|
// Always update output: in multi-step swaps (V3 → V4),
|
||||||
// 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, [s.recipient]);
|
||||||
if (!s.payerIsUser) {
|
if (!s.payerIsUser) {
|
||||||
spendFromRouter(s.tokenIn, s.amountIn);
|
spendFromRouter(s.tokenIn, s.amountIn);
|
||||||
}
|
}
|
||||||
@@ -1539,7 +1551,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
} else if (s.payerIsUser) {
|
} else if (s.payerIsUser) {
|
||||||
addUserPaid(s.tokenIn, s.amountIn);
|
addUserPaid(s.tokenIn, s.amountIn);
|
||||||
}
|
}
|
||||||
setOutput(s.tokenOut, s.amountOutMin);
|
setOutput(s.tokenOut, s.amountOutMin, [s.recipient]);
|
||||||
if (!s.payerIsUser) {
|
if (!s.payerIsUser) {
|
||||||
spendFromRouter(s.tokenIn, s.amountIn);
|
spendFromRouter(s.tokenIn, s.amountIn);
|
||||||
}
|
}
|
||||||
@@ -1570,7 +1582,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
} else if (s.payerIsUser) {
|
} else if (s.payerIsUser) {
|
||||||
addUserPaid(s.tokenIn, s.amountInMax);
|
addUserPaid(s.tokenIn, s.amountInMax);
|
||||||
}
|
}
|
||||||
setOutput(s.tokenOut, s.amountOut);
|
setOutput(s.tokenOut, s.amountOut, [s.recipient]);
|
||||||
if (!isHop(i, s.tokenOut, s.recipient)) {
|
if (!isHop(i, s.tokenOut, s.recipient)) {
|
||||||
addPayee(s.recipient, s.tokenOut);
|
addPayee(s.recipient, s.tokenOut);
|
||||||
}
|
}
|
||||||
@@ -1728,6 +1740,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
(p) => p.currency === v4.tokenIn,
|
(p) => p.currency === v4.tokenIn,
|
||||||
);
|
);
|
||||||
const amountIn = paidIn ? paidIn.amount : v4?.amountIn;
|
const amountIn = paidIn ? paidIn.amount : v4?.amountIn;
|
||||||
|
// Where the step pays its output, if it sets the output
|
||||||
|
// side (see outputRecipients).
|
||||||
|
const takeRecipients = v4
|
||||||
|
? v4.takes
|
||||||
|
.filter((t) => t.currency === v4.tokenOut)
|
||||||
|
.map((t) => t.recipient)
|
||||||
|
.concat(v4.recipientUnread ? [null] : [])
|
||||||
|
: [];
|
||||||
if (v4 && v4.hasSwapAction) {
|
if (v4 && v4.hasSwapAction) {
|
||||||
// A step with a swap action always sets the output
|
// A step with a swap action always sets the output
|
||||||
// side, as the last swap step wins, and sets the
|
// side, as the last swap step wins, and sets the
|
||||||
@@ -1744,13 +1764,15 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
outputToken = v4.tokenOut;
|
outputToken = v4.tokenOut;
|
||||||
minOutput = v4.amountOutMin;
|
minOutput = v4.amountOutMin;
|
||||||
outputEstablished = true;
|
outputEstablished = true;
|
||||||
|
outputUnwrapped = false;
|
||||||
|
outputRecipients = takeRecipients;
|
||||||
} else if (v4) {
|
} else if (v4) {
|
||||||
// A step with no swap action sets only the sides its
|
// A step with no swap action sets only the sides its
|
||||||
// SETTLE, SETTLE_ALL, TAKE or TAKE_ALL names:
|
// SETTLE, SETTLE_ALL, TAKE or TAKE_ALL names:
|
||||||
// setInputOnce() and setOutput() leave alone a side
|
// setInputOnce() and setOutput() leave alone a side
|
||||||
// whose two halves are both unstated.
|
// whose two halves are both unstated.
|
||||||
setInputOnce(v4.tokenIn, amountIn);
|
setInputOnce(v4.tokenIn, amountIn);
|
||||||
setOutput(v4.tokenOut, v4.amountOutMin);
|
setOutput(v4.tokenOut, v4.amountOutMin, takeRecipients);
|
||||||
}
|
}
|
||||||
// Every V4 step, with a swap action or without, takes
|
// Every V4 step, with a swap action or without, takes
|
||||||
// from the sender what the user pays it (an amount an
|
// from the sender what the user pays it (an amount an
|
||||||
@@ -1772,6 +1794,29 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
}
|
}
|
||||||
} else if (command === "UNWRAP_WETH") {
|
} else if (command === "UNWRAP_WETH") {
|
||||||
hasUnwrapWeth = true;
|
hasUnwrapWeth = true;
|
||||||
|
// The router unwraps all the WETH it holds, even when the
|
||||||
|
// recipient cannot be read. The output side's WETH is
|
||||||
|
// among it when the step that set the output side paid it
|
||||||
|
// to the router and the router still holds WETH. WETH
|
||||||
|
// that step paid to the user, or to anyone else, stays
|
||||||
|
// WETH, so when that step also paid some of its output
|
||||||
|
// elsewhere, or made a payout this decoder did not read,
|
||||||
|
// only part of it may be unwrapped. This decoder does not
|
||||||
|
// count how much of that step's minimum then reaches the
|
||||||
|
// router, nor read the UNWRAP_WETH's own minimum, so the
|
||||||
|
// output side keeps no figure.
|
||||||
|
const toRouter = (r) => present(r) && isRouter(r);
|
||||||
|
if (
|
||||||
|
present(outputToken) &&
|
||||||
|
WETH_ADDRESSES.includes(outputToken.toLowerCase()) &&
|
||||||
|
heldByRouter.has(outputToken.toLowerCase()) &&
|
||||||
|
outputRecipients.some(toRouter)
|
||||||
|
) {
|
||||||
|
outputUnwrapped = true;
|
||||||
|
if (!outputRecipients.every(toRouter)) {
|
||||||
|
minOutput = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
const u = decodeUnwrapWeth(inputs[i]);
|
const u = decodeUnwrapWeth(inputs[i]);
|
||||||
if (u) {
|
if (u) {
|
||||||
addPayee(u.recipient);
|
addPayee(u.recipient);
|
||||||
@@ -1912,23 +1957,22 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
outputToken = null;
|
outputToken = null;
|
||||||
minOutput = null;
|
minOutput = null;
|
||||||
outputEstablished = true;
|
outputEstablished = true;
|
||||||
|
outputUnwrapped = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve token info. A null token on either side means no step named
|
// Resolve token info. A null token on either side means no step named
|
||||||
// a currency for it, a V4 step with a swap action left it unknown, or
|
// a currency for it, a V4 step with a swap action left it unknown, or
|
||||||
// an EXECUTE_SUB_PLAN step did; tokenInfo() refuses rather than calling
|
// an EXECUTE_SUB_PLAN step did; tokenInfo() refuses rather than calling
|
||||||
// it ETH. UNWRAP_WETH turns WETH into ETH, so it makes the output ETH
|
// it ETH. UNWRAP_WETH turns WETH into ETH, so it makes the output ETH
|
||||||
// when the output side is WETH, or when no step set the output side. A
|
// when it unwraps the output side's WETH (`outputUnwrapped`), or when
|
||||||
// V4 step with a swap action, or an EXECUTE_SUB_PLAN step, did set it,
|
// no step set the output side. A V4 step with a swap action, or
|
||||||
// even when it set it unknown. Any other output keeps its own token
|
// an EXECUTE_SUB_PLAN step, did set it, even when it set it unknown.
|
||||||
// and figure: a swap that buys USDC and then unwraps the WETH it did
|
// Any other output keeps its own token and figure: a swap that pays
|
||||||
// not spend receives USDC.
|
// its WETH to the user and then unwraps receives WETH, and one that
|
||||||
const outputIsWeth =
|
// buys USDC and then unwraps the WETH it did not spend receives USDC.
|
||||||
present(outputToken) &&
|
|
||||||
WETH_ADDRESSES.includes(outputToken.toLowerCase());
|
|
||||||
const inInfo = tokenInfo(inputToken, sources);
|
const inInfo = tokenInfo(inputToken, sources);
|
||||||
const outInfo =
|
const outInfo =
|
||||||
hasUnwrapWeth && (outputIsWeth || !outputEstablished)
|
outputUnwrapped || (hasUnwrapWeth && !outputEstablished)
|
||||||
? { symbol: "ETH", decimals: 18, address: null }
|
? { symbol: "ETH", decimals: 18, address: null }
|
||||||
: tokenInfo(outputToken, sources);
|
: tokenInfo(outputToken, sources);
|
||||||
|
|
||||||
@@ -2193,4 +2237,4 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { decode };
|
module.exports = { decode, routerRelease };
|
||||||
@@ -875,7 +875,7 @@ describe("uniswap decoder", () => {
|
|||||||
const data = buildExecute(
|
const data = buildExecute(
|
||||||
solidityPacked(["uint8", "uint8"], [0x08, 0x0c]),
|
solidityPacked(["uint8", "uint8"], [0x08, 0x0c]),
|
||||||
[
|
[
|
||||||
encodeV2SwapExactIn(USER_ADDR, 1000000n, 500000000000000n, [
|
encodeV2SwapExactIn(ROUTER_ADDR, 1000000n, 500000000000000n, [
|
||||||
USDC_ADDR,
|
USDC_ADDR,
|
||||||
SEPOLIA_WETH_ADDR,
|
SEPOLIA_WETH_ADDR,
|
||||||
]),
|
]),
|
||||||
@@ -958,7 +958,7 @@ describe("uniswap decoder", () => {
|
|||||||
const data = buildExecute(
|
const data = buildExecute(
|
||||||
solidityPacked(["uint8", "uint8"], [0x08, 0x0c]),
|
solidityPacked(["uint8", "uint8"], [0x08, 0x0c]),
|
||||||
[
|
[
|
||||||
encodeV2SwapExactIn(USER_ADDR, 1000000n, 500000000000000n, [
|
encodeV2SwapExactIn(ROUTER_ADDR, 1000000n, 500000000000000n, [
|
||||||
USDT_ADDR,
|
USDT_ADDR,
|
||||||
WETH_ADDR,
|
WETH_ADDR,
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/592), 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), any token a swap
|
// (https://git.eeqj.de/sneak/AutistMask/issues/600), any token a swap
|
||||||
// leaves in the router (https://git.eeqj.de/sneak/AutistMask/issues/606), and
|
// leaves in the router (https://git.eeqj.de/sneak/AutistMask/issues/606),
|
||||||
// what each PERMIT2_TRANSFER_FROM moves
|
// what each PERMIT2_TRANSFER_FROM moves
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/611).
|
// (https://git.eeqj.de/sneak/AutistMask/issues/611), and when an
|
||||||
|
// UNWRAP_WETH makes the output ETH
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/607).
|
||||||
//
|
//
|
||||||
// Every swap command, SWEEP, WRAP_ETH, UNWRAP_WETH, TRANSFER, PAY_PORTION,
|
// 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
|
||||||
@@ -268,18 +270,28 @@ const SWAP_USDT_FOR_ETH = [
|
|||||||
[[[ZERO, USDT, 500, 10, ZERO], false, USDT_2000, ONE_WETH, "0x"]],
|
[[[ZERO, USDT, 500, 10, ZERO], false, USDT_2000, ONE_WETH, "0x"]],
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
const SWAP_USDT_FOR_WETH = [
|
||||||
|
0x06, // SWAP_EXACT_IN_SINGLE
|
||||||
|
coder.encode(
|
||||||
|
[
|
||||||
|
"tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
|
||||||
|
],
|
||||||
|
[[[USDT, WETH, 500, 10, ZERO], true, USDT_2000, ONE_WETH, "0x"]],
|
||||||
|
),
|
||||||
|
];
|
||||||
// SWAP_EXACT_IN_SINGLE with no parameters, which cannot be read.
|
// SWAP_EXACT_IN_SINGLE with no parameters, which cannot be read.
|
||||||
const UNREADABLE_SWAP = [0x06, "0x"];
|
const UNREADABLE_SWAP = [0x06, "0x"];
|
||||||
const SETTLE_USDT = [
|
const SETTLE_USDT = [
|
||||||
0x0b,
|
0x0b,
|
||||||
coder.encode(["address", "uint256", "bool"], [USDT, 0n, true]),
|
coder.encode(["address", "uint256", "bool"], [USDT, 0n, true]),
|
||||||
];
|
];
|
||||||
function take(currency, recipient) {
|
// A TAKE of `amount`, by default zero: the open delta, all the step owes.
|
||||||
|
function take(currency, recipient, amount = 0n) {
|
||||||
return [
|
return [
|
||||||
0x0e,
|
0x0e,
|
||||||
coder.encode(
|
coder.encode(
|
||||||
["address", "address", "uint256"],
|
["address", "address", "uint256"],
|
||||||
[currency, recipient, 0n],
|
[currency, recipient, amount],
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -531,7 +543,7 @@ describe("an output left in the router is not received", () => {
|
|||||||
{
|
{
|
||||||
kind: "an UNWRAP_WETH to the user, then the swap",
|
kind: "an UNWRAP_WETH to the user, then the swap",
|
||||||
data: execute(unwrapWeth(MSG_SENDER), toRouter),
|
data: execute(unwrapWeth(MSG_SENDER), toRouter),
|
||||||
min: "1.0000 ETH",
|
min: "1.0000 WETH",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "the swap, then a WRAP_ETH of zero to the user",
|
kind: "the swap, then a WRAP_ETH of zero to the user",
|
||||||
@@ -568,6 +580,171 @@ describe("an output left in the router is not received", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/607: an UNWRAP_WETH pays out,
|
||||||
|
// as ETH, the WETH the router holds when it runs. Token Out read ETH whenever
|
||||||
|
// the output side was WETH, so a swap that paid its WETH to the user before
|
||||||
|
// the UNWRAP_WETH read as ETH, though the user receives WETH.
|
||||||
|
describe("an UNWRAP_WETH makes the output ETH only when the router holds it", () => {
|
||||||
|
test("a swap paying its WETH to the user, then an UNWRAP_WETH, reads as WETH", () => {
|
||||||
|
const result = decode(
|
||||||
|
execute(swapToWeth(MSG_SENDER), unwrapWeth(MSG_SENDER)),
|
||||||
|
);
|
||||||
|
expect(result.name).toBe("Swap USDT → WETH");
|
||||||
|
expect(labels(result)).toEqual(PAID_TO_USER);
|
||||||
|
expect(detail(result, "Min. received").value).toBe("1.0000 WETH");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a swap paying its WETH to the router, then an UNWRAP_WETH to the user, reads as ETH", () => {
|
||||||
|
const result = decode(
|
||||||
|
execute(swapToWeth(ADDRESS_THIS), unwrapWeth(MSG_SENDER)),
|
||||||
|
);
|
||||||
|
expect(result.name).toBe("Swap USDT → ETH");
|
||||||
|
expect(labels(result)).toEqual(PAID_TO_USER);
|
||||||
|
expect(detail(result, "Min. received").value).toBe("1.0000 ETH");
|
||||||
|
});
|
||||||
|
|
||||||
|
// The SWEEP pays the user all the WETH the router holds, so the
|
||||||
|
// UNWRAP_WETH finds none of it.
|
||||||
|
test("a swap paying its WETH to the router, then a SWEEP of it to the user, then an UNWRAP_WETH, reads as WETH", () => {
|
||||||
|
const result = decode(
|
||||||
|
execute(
|
||||||
|
swapToWeth(ADDRESS_THIS),
|
||||||
|
sweep(WETH, MSG_SENDER),
|
||||||
|
unwrapWeth(MSG_SENDER),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(result.name).toBe("Swap USDT → WETH");
|
||||||
|
expect(detail(result, "Min. received").value).toBe("1.0000 WETH");
|
||||||
|
});
|
||||||
|
|
||||||
|
// The user receives the first step's WETH as WETH and the second's as
|
||||||
|
// ETH. The second step sets the output side and pays its WETH to the
|
||||||
|
// router, so the output reads as ETH with the second step's minimum.
|
||||||
|
test("two swaps, the second paying its WETH to the router, then an UNWRAP_WETH, reads as ETH", () => {
|
||||||
|
const result = decode(
|
||||||
|
execute(
|
||||||
|
swapToWeth(MSG_SENDER),
|
||||||
|
v2Swap(0x08, ADDRESS_THIS, USDC_1990, ONE_WETH / 2n, [
|
||||||
|
USDC,
|
||||||
|
WETH,
|
||||||
|
]),
|
||||||
|
unwrapWeth(MSG_SENDER),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(result.name).toBe("Swap USDT → ETH");
|
||||||
|
expect(detail(result, "Min. received").value).toBe("0.5000 ETH");
|
||||||
|
});
|
||||||
|
|
||||||
|
// The same two swaps the other way round. The user receives the first
|
||||||
|
// step's WETH as ETH and the second's as WETH. The second step sets the
|
||||||
|
// output side and pays its WETH to the user, so the output reads as WETH
|
||||||
|
// with the second step's minimum, though the router holds the first's.
|
||||||
|
test("two swaps, the second paying its WETH to the user, then an UNWRAP_WETH, reads as WETH", () => {
|
||||||
|
const result = decode(
|
||||||
|
execute(
|
||||||
|
v2Swap(0x08, ADDRESS_THIS, USDC_1990, ONE_WETH / 2n, [
|
||||||
|
USDC,
|
||||||
|
WETH,
|
||||||
|
]),
|
||||||
|
swapToWeth(MSG_SENDER),
|
||||||
|
unwrapWeth(MSG_SENDER),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(result.name).toBe("Swap USDC → WETH");
|
||||||
|
expect(detail(result, "Min. received").value).toBe("1.0000 WETH");
|
||||||
|
});
|
||||||
|
|
||||||
|
// One V4 step buys at least 1 WETH and TAKEs 0.6 WETH to the user and the
|
||||||
|
// rest to the router. The user receives 0.6 WETH and the rest as ETH.
|
||||||
|
// Neither minimum is shown: the decoder does not count how much of the
|
||||||
|
// step's minimum reaches the router, nor read the UNWRAP_WETH's own.
|
||||||
|
test("one V4 step paying part of its WETH to the user and the rest to the router, then an UNWRAP_WETH, reads as ETH with no minimum", () => {
|
||||||
|
const result = decode(
|
||||||
|
execute(
|
||||||
|
v4Swap(
|
||||||
|
SWAP_USDT_FOR_WETH,
|
||||||
|
SETTLE_USDT,
|
||||||
|
take(WETH, MSG_SENDER, (ONE_WETH * 6n) / 10n),
|
||||||
|
take(WETH, ADDRESS_THIS),
|
||||||
|
),
|
||||||
|
unwrapWeth(MSG_SENDER),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(result.name).toBe("Swap USDT → ETH");
|
||||||
|
expect(labels(result)).toEqual([
|
||||||
|
"Protocol",
|
||||||
|
"Token In",
|
||||||
|
"Amount",
|
||||||
|
"Token Out",
|
||||||
|
"Steps",
|
||||||
|
"Deadline",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// A TAKE the decoder cannot read may pay part of the WETH to the user.
|
||||||
|
test("one V4 step paying WETH to the router beside a TAKE that cannot be read, then an UNWRAP_WETH, reads as ETH with no minimum", () => {
|
||||||
|
const result = decode(
|
||||||
|
execute(
|
||||||
|
v4Swap(
|
||||||
|
SWAP_USDT_FOR_WETH,
|
||||||
|
SETTLE_USDT,
|
||||||
|
withDirtyWord(take(WETH, MSG_SENDER), 1),
|
||||||
|
take(WETH, ADDRESS_THIS),
|
||||||
|
),
|
||||||
|
unwrapWeth(MSG_SENDER),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(result.name).toBe("Swap USDT → ETH");
|
||||||
|
expect(detail(result, "Min. received")).toBeUndefined();
|
||||||
|
expect(detail(result, "Min. paid out")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// A step after the UNWRAP_WETH that sets the output side sets it anew.
|
||||||
|
test("a swap paying WETH to the user after an UNWRAP_WETH of an earlier swap's WETH reads as WETH", () => {
|
||||||
|
const result = decode(
|
||||||
|
execute(
|
||||||
|
swapToWeth(ADDRESS_THIS),
|
||||||
|
unwrapWeth(MSG_SENDER),
|
||||||
|
v2Swap(0x08, MSG_SENDER, USDC_1990, ONE_WETH / 2n, [
|
||||||
|
USDC,
|
||||||
|
WETH,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(result.name).toBe("Swap USDT → WETH");
|
||||||
|
expect(detail(result, "Min. received").value).toBe("0.5000 WETH");
|
||||||
|
});
|
||||||
|
|
||||||
|
// The EXECUTE_SUB_PLAN's own steps, which the decoder does not read, buy
|
||||||
|
// the user USDC as well, so Token Out is not named.
|
||||||
|
test("a swap paying its WETH to the router, then an UNWRAP_WETH, then an EXECUTE_SUB_PLAN, names no Token Out", () => {
|
||||||
|
const subPlan = [
|
||||||
|
0x21,
|
||||||
|
coder.encode(
|
||||||
|
["bytes", "bytes[]"],
|
||||||
|
[
|
||||||
|
"0x00",
|
||||||
|
[
|
||||||
|
v3SwapExactIn(
|
||||||
|
MSG_SENDER,
|
||||||
|
USDT_2000,
|
||||||
|
USDC_1990,
|
||||||
|
USDT,
|
||||||
|
USDC,
|
||||||
|
)[1],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
const result = decode(
|
||||||
|
execute(swapToWeth(ADDRESS_THIS), unwrapWeth(MSG_SENDER), subPlan),
|
||||||
|
);
|
||||||
|
expect(result.name).toBe("Uniswap Swap");
|
||||||
|
expect(detail(result, "Token Out").value).toBe(UNDECODED);
|
||||||
|
expect(detail(result, "Min. received")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// https://git.eeqj.de/sneak/AutistMask/issues/606: any token a step pays the
|
// 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
|
// 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
|
// only the swap's output. A V4 SETTLE can take more of the user's tokens than
|
||||||
|
|||||||
@@ -7,6 +7,12 @@
|
|||||||
// only when the transaction is sent to a Universal Router deployment on the
|
// only when the transaction is sent to a Universal Router deployment on the
|
||||||
// network it is for; anywhere else it is left to the raw data.
|
// network it is for; anywhere else it is left to the raw data.
|
||||||
//
|
//
|
||||||
|
// The transaction history labelled any call named `execute` or `multicall` a
|
||||||
|
// Swap, whatever address it went to
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/580). It now does so only for
|
||||||
|
// such a call sent to a Universal Router deployment on the network the
|
||||||
|
// transaction is on; any other is labelled by its method name.
|
||||||
|
//
|
||||||
// The transaction detail screen and the approval screen are driven against a
|
// The transaction detail screen and the approval screen are driven against a
|
||||||
// minimal DOM stub in the shape tests/tokenTransferDetail.test.js uses, the
|
// minimal DOM stub in the shape tests/tokenTransferDetail.test.js uses, the
|
||||||
// approval screen opened the way tests/swapStatusAmount.test.js opens it.
|
// approval screen opened the way tests/swapStatusAmount.test.js opens it.
|
||||||
@@ -33,6 +39,7 @@ globalThis.chrome = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { state } = require("../src/shared/state");
|
const { state } = require("../src/shared/state");
|
||||||
|
const { fetchRecentTransactions } = require("../src/shared/transactions");
|
||||||
const approval = require("../src/popup/views/approval");
|
const approval = require("../src/popup/views/approval");
|
||||||
const { decodeCalldata } = approval;
|
const { decodeCalldata } = approval;
|
||||||
const transactionDetail = require("../src/popup/views/transactionDetail");
|
const transactionDetail = require("../src/popup/views/transactionDetail");
|
||||||
@@ -47,6 +54,8 @@ const SEPOLIA_ROUTER = "0x3a9d48ab9751398bbfa63ad67599bb04e4bdf98b";
|
|||||||
const ROUTER_ON_BOTH = "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD";
|
const ROUTER_ON_BOTH = "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD";
|
||||||
// The address with no code from the issue.
|
// The address with no code from the issue.
|
||||||
const ORDINARY = "0x0b0050Da68435b3EcBC236c40F16dB2070bCD79A";
|
const ORDINARY = "0x0b0050Da68435b3EcBC236c40F16dB2070bCD79A";
|
||||||
|
// A contract that is not a router, such as a multisig.
|
||||||
|
const OTHER_CONTRACT = "0x5afe000000000000000000000000000000005afe";
|
||||||
|
|
||||||
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
||||||
const SEPOLIA_WETH = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14";
|
const SEPOLIA_WETH = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14";
|
||||||
@@ -129,8 +138,13 @@ describe("execute() calldata on the approval screen's decoder", () => {
|
|||||||
// The transaction the explorer reports: SWAP_DATA, sent to `mockSentTo`.
|
// The transaction the explorer reports: SWAP_DATA, sent to `mockSentTo`.
|
||||||
const TX_HASH = "0x" + "5".repeat(64);
|
const TX_HASH = "0x" + "5".repeat(64);
|
||||||
let mockSentTo = null;
|
let mockSentTo = null;
|
||||||
|
// The transactions the explorer reports for RECIPIENT's history.
|
||||||
|
let mockHistory = [];
|
||||||
|
|
||||||
function mockExplorerAnswer(url) {
|
function mockExplorerAnswer(url) {
|
||||||
|
if (url.endsWith("/addresses/" + RECIPIENT + "/transactions")) {
|
||||||
|
return { items: mockHistory };
|
||||||
|
}
|
||||||
if (url.endsWith("/transactions/" + TX_HASH)) {
|
if (url.endsWith("/transactions/" + TX_HASH)) {
|
||||||
return {
|
return {
|
||||||
to: { hash: mockSentTo },
|
to: { hash: mockSentTo },
|
||||||
@@ -187,10 +201,9 @@ function node(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Open the transaction detail screen with `activeNetworkId` active, for the
|
// Open the transaction detail screen with `activeNetworkId` active, for the
|
||||||
// Sepolia transaction the explorer reports sent to `sentTo`, and wait for its
|
// history row `row`, and wait for its decoded details.
|
||||||
// decoded details.
|
async function showDetail(row, activeNetworkId) {
|
||||||
async function showDetailSentTo(sentTo, activeNetworkId) {
|
mockSentTo = row.to;
|
||||||
mockSentTo = sentTo;
|
|
||||||
globalThis.document = makeDocument();
|
globalThis.document = makeDocument();
|
||||||
globalThis.window = { location: { search: "" } };
|
globalThis.window = { location: { search: "" } };
|
||||||
state.networkId = activeNetworkId;
|
state.networkId = activeNetworkId;
|
||||||
@@ -198,7 +211,16 @@ async function showDetailSentTo(sentTo, activeNetworkId) {
|
|||||||
state.viewData = {};
|
state.viewData = {};
|
||||||
state.viewStack = [];
|
state.viewStack = [];
|
||||||
state.currentView = null;
|
state.currentView = null;
|
||||||
transactionDetail.show({
|
transactionDetail.show(row);
|
||||||
|
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the transaction detail screen with `activeNetworkId` active, for the
|
||||||
|
// Sepolia transaction the explorer reports sent to `sentTo`, and wait for its
|
||||||
|
// decoded details.
|
||||||
|
async function showDetailSentTo(sentTo, activeNetworkId) {
|
||||||
|
await showDetail(
|
||||||
|
{
|
||||||
hash: TX_HASH,
|
hash: TX_HASH,
|
||||||
from: RECIPIENT,
|
from: RECIPIENT,
|
||||||
to: sentTo,
|
to: sentTo,
|
||||||
@@ -210,8 +232,9 @@ async function showDetailSentTo(sentTo, activeNetworkId) {
|
|||||||
directionLabel: "Swap",
|
directionLabel: "Swap",
|
||||||
method: "execute",
|
method: "execute",
|
||||||
chainId: SEPOLIA,
|
chainId: SEPOLIA,
|
||||||
});
|
},
|
||||||
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
activeNetworkId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("execute() calldata on the transaction detail screen", () => {
|
describe("execute() calldata on the transaction detail screen", () => {
|
||||||
@@ -245,6 +268,58 @@ describe("execute() calldata on the transaction detail screen", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The history row made from the explorer's answer for RECIPIENT's `method`
|
||||||
|
// call sent to the contract `sentTo` on the network with `chainId`. The
|
||||||
|
// explorer gives the address in checksummed form.
|
||||||
|
async function historyRow(method, sentTo, chainId) {
|
||||||
|
mockHistory = [
|
||||||
|
{
|
||||||
|
hash: TX_HASH,
|
||||||
|
block_number: 7000000,
|
||||||
|
timestamp: "2026-09-01T00:00:00Z",
|
||||||
|
from: { hash: RECIPIENT },
|
||||||
|
to: { hash: getAddress(sentTo), is_contract: true },
|
||||||
|
value: "0",
|
||||||
|
method,
|
||||||
|
status: "ok",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const rows = await fetchRecentTransactions(
|
||||||
|
RECIPIENT,
|
||||||
|
"https://explorer.example/api/v2",
|
||||||
|
chainId,
|
||||||
|
);
|
||||||
|
return rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The history row's label and TransactionDetail's Type line, for a Sepolia
|
||||||
|
// transaction. The active network does not decide either.
|
||||||
|
describe.each([
|
||||||
|
["execute", "Execute"],
|
||||||
|
["multicall", "Multicall"],
|
||||||
|
])("a %s() call in the history", (method, methodLabel) => {
|
||||||
|
test("sent to a router on its network, it is a Swap", async () => {
|
||||||
|
const row = await historyRow(method, SEPOLIA_ROUTER, SEPOLIA);
|
||||||
|
expect(row.directionLabel).toBe("Swap");
|
||||||
|
await showDetail(row, "mainnet");
|
||||||
|
expect(node("tx-detail-type").textContent).toBe("Swap");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sent to another contract, it is labelled by its method name", async () => {
|
||||||
|
const row = await historyRow(method, OTHER_CONTRACT, SEPOLIA);
|
||||||
|
expect(row.directionLabel).toBe(methodLabel);
|
||||||
|
await showDetail(row, "sepolia");
|
||||||
|
expect(node("tx-detail-type").textContent).toBe("Contract Call");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sent to a router of the other network only, it is labelled by its method name", async () => {
|
||||||
|
const row = await historyRow(method, MAINNET_ROUTER, SEPOLIA);
|
||||||
|
expect(row.directionLabel).toBe(methodLabel);
|
||||||
|
await showDetail(row, "mainnet");
|
||||||
|
expect(node("tx-detail-type").textContent).toBe("Contract Call");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Open the approval screen with Ethereum Mainnet active, for a site's Sepolia
|
// Open the approval screen with Ethereum Mainnet active, for a site's Sepolia
|
||||||
// transaction that sends SWAP_DATA to `sentTo`.
|
// transaction that sends SWAP_DATA to `sentTo`.
|
||||||
async function showApprovalSentTo(sentTo) {
|
async function showApprovalSentTo(sentTo) {
|
||||||
|
|||||||
Reference in new issue
Block a user