diff --git a/README.md b/README.md index 24e1d92..c325f27 100644 --- a/README.md +++ b/README.md @@ -2983,15 +2983,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` @@ -3004,11 +3014,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 @@ -3083,7 +3089,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 diff --git a/src/shared/uniswap.js b/src/shared/uniswap.js index 88c5a6a..fee7209 100644 --- a/src/shared/uniswap.js +++ b/src/shared/uniswap.js @@ -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 { diff --git a/tests/uniswapRecipient.test.js b/tests/uniswapRecipient.test.js index 9295b56..75a21ca 100644 --- a/tests/uniswapRecipient.test.js +++ b/tests/uniswapRecipient.test.js @@ -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