fix: each router release names and reads its own commands (closes #592) #616

Merged
clawbot merged 1 commits from issue-592-release-command-names into next 2026-10-19 18:00:14 +02:00
3 changed files with 501 additions and 115 deletions
+22 -15
View File
@@ -2971,15 +2971,25 @@ Currently supported:
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
any other shape is not read, and its step is listed as
`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 the command a step is read as. The router
reverts the whole transaction when a step fails, unless the step's command
byte has that flag set and its command is one the router lets fail, as each
release's `contracts/base/Dispatcher.sol` shows: on `UniversalRouterV2`,
`V4 Swap (could not be decoded)`. Each step is named and read as the command
that the release of the router at that address runs for its command type, as
that release's `contracts/libraries/Commands.sol` names it. The decoder reads
the type from the command byte with the mask that router 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 the command a step is read as. Releases run different commands for some
types: on `UniversalRouterV1` and `UniversalRouterV1_2_V2Support`, `0x10` to
`0x13` are NFT commands, where the later routers run `V4_SWAP`,
`V3_POSITION_MANAGER_PERMIT`, `V3_POSITION_MANAGER_CALL` and
`V4_INITIALIZE_POOL`. The decoder does not read those routers' NFT commands,
and lists each by its name there, for example `Seaport` for `SEAPORT` (`0x10`
on `UniversalRouterV1`). A type a release names no command for is listed by
its number, for example `Command 0x0e` on `UniversalRouterV1`, which has no
`BALANCE_CHECK_ERC20` and reverts on it. The router reverts the whole
transaction when a step fails, unless the step's command byte has that flag
set and its command is one the router lets fail, as each release's
`contracts/base/Dispatcher.sol` shows: on `UniversalRouterV2`,
`UniversalRouterV2_1_1` and `UniversalRouterV2_1_2`, `PERMIT2_PERMIT`,
`PERMIT2_PERMIT_BATCH`, `BALANCE_CHECK_ERC20`, `V3_POSITION_MANAGER_PERMIT`,
`V3_POSITION_MANAGER_CALL`, `V4_INITIALIZE_POOL`, `V4_POSITION_MANAGER_CALL`
@@ -2992,11 +3002,7 @@ Currently supported:
V3 and V4 swap and each payment of ETH or an ERC-20 token, the flag has no
effect: the step succeeds or the whole transaction reverts, so the step reads
as it does without the flag. A `BALANCE_CHECK_ERC20` the router lets fail
guarantees nothing, so it is read as one whose `minBalance` is zero. On
`UniversalRouterV1` and `UniversalRouterV1_2_V2Support`, `0x10` is an NFT
market call the router lets fail, which the decoder still reads as a V4 swap
([#592](https://git.eeqj.de/sneak/AutistMask/issues/592)); with the flag set,
a minimum it states is read as zero: `None (no minimum guaranteed)`. Only the
guarantees nothing, so it is read as one whose `minBalance` is zero. Only the
routers with the `0x7f` mask have `PAY_PORTION_FULL_PRECISION` (`0x07`),
listed as `Pay Portion (Full Precision)`, and `ACROSS_V4_DEPOSIT_V3` (`0x40`),
listed as `Across Bridge Deposit`; the other routers revert on `0x07`, and it
@@ -3071,7 +3077,8 @@ Currently supported:
`V3_SWAP_EXACT_IN`, `V4_SWAP`, `SWEEP`, `TRANSFER`, `PAY_PORTION`,
`PAY_PORTION_FULL_PRECISION`, `ACROSS_V4_DEPOSIT_V3`, `WRAP_ETH`,
`UNWRAP_WETH` and `BALANCE_CHECK_ERC20` (`V3_SWAP_EXACT_OUT`,
`PERMIT2_TRANSFER_FROM_BATCH` and `EXECUTE_SUB_PLAN` among them).
`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
+175 -71
View File
@@ -24,8 +24,8 @@ const ROUTER_IFACE = new Interface([
"function execute(bytes commands, bytes[] inputs, uint256 deadline)",
]);
// The Universal Router releases deployed at the addresses below, each with two
// things read from its source at the commit linked for it:
// The Universal Router releases deployed at the addresses below, each with
// three things read from its source at the commit linked for it:
//
// - `commandTypeMask`, the mask it reads a command's type with: its dispatcher
// runs `commandType & Commands.COMMAND_TYPE_MASK`, and the bits above the
@@ -33,6 +33,11 @@ const ROUTER_IFACE = new Interface([
// releases. Each is `COMMAND_TYPE_MASK` in the release's
// `contracts/libraries/Commands.sol`, the same file as in the source
// verified for its addresses.
// - `commands`, the command it runs for each command type, by the name that
// file gives it. Releases give some types different commands. A type the
// file names no command for, or only a placeholder, is left out: the router
// reverts on it, except UniversalRouterV1_2_V2Support on 0x14, which does
// nothing. decode() names and reads each step as its release's command.
// - `allowRevert`, the command types it lets fail without reverting the
// transaction. Its `execute()`, in `contracts/UniversalRouter.sol`, runs each
// step through `dispatch()`, in `contracts/base/Dispatcher.sol`, and reverts
@@ -46,6 +51,35 @@ const ROUTER_IFACE = new Interface([
// UniversalRouterV1: https://github.com/Uniswap/universal-router/tree/cda697da221e24ca143bc4f55023694a2a16796f/contracts
const ROUTER_V1 = {
commandTypeMask: 0x1f,
commands: {
0x00: "V3_SWAP_EXACT_IN",
0x01: "V3_SWAP_EXACT_OUT",
0x02: "PERMIT2_TRANSFER_FROM",
0x03: "PERMIT2_PERMIT_BATCH",
0x04: "SWEEP",
0x05: "TRANSFER",
0x06: "PAY_PORTION",
0x08: "V2_SWAP_EXACT_IN",
0x09: "V2_SWAP_EXACT_OUT",
0x0a: "PERMIT2_PERMIT",
0x0b: "WRAP_ETH",
0x0c: "UNWRAP_WETH",
0x0d: "PERMIT2_TRANSFER_FROM_BATCH",
0x10: "SEAPORT",
0x11: "LOOKS_RARE_721",
0x12: "NFTX",
0x13: "CRYPTOPUNKS",
0x14: "LOOKS_RARE_1155",
0x15: "OWNER_CHECK_721",
0x16: "OWNER_CHECK_1155",
0x17: "SWEEP_ERC721",
0x18: "X2Y2_721",
0x19: "SUDOSWAP",
0x1a: "NFT20",
0x1b: "X2Y2_1155",
0x1c: "FOUNDATION",
0x1d: "SWEEP_ERC1155",
},
// Its NFT market calls (0x10 to 0x14, 0x18 to 0x1c) and NFT owner checks
// (0x15, 0x16).
allowRevert: [
@@ -55,6 +89,39 @@ const ROUTER_V1 = {
// UniversalRouterV1_2_V2Support: https://github.com/Uniswap/universal-router/tree/41183d6eb154f0ab0e74a0e911a5ef9ea51fc4bd/contracts
const ROUTER_V1_2 = {
commandTypeMask: 0x3f,
commands: {
0x00: "V3_SWAP_EXACT_IN",
0x01: "V3_SWAP_EXACT_OUT",
0x02: "PERMIT2_TRANSFER_FROM",
0x03: "PERMIT2_PERMIT_BATCH",
0x04: "SWEEP",
0x05: "TRANSFER",
0x06: "PAY_PORTION",
0x08: "V2_SWAP_EXACT_IN",
0x09: "V2_SWAP_EXACT_OUT",
0x0a: "PERMIT2_PERMIT",
0x0b: "WRAP_ETH",
0x0c: "UNWRAP_WETH",
0x0d: "PERMIT2_TRANSFER_FROM_BATCH",
0x0e: "BALANCE_CHECK_ERC20",
0x10: "SEAPORT_V1_5",
0x11: "LOOKS_RARE_V2",
0x12: "NFTX",
0x13: "CRYPTOPUNKS",
0x15: "OWNER_CHECK_721",
0x16: "OWNER_CHECK_1155",
0x17: "SWEEP_ERC721",
0x18: "X2Y2_721",
0x19: "SUDOSWAP",
0x1a: "NFT20",
0x1b: "X2Y2_1155",
0x1c: "FOUNDATION",
0x1d: "SWEEP_ERC1155",
0x1e: "ELEMENT_MARKET",
0x20: "SEAPORT_V1_4",
0x21: "EXECUTE_SUB_PLAN",
0x22: "APPROVE_ERC20",
},
// BALANCE_CHECK_ERC20 (0x0e), its NFT market calls (0x10 to 0x13, 0x18 to
// 0x1c, 0x1e, 0x20), its NFT owner checks (0x15, 0x16) and
// EXECUTE_SUB_PLAN (0x21).
@@ -66,6 +133,28 @@ const ROUTER_V1_2 = {
// UniversalRouterV2: https://github.com/Uniswap/universal-router/tree/3663f6db6e2fe121753cd2d899699c2dc75dca86/contracts
const ROUTER_V2 = {
commandTypeMask: 0x3f,
commands: {
0x00: "V3_SWAP_EXACT_IN",
0x01: "V3_SWAP_EXACT_OUT",
0x02: "PERMIT2_TRANSFER_FROM",
0x03: "PERMIT2_PERMIT_BATCH",
0x04: "SWEEP",
0x05: "TRANSFER",
0x06: "PAY_PORTION",
0x08: "V2_SWAP_EXACT_IN",
0x09: "V2_SWAP_EXACT_OUT",
0x0a: "PERMIT2_PERMIT",
0x0b: "WRAP_ETH",
0x0c: "UNWRAP_WETH",
0x0d: "PERMIT2_TRANSFER_FROM_BATCH",
0x0e: "BALANCE_CHECK_ERC20",
0x10: "V4_SWAP",
0x11: "V3_POSITION_MANAGER_PERMIT",
0x12: "V3_POSITION_MANAGER_CALL",
0x13: "V4_INITIALIZE_POOL",
0x14: "V4_POSITION_MANAGER_CALL",
0x21: "EXECUTE_SUB_PLAN",
},
// PERMIT2_PERMIT_BATCH (0x03), PERMIT2_PERMIT (0x0a), BALANCE_CHECK_ERC20
// (0x0e), V3_POSITION_MANAGER_PERMIT, V3_POSITION_MANAGER_CALL,
// V4_INITIALIZE_POOL and V4_POSITION_MANAGER_CALL (0x11 to 0x14), and
@@ -77,6 +166,12 @@ const ROUTER_V2 = {
// The three files are the same in both releases.
const ROUTER_V2_1 = {
commandTypeMask: 0x7f,
// UniversalRouterV2's, and two more.
commands: {
...ROUTER_V2.commands,
0x07: "PAY_PORTION_FULL_PRECISION",
0x40: "ACROSS_V4_DEPOSIT_V3",
},
// The same as UniversalRouterV2's.
allowRevert: ROUTER_V2.allowRevert,
};
@@ -171,34 +266,52 @@ function v2PairFor(chainId, tokenA, tokenB) {
);
}
// Universal Router command types, each read from its command byte with the
// router's mask (see ROUTER_V1). Only the routers with the 0x7f mask
// have 0x07 and 0x40: 0x40 is above the other masks, and the other routers
// revert on 0x07, so decode() names it there as it names a command not
// listed here.
// The Steps line's name for each command in the releases' `commands` (see
// ROUTER_V1).
const COMMAND_NAMES = {
0x00: "V3 Swap (Exact In)",
0x01: "V3 Swap (Exact Out)",
0x02: "Permit2 Transfer",
0x03: "Permit2 Permit Batch",
0x04: "Sweep",
0x05: "Transfer",
0x06: "Pay Portion",
0x07: "Pay Portion (Full Precision)",
0x08: "V2 Swap (Exact In)",
0x09: "V2 Swap (Exact Out)",
0x0a: "Permit2 Permit",
0x0b: "Wrap ETH",
0x0c: "Unwrap WETH",
0x0d: "Permit2 Transfer Batch",
0x0e: "Balance Check",
0x10: "V4 Swap",
0x11: "V3 Position Mgr Permit",
0x12: "V3 Position Mgr Call",
0x13: "V4 Initialize Pool",
0x14: "V4 Position Mgr Call",
0x21: "Execute Sub-Plan",
0x40: "Across Bridge Deposit",
V3_SWAP_EXACT_IN: "V3 Swap (Exact In)",
V3_SWAP_EXACT_OUT: "V3 Swap (Exact Out)",
PERMIT2_TRANSFER_FROM: "Permit2 Transfer",
PERMIT2_PERMIT_BATCH: "Permit2 Permit Batch",
SWEEP: "Sweep",
TRANSFER: "Transfer",
PAY_PORTION: "Pay Portion",
PAY_PORTION_FULL_PRECISION: "Pay Portion (Full Precision)",
V2_SWAP_EXACT_IN: "V2 Swap (Exact In)",
V2_SWAP_EXACT_OUT: "V2 Swap (Exact Out)",
PERMIT2_PERMIT: "Permit2 Permit",
WRAP_ETH: "Wrap ETH",
UNWRAP_WETH: "Unwrap WETH",
PERMIT2_TRANSFER_FROM_BATCH: "Permit2 Transfer Batch",
BALANCE_CHECK_ERC20: "Balance Check",
V4_SWAP: "V4 Swap",
V3_POSITION_MANAGER_PERMIT: "V3 Position Mgr Permit",
V3_POSITION_MANAGER_CALL: "V3 Position Mgr Call",
V4_INITIALIZE_POOL: "V4 Initialize Pool",
V4_POSITION_MANAGER_CALL: "V4 Position Mgr Call",
EXECUTE_SUB_PLAN: "Execute Sub-Plan",
ACROSS_V4_DEPOSIT_V3: "Across Bridge Deposit",
// The NFT commands of UniversalRouterV1 and UniversalRouterV1_2_V2Support,
// and the latter's APPROVE_ERC20.
SEAPORT: "Seaport",
SEAPORT_V1_4: "Seaport V1.4",
SEAPORT_V1_5: "Seaport V1.5",
LOOKS_RARE_721: "LooksRare 721",
LOOKS_RARE_1155: "LooksRare 1155",
LOOKS_RARE_V2: "LooksRare V2",
NFTX: "NFTX",
CRYPTOPUNKS: "CryptoPunks",
OWNER_CHECK_721: "Owner Check 721",
OWNER_CHECK_1155: "Owner Check 1155",
SWEEP_ERC721: "Sweep ERC-721",
SWEEP_ERC1155: "Sweep ERC-1155",
X2Y2_721: "X2Y2 721",
X2Y2_1155: "X2Y2 1155",
SUDOSWAP: "Sudoswap",
NFT20: "NFT20",
FOUNDATION: "Foundation",
ELEMENT_MARKET: "Element Market",
APPROVE_ERC20: "Approve ERC-20",
};
// The swap's Amount and Min. received lines land on the same approval screen,
@@ -913,7 +1026,7 @@ function decodeV4Swap(input) {
function decode(data, toAddress, chainId, sources, fromAddress) {
const release = routerRelease(toAddress, chainId);
if (release === null) return null;
const { commandTypeMask, allowRevert } = release;
const { commandTypeMask, commands, allowRevert } = release;
try {
const parsed = ROUTER_IFACE.parseTransaction({ data });
if (!parsed) return null;
@@ -1026,7 +1139,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// router adds only what it holds.
const isHop = (i, token, recipient) => {
if (i + 1 >= commandsBytes.length) return false;
if ((commandsBytes[i + 1] & commandTypeMask) !== 0x08) return false;
const nextCommand =
commands[commandsBytes[i + 1] & commandTypeMask];
if (nextCommand !== "V2_SWAP_EXACT_IN") return false;
const next = decodeV2SwapExactIn(inputs[i + 1]);
return (
next !== null &&
@@ -1042,23 +1157,22 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
for (let i = 0; i < commandsBytes.length; i++) {
const cmdId = commandsBytes[i] & commandTypeMask;
// The command this release runs for the step (see ROUTER_V1),
// undefined for a type it names no command for.
const command = commands[cmdId];
// The router goes on to the next step if this one fails (see
// ROUTER_V1). The flag on any other command has no effect.
const allowedToFail =
(commandsBytes[i] & ALLOW_REVERT_FLAG) !== 0 &&
allowRevert.includes(cmdId);
// Only the routers with the 0x7f mask have command 0x07 (see
// COMMAND_NAMES).
const name =
cmdId === 0x07 && commandTypeMask !== 0x7f
? null
: COMMAND_NAMES[cmdId];
commandNames.push(
name || "Command 0x" + cmdId.toString(16).padStart(2, "0"),
present(command)
? COMMAND_NAMES[command]
: "Command 0x" + cmdId.toString(16).padStart(2, "0"),
);
try {
if (cmdId === 0x21) {
if (command === "EXECUTE_SUB_PLAN") {
// 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.
@@ -1066,13 +1180,13 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
hasSubPlan = true;
commandNames[i] += " (its steps are not shown)";
payeeUnread = true;
} else if (cmdId === 0x0a) {
} else if (command === "PERMIT2_PERMIT") {
const p = decodePermit2(inputs[i]);
// A permit states both halves itself, so it may replace an
// input side an earlier hop established without breaking
// the invariant.
if (p) setInput(p.token, p.amount);
} else if (cmdId === 0x02) {
} else if (command === "PERMIT2_TRANSFER_FROM") {
const p = decodePermit2TransferFrom(inputs[i]);
if (!p) {
payeeUnread = true;
@@ -1091,7 +1205,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} else if (!isHop(i, p.token, p.recipient)) {
addPayee(p.recipient);
}
} else if (cmdId === 0x0e) {
} else if (command === "BALANCE_CHECK_ERC20") {
const b = decodeBalanceCheck(inputs[i]);
if (!b) {
// The router still runs a check whose input does not
@@ -1121,7 +1235,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
setOutput(b.token, minBalance);
}
}
} else if (cmdId === 0x00) {
} else if (command === "V3_SWAP_EXACT_IN") {
const s = decodeV3SwapExactIn(inputs[i]);
if (s) {
setInputOnce(s.tokenIn, s.amountIn);
@@ -1135,7 +1249,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} else {
payeeUnread = true;
}
} else if (cmdId === 0x08) {
} else if (command === "V2_SWAP_EXACT_IN") {
const s = decodeV2SwapExactIn(inputs[i]);
if (s) {
setInputOnce(s.tokenIn, s.amountIn);
@@ -1146,7 +1260,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} else {
payeeUnread = true;
}
} else if (cmdId === 0x09) {
} else if (command === "V2_SWAP_EXACT_OUT") {
// Buys exactly amountOut and spends at most amountInMax.
hasV2ExactOut = true;
const s = decodeV2SwapExactOut(inputs[i]);
@@ -1159,7 +1273,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} else {
payeeUnread = true;
}
} else if (cmdId === 0x04) {
} else if (command === "SWEEP") {
const s = decodeSweep(inputs[i]);
if (s) {
addPayee(s.recipient);
@@ -1170,9 +1284,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
payeeUnread = true;
}
} else if (
cmdId === 0x05 ||
cmdId === 0x06 ||
(cmdId === 0x07 && commandTypeMask === 0x7f)
command === "TRANSFER" ||
command === "PAY_PORTION" ||
command === "PAY_PORTION_FULL_PRECISION"
) {
const t = decodeTransfer(inputs[i]);
if (t) {
@@ -1183,16 +1297,16 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// PAY_PORTION_FULL_PRECISION pays all the router
// holds of the token, as SWEEP does.
const all =
cmdId === 0x05
command === "TRANSFER"
? CONTRACT_BALANCE
: cmdId === 0x06
: command === "PAY_PORTION"
? 10000n
: 10n ** 18n;
// A TRANSFER of ETH has no such amount: the router
// sends ETH as written, so a TRANSFER of
// CONTRACT_BALANCE of ETH always reverts.
const ethTransfer =
cmdId === 0x05 &&
command === "TRANSFER" &&
t.token ===
"0x0000000000000000000000000000000000000000";
if (
@@ -1205,9 +1319,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} else {
payeeUnread = true;
}
} else if (cmdId === 0x40) {
// Only the routers with the 0x7f mask read a command type
// of 0x40.
} else if (command === "ACROSS_V4_DEPOSIT_V3") {
const a = decodeAcrossV4DepositV3(inputs[i]);
if (a) {
bridgeDeposit = true;
@@ -1230,7 +1342,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} else {
payeeUnread = true;
}
} else if (cmdId === 0x0b) {
} else if (command === "WRAP_ETH") {
const w = decodeWrapEth(inputs[i]);
if (w) {
setInputOnce(
@@ -1241,17 +1353,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} else {
payeeUnread = true;
}
} else if (cmdId === 0x10) {
let v4 = decodeV4Swap(inputs[i]);
// A step the router lets fail guarantees nothing, so a
// minimum it states is read as zero, as a
// BALANCE_CHECK_ERC20's is. Only UniversalRouterV1 and
// UniversalRouterV1_2_V2Support let this command fail; on
// them it is an NFT market call, still read here as a V4
// swap (https://git.eeqj.de/sneak/AutistMask/issues/592).
if (v4 && allowedToFail && present(v4.amountOutMin)) {
v4 = { ...v4, amountOutMin: 0n };
}
} else if (command === "V4_SWAP") {
const v4 = decodeV4Swap(inputs[i]);
if (v4) {
for (const t of v4.takes) {
if (!isHop(i, t.currency, t.recipient)) {
@@ -1292,7 +1395,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
setInputOnce(v4.tokenIn, v4.amountIn);
setOutput(v4.tokenOut, v4.amountOutMin);
}
} else if (cmdId === 0x0c) {
} else if (command === "UNWRAP_WETH") {
hasUnwrapWeth = true;
const u = decodeUnwrapWeth(inputs[i]);
if (u) {
@@ -1306,9 +1409,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
payeeUnread = true;
}
} else {
// Any other command may pay an address this decoder does
// not read: V3_SWAP_EXACT_OUT and
// PERMIT2_TRANSFER_FROM_BATCH among them.
// Any other step may pay an address this decoder does not
// read: V3_SWAP_EXACT_OUT, PERMIT2_TRANSFER_FROM_BATCH and
// the NFT commands among them, and a type the release
// names no command for.
payeeUnread = true;
}
} catch {
+304 -29
View File
@@ -2,7 +2,9 @@
// (https://git.eeqj.de/sneak/AutistMask/issues/594 and
// https://git.eeqj.de/sneak/AutistMask/issues/599), the allow-revert flag
// on the steps that pay it
// (https://git.eeqj.de/sneak/AutistMask/issues/602), and the address whose
// (https://git.eeqj.de/sneak/AutistMask/issues/602), the command each router
// release runs for a command type
// (https://git.eeqj.de/sneak/AutistMask/issues/592), and the address whose
// balance a BALANCE_CHECK_ERC20 checks
// (https://git.eeqj.de/sneak/AutistMask/issues/600).
//
@@ -1570,32 +1572,6 @@ describe("the allow-revert flag", () => {
);
});
// On these two routers 0x10 is an NFT market call the router lets fail,
// still read as a V4 swap
// (https://git.eeqj.de/sneak/AutistMask/issues/592).
test.each([
{ router: "UniversalRouterV1", address: ROUTER_V1 },
{ router: "UniversalRouterV1_2_V2Support", address: ROUTER_V1_2 },
])(
"on $router, a V4_SWAP with the flag is allowed to fail and states no minimum",
({ address }) => {
const step = v4Swap(SWAP_USDT_FOR_USDC, SETTLE_USDT, takeAll(USDC));
const plain = decode(execute(step), USER, address);
expect(detail(plain, "Steps").value).toBe("V4 Swap");
expect(detail(plain, "Min. received").value).toBe("1990.0000 USDC");
const withFlag = decode(execute(flagged(step)), USER, address);
expect(detail(withFlag, "Steps").value).toBe(
"V4 Swap (allowed to fail)",
);
expect(detail(withFlag, "Token Out").address).toBe(USDC);
expect(detail(withFlag, "Min. received").value).toBe(
"None (no minimum guaranteed)",
);
},
);
// Only the routers from UniversalRouterV2 on let a PERMIT2_PERMIT fail.
test.each([
{ router: "UniversalRouterV1", address: ROUTER_V1, mark: "" },
@@ -1663,14 +1639,14 @@ describe("the allow-revert flag", () => {
address: ROUTER_V1,
command: "FOUNDATION",
id: 0x1c,
name: "Command 0x1c",
name: "Foundation",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
command: "ELEMENT_MARKET",
id: 0x1e,
name: "Command 0x1e",
name: "Element Market",
},
{
router: "UniversalRouterV2",
@@ -1718,6 +1694,305 @@ describe("the allow-revert flag", () => {
);
});
// https://git.eeqj.de/sneak/AutistMask/issues/592: each release runs its own
// command for a command type, as its Commands.sol names it. The decoder named
// and read every type as UniversalRouterV2 does, so on the two older routers
// an NFT command read as a V4 swap, and on UniversalRouterV1, which reverts on
// 0x0e, that type read as a balance check that set the minimum.
describe("the command each release runs for a command type", () => {
// Inputs UniversalRouterV2 reads as a V4 swap of 2,000 USDT for at least
// 1,990 USDC paid to the user, and as a check that the user holds at
// least 1 WETH.
const swapInput = v4Swap(SWAP_USDT_FOR_USDC, SETTLE_USDT, takeAll(USDC))[1];
const checkInput = balanceCheck(WETH, ONE_WETH)[1];
// The lines of a transaction whose one step the decoder does not read.
const NOT_READ = [
"Protocol",
"Token In",
"Token Out",
"Paid to",
"Steps",
"Deadline",
];
test.each([
{
router: "UniversalRouterV1",
address: ROUTER_V1,
id: 0x0e,
input: checkInput,
name: "Command 0x0e",
},
{
router: "UniversalRouterV1",
address: ROUTER_V1,
id: 0x10,
input: swapInput,
name: "Seaport",
},
{
router: "UniversalRouterV1",
address: ROUTER_V1,
id: 0x11,
input: swapInput,
name: "LooksRare 721",
},
{
router: "UniversalRouterV1",
address: ROUTER_V1,
id: 0x12,
input: swapInput,
name: "NFTX",
},
{
router: "UniversalRouterV1",
address: ROUTER_V1,
id: 0x13,
input: swapInput,
name: "CryptoPunks",
},
{
router: "UniversalRouterV1",
address: ROUTER_V1,
id: 0x14,
input: swapInput,
name: "LooksRare 1155",
},
{
router: "UniversalRouterV1",
address: ROUTER_V1,
id: 0x15,
input: swapInput,
name: "Owner Check 721",
},
{
router: "UniversalRouterV1",
address: ROUTER_V1,
id: 0x16,
input: swapInput,
name: "Owner Check 1155",
},
{
router: "UniversalRouterV1",
address: ROUTER_V1,
id: 0x17,
input: swapInput,
name: "Sweep ERC-721",
},
{
router: "UniversalRouterV1",
address: ROUTER_V1,
id: 0x18,
input: swapInput,
name: "X2Y2 721",
},
{
router: "UniversalRouterV1",
address: ROUTER_V1,
id: 0x19,
input: swapInput,
name: "Sudoswap",
},
{
router: "UniversalRouterV1",
address: ROUTER_V1,
id: 0x1a,
input: swapInput,
name: "NFT20",
},
{
router: "UniversalRouterV1",
address: ROUTER_V1,
id: 0x1b,
input: swapInput,
name: "X2Y2 1155",
},
{
router: "UniversalRouterV1",
address: ROUTER_V1,
id: 0x1c,
input: swapInput,
name: "Foundation",
},
{
router: "UniversalRouterV1",
address: ROUTER_V1,
id: 0x1d,
input: swapInput,
name: "Sweep ERC-1155",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
id: 0x10,
input: swapInput,
name: "Seaport V1.5",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
id: 0x11,
input: swapInput,
name: "LooksRare V2",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
id: 0x12,
input: swapInput,
name: "NFTX",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
id: 0x13,
input: swapInput,
name: "CryptoPunks",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
id: 0x14,
input: swapInput,
name: "Command 0x14",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
id: 0x15,
input: swapInput,
name: "Owner Check 721",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
id: 0x16,
input: swapInput,
name: "Owner Check 1155",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
id: 0x17,
input: swapInput,
name: "Sweep ERC-721",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
id: 0x18,
input: swapInput,
name: "X2Y2 721",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
id: 0x19,
input: swapInput,
name: "Sudoswap",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
id: 0x1a,
input: swapInput,
name: "NFT20",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
id: 0x1b,
input: swapInput,
name: "X2Y2 1155",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
id: 0x1c,
input: swapInput,
name: "Foundation",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
id: 0x1d,
input: swapInput,
name: "Sweep ERC-1155",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
id: 0x1e,
input: swapInput,
name: "Element Market",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
id: 0x20,
input: swapInput,
name: "Seaport V1.4",
},
{
router: "UniversalRouterV1_2_V2Support",
address: ROUTER_V1_2,
id: 0x22,
input: swapInput,
name: "Approve ERC-20",
},
{
router: "UniversalRouterV2",
address: ROUTER,
id: 0x11,
input: swapInput,
name: "V3 Position Mgr Permit",
},
{
router: "UniversalRouterV2",
address: ROUTER,
id: 0x12,
input: swapInput,
name: "V3 Position Mgr Call",
},
{
router: "UniversalRouterV2",
address: ROUTER,
id: 0x13,
input: swapInput,
name: "V4 Initialize Pool",
},
])(
"on $router, '$name' is a step the decoder does not read",
({ address, id, input, name }) => {
const result = decode(execute([id, input]), USER, address);
expect(detail(result, "Steps").value).toBe(name);
expect(result.name).toBe("Uniswap Swap");
expect(labels(result)).toEqual(NOT_READ);
expect(paidTo(result)).toEqual([UNDECODED]);
},
);
test.each([
{ router: "UniversalRouterV1_2_V2Support", address: ROUTER_V1_2 },
{ router: "UniversalRouterV2", address: ROUTER },
])(
"on $router, 0x0e is a balance check, which sets the minimum",
({ address }) => {
const result = decode(execute([0x0e, checkInput]), USER, address);
expect(detail(result, "Steps").value).toBe("Balance Check");
expect(detail(result, "Token Out").address).toBe(WETH);
expect(detail(result, "Min. paid out").value).toBe("1.0000 WETH");
},
);
test("on UniversalRouterV2, 0x10 is a V4 swap", () => {
const result = decode(execute([0x10, swapInput]), USER, ROUTER);
expect(detail(result, "Steps").value).toBe("V4 Swap");
expect(result.name).toBe("Swap USDT → USDC");
expect(detail(result, "Min. received").value).toBe("1990.0000 USDC");
});
});
// https://git.eeqj.de/sneak/AutistMask/issues/600: on every release that has
// BALANCE_CHECK_ERC20, the router checks the balance of the owner as written,
// without map(), so address(1) and address(2) there are those addresses, not