Compare commits
2
Commits
acb2d00b5a
...
5b8fc1e738
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b8fc1e738 | ||
|
|
f8b050f501 |
@@ -1195,9 +1195,10 @@ read:
|
|||||||
The swap's `Token In` and `Token Out` lines name a currency, not an amount; each
|
The swap's `Token In` and `Token Out` lines name a currency, not an amount; each
|
||||||
reads `Unknown (not named in the calldata)` when the decoder found no token for
|
reads `Unknown (not named in the calldata)` when the decoder found no token for
|
||||||
that side, or `Unknown (could not be decoded)` instead when a V4 swap action in
|
that side, or `Unknown (could not be decoded)` instead when a V4 swap action in
|
||||||
the transaction could not be read (see Transaction Decoding below). A V4 step
|
the transaction could not be read or the transaction has an `EXECUTE_SUB_PLAN`
|
||||||
with a swap action always sets the output side, and sets the input side only
|
step, which leaves both sides unknown (see Transaction Decoding below). A V4
|
||||||
when no earlier step set it. It finds each side by three rules:
|
step with a swap action always sets the output side, and sets the input side
|
||||||
|
only when no earlier step set it. It finds each side by three rules:
|
||||||
|
|
||||||
1. Its input side is the currency its `SETTLE` or `SETTLE_ALL` names, or else
|
1. Its input side is the currency its `SETTLE` or `SETTLE_ALL` names, or else
|
||||||
the one currency that some swap action in the step spends and none buys. Its
|
the one currency that some swap action in the step spends and none buys. Its
|
||||||
@@ -2892,7 +2893,18 @@ Currently supported:
|
|||||||
in the parameter shape deployed on mainnet and Sepolia, not in the newer one
|
in the parameter shape deployed on mainnet and Sepolia, not in the newer one
|
||||||
on `v4-periphery` `main`, which adds a `minHopPriceX36` field. An action in
|
on `v4-periphery` `main`, which adds a `minHopPriceX36` field. An action in
|
||||||
any other shape is not read, and its step is listed as
|
any other shape is not read, and its step is listed as
|
||||||
`V4 Swap (could not be decoded)`.
|
`V4 Swap (could not be decoded)`. Each step is named by its command type,
|
||||||
|
which the decoder reads from the command byte with the mask the router at that
|
||||||
|
address uses: `0x1f` for `UniversalRouterV1`, `0x3f` for
|
||||||
|
`UniversalRouterV1_2_V2Support` and `UniversalRouterV2`, and `0x7f` for
|
||||||
|
`UniversalRouterV2_1_1` and `UniversalRouterV2_1_2`. The allow-revert flag
|
||||||
|
(`0x80`) therefore does not change a step's name. The decoder does not read
|
||||||
|
the commands inside an `EXECUTE_SUB_PLAN` (`0x21`) step, and wherever that
|
||||||
|
step stands they may spend the user's tokens or pay the user. That step is
|
||||||
|
listed as `Execute Sub-Plan (its steps are not shown)`, and whatever the other
|
||||||
|
steps state, the summary names no swap direction, `Token In` and `Token Out`
|
||||||
|
read `Unknown (could not be decoded)`, and there is no `Amount` or
|
||||||
|
`Min. received` line.
|
||||||
|
|
||||||
### Non-Goals Forever
|
### Non-Goals Forever
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -306,7 +306,14 @@ enter your password and click "Confirm" to authorize it. Message and typed-data
|
|||||||
signature requests work the same way, with a "Sign" button, and also require
|
signature requests work the same way, with a "Sign" button, and also require
|
||||||
your password.
|
your password.
|
||||||
|
|
||||||
If the requesting site's domain is on the phishing blocklist, all three approval
|
A connected site can also ask to switch the wallet's network. A popup shows the
|
||||||
|
site's origin, the current network and the requested network. Nothing changes
|
||||||
|
unless you click "Switch"; clicking "Reject" or closing the popup leaves
|
||||||
|
everything as it was. Switching works exactly as it does in Settings, and
|
||||||
|
changes the network for the whole wallet and for every site, not only for the
|
||||||
|
one that asked.
|
||||||
|
|
||||||
|
If the requesting site's domain is on the phishing blocklist, all four approval
|
||||||
screens show a red phishing warning before you decide.
|
screens show a red phishing warning before you decide.
|
||||||
|
|
||||||
You can manage site permissions in Settings. Allowed and denied sites can be
|
You can manage site permissions in Settings. Allowed and denied sites can be
|
||||||
@@ -378,7 +385,8 @@ Click the gear icon on the home screen to access settings:
|
|||||||
- **Display**: Toggle whether tracked tokens with zero balance are shown, switch
|
- **Display**: Toggle whether tracked tokens with zero balance are shown, switch
|
||||||
timestamps to UTC, and choose the theme (System, Light, or Dark).
|
timestamps to UTC, and choose the theme (System, Light, or Dark).
|
||||||
- **Network**: Switch between Ethereum Mainnet and Sepolia Testnet. Switching
|
- **Network**: Switch between Ethereum Mainnet and Sepolia Testnet. Switching
|
||||||
resets the RPC and Blockscout endpoints to that network's defaults.
|
restores the RPC and Blockscout endpoints you last used on that network, or
|
||||||
|
that network's defaults if you have not used it before.
|
||||||
- **Ethereum RPC**: Change the Ethereum node endpoint. Default is a public RPC.
|
- **Ethereum RPC**: Change the Ethereum node endpoint. Default is a public RPC.
|
||||||
You can use your own node for maximum privacy.
|
You can use your own node for maximum privacy.
|
||||||
- **Blockscout API**: Change the Blockscout instance used for token balances and
|
- **Blockscout API**: Change the Blockscout instance used for token balances and
|
||||||
|
|||||||
+74
-27
@@ -19,39 +19,54 @@ const ROUTER_IFACE = new Interface([
|
|||||||
// The Universal Router deployments on each network, by chain id, copied from
|
// The Universal Router deployments on each network, by chain id, copied from
|
||||||
// the deployment files Uniswap publishes, each address named by its key there.
|
// the deployment files Uniswap publishes, each address named by its key there.
|
||||||
// The files also list `UnsupportedProtocol`, which is not a router.
|
// The files also list `UnsupportedProtocol`, which is not a router.
|
||||||
|
//
|
||||||
|
// Beside each address is the mask that router reads a command's type with:
|
||||||
|
// its dispatcher runs `commandType & Commands.COMMAND_TYPE_MASK`, and the bits
|
||||||
|
// above the mask are the allow-revert flag (0x80) or ignored. The mask grew
|
||||||
|
// between releases. Each is `COMMAND_TYPE_MASK` in the release's
|
||||||
|
// `contracts/libraries/Commands.sol`, the same file as in the source verified
|
||||||
|
// for that address:
|
||||||
|
// - 0x1f, UniversalRouterV1: https://github.com/Uniswap/universal-router/blob/cda697da221e24ca143bc4f55023694a2a16796f/contracts/libraries/Commands.sol
|
||||||
|
// - 0x3f, UniversalRouterV1_2_V2Support: https://github.com/Uniswap/universal-router/blob/41183d6eb154f0ab0e74a0e911a5ef9ea51fc4bd/contracts/libraries/Commands.sol
|
||||||
|
// - 0x3f, UniversalRouterV2: https://github.com/Uniswap/universal-router/blob/3663f6db6e2fe121753cd2d899699c2dc75dca86/contracts/libraries/Commands.sol
|
||||||
|
// - 0x7f, UniversalRouterV2_1_1: https://github.com/Uniswap/universal-router/blob/999d561c3ad58fb5cab91b602911f3c75591a9c7/contracts/libraries/Commands.sol
|
||||||
|
// - 0x7f, UniversalRouterV2_1_2: https://github.com/Uniswap/universal-router/blob/802fe4c18f47300e0f183e2a42e9146ec2ea9fc3/contracts/libraries/Commands.sol
|
||||||
const ROUTER_ADDRESSES = new Map([
|
const ROUTER_ADDRESSES = new Map([
|
||||||
[
|
[
|
||||||
NETWORKS.mainnet.chainId,
|
NETWORKS.mainnet.chainId,
|
||||||
// https://github.com/Uniswap/universal-router/blob/543e1a19d6e21e31ced2512eec5792b50f13a0ba/deploy-addresses/mainnet.json
|
// https://github.com/Uniswap/universal-router/blob/543e1a19d6e21e31ced2512eec5792b50f13a0ba/deploy-addresses/mainnet.json
|
||||||
[
|
[
|
||||||
"0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B", // UniversalRouterV1
|
["0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B", 0x1f], // UniversalRouterV1
|
||||||
"0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", // UniversalRouterV1_2_V2Support
|
["0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", 0x3f], // UniversalRouterV1_2_V2Support
|
||||||
"0x66a9893cc07d91d95644aedd05d03f95e1dba8af", // UniversalRouterV2
|
["0x66a9893cc07d91d95644aedd05d03f95e1dba8af", 0x3f], // UniversalRouterV2
|
||||||
"0x4C82D1fBFe28C977cBB58D8C7FF8FCF9F70a2cCA", // UniversalRouterV2_1_1
|
["0x4C82D1fBFe28C977cBB58D8C7FF8FCF9F70a2cCA", 0x7f], // UniversalRouterV2_1_1
|
||||||
"0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85", // UniversalRouterV2_1_2
|
["0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85", 0x7f], // UniversalRouterV2_1_2
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
NETWORKS.sepolia.chainId,
|
NETWORKS.sepolia.chainId,
|
||||||
// https://github.com/Uniswap/universal-router/blob/543e1a19d6e21e31ced2512eec5792b50f13a0ba/deploy-addresses/sepolia.json
|
// https://github.com/Uniswap/universal-router/blob/543e1a19d6e21e31ced2512eec5792b50f13a0ba/deploy-addresses/sepolia.json
|
||||||
[
|
[
|
||||||
"0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", // UniversalRouterV1_2_V2Support
|
["0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", 0x3f], // UniversalRouterV1_2_V2Support
|
||||||
"0x3a9d48ab9751398bbfa63ad67599bb04e4bdf98b", // UniversalRouterV2
|
["0x3a9d48ab9751398bbfa63ad67599bb04e4bdf98b", 0x3f], // UniversalRouterV2
|
||||||
"0x7DfD4F31be6814D2906BDE155c3e1B146EAc1468", // UniversalRouterV2_1_1
|
["0x7DfD4F31be6814D2906BDE155c3e1B146EAc1468", 0x7f], // UniversalRouterV2_1_1
|
||||||
"0x7E4f6c5e954Da5c61B3423D81E2277431Ac043f3", // UniversalRouterV2_1_2
|
["0x7E4f6c5e954Da5c61B3423D81E2277431Ac043f3", 0x7f], // UniversalRouterV2_1_2
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Whether `address` is a Universal Router deployment on the network with this
|
// The mask the Universal Router deployment at `address` on the network with
|
||||||
// chain id. Address case is ignored.
|
// this chain id reads a command's type with, or null when `address` is not one
|
||||||
function isRouterAddress(address, chainId) {
|
// of them. Address case is ignored.
|
||||||
|
function routerCommandTypeMask(address, chainId) {
|
||||||
const routers = ROUTER_ADDRESSES.get(chainId) || [];
|
const routers = ROUTER_ADDRESSES.get(chainId) || [];
|
||||||
const wanted = (address || "").toLowerCase();
|
const wanted = (address || "").toLowerCase();
|
||||||
return routers.some((router) => router.toLowerCase() === wanted);
|
const router = routers.find(([a]) => a.toLowerCase() === wanted);
|
||||||
|
return router ? router[1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Universal Router command IDs (lower 5 bits of each command byte)
|
// Universal Router command types, each read from its command byte with the
|
||||||
|
// router's mask (see ROUTER_ADDRESSES)
|
||||||
const COMMAND_NAMES = {
|
const COMMAND_NAMES = {
|
||||||
0x00: "V3 Swap (Exact In)",
|
0x00: "V3 Swap (Exact In)",
|
||||||
0x01: "V3 Swap (Exact Out)",
|
0x01: "V3 Swap (Exact Out)",
|
||||||
@@ -90,8 +105,10 @@ function formatAmount(raw, decimals) {
|
|||||||
const UNNAMED_CURRENCY = "Unknown (not named in the calldata)";
|
const UNNAMED_CURRENCY = "Unknown (not named in the calldata)";
|
||||||
|
|
||||||
// The same refusal when a V4 swap action could not be read (see
|
// The same refusal when a V4 swap action could not be read (see
|
||||||
// decodeExactly()). That action names both of its currencies, so saying the
|
// decodeExactly()), or the transaction has an EXECUTE_SUB_PLAN step, whose
|
||||||
// calldata did not name one would be false.
|
// commands this decoder does not read. That action names both of its
|
||||||
|
// currencies, and those commands may name one, so saying the calldata did not
|
||||||
|
// name it would be false.
|
||||||
const UNDECODED_CURRENCY = "Unknown (could not be decoded)";
|
const UNDECODED_CURRENCY = "Unknown (could not be decoded)";
|
||||||
|
|
||||||
// Explicit presence, never truthiness. Every gate in this file that guards a
|
// Explicit presence, never truthiness. Every gate in this file that guards a
|
||||||
@@ -603,7 +620,8 @@ function decodeV4Swap(input) {
|
|||||||
// sent to any other contract is not a swap through the router, whatever that
|
// sent to any other contract is not a swap through the router, whatever that
|
||||||
// contract does with it.
|
// contract does with it.
|
||||||
function decode(data, toAddress, chainId, sources) {
|
function decode(data, toAddress, chainId, sources) {
|
||||||
if (!isRouterAddress(toAddress, chainId)) return null;
|
const commandTypeMask = routerCommandTypeMask(toAddress, chainId);
|
||||||
|
if (commandTypeMask === null) return null;
|
||||||
try {
|
try {
|
||||||
const parsed = ROUTER_IFACE.parseTransaction({ data });
|
const parsed = ROUTER_IFACE.parseTransaction({ data });
|
||||||
if (!parsed) return null;
|
if (!parsed) return null;
|
||||||
@@ -621,6 +639,7 @@ function decode(data, toAddress, chainId, sources) {
|
|||||||
let hasUnwrapWeth = false;
|
let hasUnwrapWeth = false;
|
||||||
let hasV2ExactOut = false;
|
let hasV2ExactOut = false;
|
||||||
let swapUndecoded = false;
|
let swapUndecoded = false;
|
||||||
|
let hasSubPlan = false;
|
||||||
const commandNames = [];
|
const commandNames = [];
|
||||||
|
|
||||||
// THE INVARIANT: an amount and the token it is counted in always come
|
// THE INVARIANT: an amount and the token it is counted in always come
|
||||||
@@ -660,13 +679,21 @@ function decode(data, toAddress, chainId, sources) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < commandsBytes.length; i++) {
|
for (let i = 0; i < commandsBytes.length; i++) {
|
||||||
const cmdId = commandsBytes[i] & 0x1f;
|
const cmdId = commandsBytes[i] & commandTypeMask;
|
||||||
commandNames.push(
|
commandNames.push(
|
||||||
COMMAND_NAMES[cmdId] ||
|
COMMAND_NAMES[cmdId] ||
|
||||||
"Command 0x" + cmdId.toString(16).padStart(2, "0"),
|
"Command 0x" + cmdId.toString(16).padStart(2, "0"),
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (cmdId === 0x21) {
|
||||||
|
// EXECUTE_SUB_PLAN runs a further list of commands held in
|
||||||
|
// its input, which this decoder does not read. The step
|
||||||
|
// says so, and after this loop neither side is named.
|
||||||
|
hasSubPlan = true;
|
||||||
|
commandNames[i] += " (its steps are not shown)";
|
||||||
|
}
|
||||||
|
|
||||||
if (cmdId === 0x0a) {
|
if (cmdId === 0x0a) {
|
||||||
const p = decodePermit2(inputs[i]);
|
const p = decodePermit2(inputs[i]);
|
||||||
// A permit states both halves itself, so it may replace an
|
// A permit states both halves itself, so it may replace an
|
||||||
@@ -767,14 +794,28 @@ function decode(data, toAddress, chainId, sources) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The commands of an EXECUTE_SUB_PLAN step may spend the user's tokens
|
||||||
|
// or pay the user, before or after any other step, and a permit among
|
||||||
|
// them would replace the input side. What the other steps state is
|
||||||
|
// then not the whole swap, so both sides are set unknown, with no
|
||||||
|
// figure, wherever the step stands.
|
||||||
|
if (hasSubPlan) {
|
||||||
|
inputToken = null;
|
||||||
|
inputAmount = null;
|
||||||
|
outputToken = null;
|
||||||
|
minOutput = null;
|
||||||
|
outputEstablished = true;
|
||||||
|
}
|
||||||
|
|
||||||
// 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, or a V4 step with a swap action left it
|
// a currency for it, a V4 step with a swap action left it unknown, or
|
||||||
// unknown; tokenInfo() refuses rather than calling it ETH.
|
// an EXECUTE_SUB_PLAN step did; tokenInfo() refuses rather than calling
|
||||||
// UNWRAP_WETH turns WETH into ETH, so it makes the output ETH when the
|
// it ETH. UNWRAP_WETH turns WETH into ETH, so it makes the output ETH
|
||||||
// output side is WETH, or when no step set the output side. A V4 step
|
// when the output side is WETH, or when no step set the output side. A
|
||||||
// with a swap action did set it, even when it set it unknown. Any other
|
// V4 step with a swap action, or an EXECUTE_SUB_PLAN step, did set it,
|
||||||
// output keeps its own token and figure: a swap that buys USDC and
|
// even when it set it unknown. Any other output keeps its own token
|
||||||
// then unwraps the WETH it did not spend receives USDC.
|
// and figure: a swap that buys USDC and then unwraps the WETH it did
|
||||||
|
// not spend receives USDC.
|
||||||
const outputIsWeth =
|
const outputIsWeth =
|
||||||
present(outputToken) &&
|
present(outputToken) &&
|
||||||
WETH_ADDRESSES.includes(outputToken.toLowerCase());
|
WETH_ADDRESSES.includes(outputToken.toLowerCase());
|
||||||
@@ -819,7 +860,10 @@ function decode(data, toAddress, chainId, sources) {
|
|||||||
// screen must not describe the same condition in two ways.
|
// screen must not describe the same condition in two ways.
|
||||||
details.push({
|
details.push({
|
||||||
label: "Token In",
|
label: "Token In",
|
||||||
value: swapUndecoded ? UNDECODED_CURRENCY : UNNAMED_CURRENCY,
|
value:
|
||||||
|
swapUndecoded || hasSubPlan
|
||||||
|
? UNDECODED_CURRENCY
|
||||||
|
: UNNAMED_CURRENCY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -885,7 +929,10 @@ function decode(data, toAddress, chainId, sources) {
|
|||||||
// calldata did not state.
|
// calldata did not state.
|
||||||
details.push({
|
details.push({
|
||||||
label: "Token Out",
|
label: "Token Out",
|
||||||
value: swapUndecoded ? UNDECODED_CURRENCY : UNNAMED_CURRENCY,
|
value:
|
||||||
|
swapUndecoded || hasSubPlan
|
||||||
|
? UNDECODED_CURRENCY
|
||||||
|
: UNNAMED_CURRENCY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+182
-1
@@ -1,8 +1,12 @@
|
|||||||
const { AbiCoder, Interface, solidityPacked } = require("ethers");
|
const { AbiCoder, Interface, solidityPacked } = require("ethers");
|
||||||
const uniswap = require("../src/shared/uniswap");
|
const uniswap = require("../src/shared/uniswap");
|
||||||
|
|
||||||
// A Universal Router on Ethereum Mainnet, chain id 0x1.
|
// A Universal Router on Ethereum Mainnet, chain id 0x1: UniversalRouterV2,
|
||||||
|
// which reads a command's type with the mask 0x3f.
|
||||||
const ROUTER_ADDR = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
const ROUTER_ADDR = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
||||||
|
// Two more there, which read it with 0x1f and 0x7f.
|
||||||
|
const ROUTER_V1_ADDR = "0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B";
|
||||||
|
const ROUTER_V2_1_2_ADDR = "0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85";
|
||||||
const USDT_ADDR = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
const USDT_ADDR = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
||||||
const WETH_ADDR = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
|
const WETH_ADDR = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
|
||||||
const USDC_ADDR = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
const USDC_ADDR = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
||||||
@@ -105,6 +109,12 @@ function encodeV4Swap(actions, params) {
|
|||||||
return coder.encode(["bytes", "bytes[]"], [actions, params]);
|
return coder.encode(["bytes", "bytes[]"], [actions, params]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: encode an EXECUTE_SUB_PLAN input (command 0x21), which holds the
|
||||||
|
// sub-plan's own commands and inputs
|
||||||
|
function encodeSubPlan(commands, inputs) {
|
||||||
|
return coder.encode(["bytes", "bytes[]"], [commands, inputs]);
|
||||||
|
}
|
||||||
|
|
||||||
// V4 inner action IDs, as src/shared/uniswap.js names them.
|
// V4 inner action IDs, as src/shared/uniswap.js names them.
|
||||||
const V4_SWAP_EXACT_IN = 0x07;
|
const V4_SWAP_EXACT_IN = 0x07;
|
||||||
const V4_SWAP_EXACT_IN_SINGLE_ID = 0x06;
|
const V4_SWAP_EXACT_IN_SINGLE_ID = 0x06;
|
||||||
@@ -864,6 +874,177 @@ describe("uniswap decoder", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/535 — every command's type
|
||||||
|
// was read from its low five bits, as only UniversalRouterV1 reads it, so
|
||||||
|
// the later routers listed 0x21 as a V3 exact-out swap, and
|
||||||
|
// UniversalRouterV2_1_2 listed 0x40 as a V3 exact-in swap.
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
router: "UniversalRouterV1",
|
||||||
|
address: ROUTER_V1_ADDR,
|
||||||
|
command: "0x21",
|
||||||
|
steps: "V3 Swap (Exact Out)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
router: "UniversalRouterV2",
|
||||||
|
address: ROUTER_ADDR,
|
||||||
|
command: "0x21",
|
||||||
|
steps: "Execute Sub-Plan (its steps are not shown)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
router: "UniversalRouterV2_1_2",
|
||||||
|
address: ROUTER_V2_1_2_ADDR,
|
||||||
|
command: "0x21",
|
||||||
|
steps: "Execute Sub-Plan (its steps are not shown)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
router: "UniversalRouterV1",
|
||||||
|
address: ROUTER_V1_ADDR,
|
||||||
|
command: "0x40",
|
||||||
|
steps: "V3 Swap (Exact In)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
router: "UniversalRouterV2",
|
||||||
|
address: ROUTER_ADDR,
|
||||||
|
command: "0x40",
|
||||||
|
steps: "V3 Swap (Exact In)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
router: "UniversalRouterV2_1_2",
|
||||||
|
address: ROUTER_V2_1_2_ADDR,
|
||||||
|
command: "0x40",
|
||||||
|
steps: "Command 0x40",
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"command $command sent to $router is listed as $steps",
|
||||||
|
({ address, command, steps }) => {
|
||||||
|
const data = buildExecute(command, ["0x"], 9999999999n);
|
||||||
|
const result = uniswap.decode(data, address, "0x1");
|
||||||
|
expect(detail(result, "Steps").value).toBe(steps);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// A sub-plan holding a V3 swap of USDT for WETH, which the decoder does
|
||||||
|
// not read. With the allow-revert flag (0x80) the command byte is 0xa1.
|
||||||
|
test.each([
|
||||||
|
{ flag: "without", command: 0x21 },
|
||||||
|
{ flag: "with", command: 0xa1 },
|
||||||
|
])(
|
||||||
|
"an EXECUTE_SUB_PLAN step $flag the allow-revert flag says its steps are not shown",
|
||||||
|
({ command }) => {
|
||||||
|
const subPlan = encodeSubPlan("0x00", [
|
||||||
|
encodeV3SwapExactIn(USER_ADDR, 2000000n, ONE_WETH, [
|
||||||
|
USDT_ADDR,
|
||||||
|
WETH_ADDR,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
const data = buildExecute(
|
||||||
|
solidityPacked(["uint8"], [command]),
|
||||||
|
[subPlan],
|
||||||
|
9999999999n,
|
||||||
|
);
|
||||||
|
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||||
|
expect(result.name).toBe("Uniswap Swap");
|
||||||
|
expect(detail(result, "Steps").value).toBe(
|
||||||
|
"Execute Sub-Plan (its steps are not shown)",
|
||||||
|
);
|
||||||
|
// The sub-plan names both currencies, so the lines must not say
|
||||||
|
// the calldata did not.
|
||||||
|
expect(detail(result, "Token In").value).toBe(
|
||||||
|
"Unknown (could not be decoded)",
|
||||||
|
);
|
||||||
|
expect(detail(result, "Token Out").value).toBe(
|
||||||
|
"Unknown (could not be decoded)",
|
||||||
|
);
|
||||||
|
expect(detail(result, "Amount")).toBeUndefined();
|
||||||
|
expect(detail(result, "Min. received")).toBeUndefined();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// The steps inside a sub-plan may spend the user's tokens or pay the user,
|
||||||
|
// so the steps read beside it are not the whole swap, whether they come
|
||||||
|
// before it or after it. No side is named and no figure is given.
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
// 2,000 USDT for at least 1 WETH, paid to the router; the sub-plan
|
||||||
|
// swaps that WETH for USDC.
|
||||||
|
beside: "a swap before",
|
||||||
|
commands: "0x0021",
|
||||||
|
inputs: [
|
||||||
|
encodeV3SwapExactIn(ROUTER_ADDR, 2000000000n, ONE_WETH, [
|
||||||
|
USDT_ADDR,
|
||||||
|
WETH_ADDR,
|
||||||
|
]),
|
||||||
|
encodeSubPlan("0x00", [
|
||||||
|
encodeV3SwapExactIn(USER_ADDR, ONE_WETH, 2000000000n, [
|
||||||
|
WETH_ADDR,
|
||||||
|
USDC_ADDR,
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
steps: "V3 Swap (Exact In) \u2192 Execute Sub-Plan (its steps are not shown)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// The sub-plan buys WETH with the user's USDC; 1 WETH is then
|
||||||
|
// swapped for at least 2,000 DAI.
|
||||||
|
beside: "a swap after",
|
||||||
|
commands: "0x2100",
|
||||||
|
inputs: [
|
||||||
|
encodeSubPlan("0x00", [
|
||||||
|
encodeV3SwapExactIn(ROUTER_ADDR, 2000000000n, ONE_WETH, [
|
||||||
|
USDC_ADDR,
|
||||||
|
WETH_ADDR,
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
encodeV3SwapExactIn(USER_ADDR, ONE_WETH, 2000n * ONE_WETH, [
|
||||||
|
WETH_ADDR,
|
||||||
|
DAI_ADDR,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
steps: "Execute Sub-Plan (its steps are not shown) \u2192 V3 Swap (Exact In)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// A permit among the sub-plan's steps would replace this one.
|
||||||
|
beside: "a permit before",
|
||||||
|
commands: "0x0a21",
|
||||||
|
inputs: [encodePermit2(USDT_ADDR, 1000000n, ROUTER_ADDR), "0x"],
|
||||||
|
steps: "Permit2 Permit \u2192 Execute Sub-Plan (its steps are not shown)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// The sub-plan buys USDC for the user; an UNWRAP_WETH of 0
|
||||||
|
// follows. No step outside the sub-plan sets the output side, and
|
||||||
|
// UNWRAP_WETH would then make Token Out ETH.
|
||||||
|
beside: "an UNWRAP_WETH after",
|
||||||
|
commands: "0x210c",
|
||||||
|
inputs: [
|
||||||
|
encodeSubPlan("0x00", [
|
||||||
|
encodeV3SwapExactIn(USER_ADDR, 2000000000n, 1900000000n, [
|
||||||
|
USDT_ADDR,
|
||||||
|
USDC_ADDR,
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH, same encoding
|
||||||
|
],
|
||||||
|
steps: "Execute Sub-Plan (its steps are not shown) \u2192 Unwrap WETH",
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"$beside an EXECUTE_SUB_PLAN step does not name either side",
|
||||||
|
({ commands, inputs, steps }) => {
|
||||||
|
const data = buildExecute(commands, inputs, 9999999999n);
|
||||||
|
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||||
|
expect(detail(result, "Steps").value).toBe(steps);
|
||||||
|
expect(result.name).toBe("Uniswap Swap");
|
||||||
|
expect(detail(result, "Token In").value).toBe(
|
||||||
|
"Unknown (could not be decoded)",
|
||||||
|
);
|
||||||
|
expect(detail(result, "Amount")).toBeUndefined();
|
||||||
|
expect(detail(result, "Token Out").value).toBe(
|
||||||
|
"Unknown (could not be decoded)",
|
||||||
|
);
|
||||||
|
expect(detail(result, "Min. received")).toBeUndefined();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("shows the deadline as a UTC date and time", () => {
|
test("shows the deadline as a UTC date and time", () => {
|
||||||
const result = uniswap.decode(FIRST_SWAP_CALLDATA, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(FIRST_SWAP_CALLDATA, ROUTER_ADDR, "0x1");
|
||||||
expect(detail(result, "Deadline").value).toBe("2026-02-27 08:25:51");
|
expect(detail(result, "Deadline").value).toBe("2026-02-27 08:25:51");
|
||||||
|
|||||||
Reference in New Issue
Block a user