From aebfa391d3e3466fb083f5c03aad53ff7d76361c Mon Sep 17 00:00:00 2001 From: diana Date: Wed, 16 Sep 2026 19:20:37 -0400 Subject: [PATCH 1/3] revert: undo #491 and #497 input-bounding and nested-unlock changes (#514) * revert: bound router command input decoding (#497) Reverts d203e7f5525aeae385800f9490b93886711701df. This change was deployed to mainnet at 0x0542093271A31f6FC1DADB232bd59eeb27de780F but was never intended to ship. Removes the checkInputLength calldata bounds checks, the BytesLib hardening, the SWEEP uint160 truncation fix, the UNWRAP_WETH_EXACT command (0x0f), and unwrapWETH9Exact. Co-Authored-By: Claude Opus 5 (1M context) * revert: allow V4_SWAP within an existing PoolManager unlock (#491) Reverts 9e9a780a3c17b61fc78a1a73c85684859dda1bad. This change was deployed to mainnet at 0x0542093271A31f6FC1DADB232bd59eeb27de780F but was never intended to ship. It is the root cause of audit finding M-01: V4 deltas accrue under the router regardless of which contract called execute(), so within one foreign unlock, one call can leave debt that a later call settles from a different msgSender(). Removing the fast path restores the pre-#491 behaviour: a nested V4_SWAP reverts with AlreadyUnlocked. This supersedes #502, #512 and #513, which existed only to gate the fast path. Co-Authored-By: Claude Opus 5 (1M context) * chore: regenerate UniversalRouter bytecode size snapshot 23705 -> 23303 after reverting #491 and #497. The hardhat gas snapshots under test/integration-tests/gas-tests/__snapshots__/ are deliberately left untouched: they require a mainnet fork (FORK_URL, block 20010000) to regenerate and cannot be derived by hand. They must be regenerated with 'UPDATE_SNAPSHOT=1 yarn test:hardhat --grep gas' before this merges. Co-Authored-By: Claude Opus 5 (1M context) * chore: regenerate hardhat gas snapshots after reverting #491 and #497 65 snapshots updated across 5 suites. One snapshot removed: 'UNWRAP_WETH_EXACT partial amount', whose command (0x0f) no longer exists after the #497 revert. Regenerated with UPDATE_SNAPSHOT=1 against a mainnet fork at block 20010000. Co-Authored-By: Claude Opus 5 (1M context) * build: bump v4-periphery to main with the #584 revert Moves lib/v4-periphery 07336f2 -> a7af5b3, which is v4-periphery main after #601 merged (revert of #584, tolerate hook-funded input on exact-output swaps). #564's exact-output partial-fill revert is retained; the only src/ change against the previous pin is V4Router.sol (+4/-23). Co-Authored-By: Claude Opus 5 (1M context) * chore: regenerate bytecode snapshot after v4-periphery bump 23303 -> 23233. Hardhat gas snapshots unchanged: the #584 revert only removes hook-funded exact-output paths, which the gas suite does not exercise. Co-Authored-By: Claude Opus 5 (1M context) --------- Co-authored-by: Claude Opus 5 (1M context) --- README.md | 18 +- contracts/base/Dispatcher.sol | 103 +-- contracts/interfaces/IUniversalRouter.sol | 6 +- contracts/libraries/Commands.sol | 2 +- contracts/modules/Payments.sol | 13 - contracts/modules/V3ToV4Migrator.sol | 6 - contracts/modules/uniswap/v3/BytesLib.sol | 37 +- lib/v4-periphery | 2 +- snapshots/UniversalRouterTest.json | 2 +- test/foundry-tests/RouteSigner.t.sol | 87 --- test/foundry-tests/UniversalRouter.t.sol | 139 ---- test/foundry-tests/V4SwapWithinUnlock.t.sol | 667 ------------------ .../integration-tests/UniversalRouter.test.ts | 72 -- .../gas-tests/Payments.gas.test.ts | 11 - .../CheckOwnership.gas.test.ts.snap | 2 +- .../__snapshots__/Payments.gas.test.ts.snap | 21 +- .../__snapshots__/Uniswap.gas.test.ts.snap | 78 +- .../UniversalVSSwapRouter.gas.test.ts.snap | 20 +- .../V3ToV4Migration.gas.test.ts.snap | 16 +- test/integration-tests/shared/planner.ts | 2 - 20 files changed, 91 insertions(+), 1213 deletions(-) delete mode 100644 test/foundry-tests/V4SwapWithinUnlock.t.sol diff --git a/README.md b/README.md index fcc857e68..f7e385cab 100644 --- a/README.md +++ b/README.md @@ -84,21 +84,7 @@ Each command is a `bytes1` containing the following 8 bits: ├──────┼───────────────────────────────┤ │ 0x0d │ PERMIT2_TRANSFER_FROM_BATCH │ ├──────┼───────────────────────────────┤ - │ 0x0e │ BALANCE_CHECK_ERC20 │ - ├──────┼───────────────────────────────┤ - │ 0x0f │ UNWRAP_WETH_EXACT │ - ├──────┼───────────────────────────────┤ - │ 0x10 │ V4_SWAP │ - ├──────┼───────────────────────────────┤ - │ 0x11 │ V3_POSITION_MANAGER_PERMIT │ - ├──────┼───────────────────────────────┤ - │ 0x12 │ V3_POSITION_MANAGER_CALL │ - ├──────┼───────────────────────────────┤ - │ 0x13 │ V4_INITIALIZE_POOL │ - ├──────┼───────────────────────────────┤ - │ 0x14 │ V4_POSITION_MANAGER_CALL │ - ├──────┼───────────────────────────────┤ - │ 0x15-│ ------- │ + │ 0x0e-│ ------- │ │ 0x20 │ │ ├──────┼───────────────────────────────┤ │ 0x21 │ EXECUTE_SUB_PLAN │ @@ -110,8 +96,6 @@ Each command is a `bytes1` containing the following 8 bits: Note that some of the commands in the middle of the series are unused. These gaps allowed us to create gas-efficiencies when selecting which command to execute. -`UNWRAP_WETH_EXACT` takes `(recipient, amount)` and unwraps an exact amount of the contract's WETH, reverting if the balance is insufficient. - #### How the input bytes are structures Each input bytes string is merely the abi encoding of a set of parameters. Depending on the command chosen, the input bytes string will be different. For example: diff --git a/contracts/base/Dispatcher.sol b/contracts/base/Dispatcher.sol index 6817f5953..12d4a79fd 100644 --- a/contracts/base/Dispatcher.sol +++ b/contracts/base/Dispatcher.sol @@ -17,7 +17,6 @@ import {ActionConstants} from '@uniswap/v4-periphery/src/libraries/ActionConstan import {CalldataDecoder} from '@uniswap/v4-periphery/src/libraries/CalldataDecoder.sol'; import {PoolKey} from '@uniswap/v4-core/src/types/PoolKey.sol'; import {IPoolManager} from '@uniswap/v4-core/src/interfaces/IPoolManager.sol'; -import {TransientStateLibrary} from '@uniswap/v4-core/src/libraries/TransientStateLibrary.sol'; import {ChainedActions} from '../modules/ChainedActions.sol'; /// @title Decodes and Executes Commands @@ -33,7 +32,6 @@ abstract contract Dispatcher is { using BytesLib for bytes; using CalldataDecoder for bytes; - using TransientStateLibrary for IPoolManager; error InvalidCommandType(uint256 commandType); error BalanceTooLow(); @@ -68,7 +66,6 @@ abstract contract Dispatcher is // 0x00 <= command < 0x08 if (command < Commands.V2_SWAP_EXACT_IN) { if (command == Commands.V3_SWAP_EXACT_IN) { - checkInputLength(inputs, 0xc0); // equivalent: abi.decode(inputs, (address, uint256, uint256, bytes, bool, uint256[])) address recipient; uint256 amountIn; @@ -86,7 +83,6 @@ abstract contract Dispatcher is address payer = payerIsUser ? msgSender() : address(this); v3SwapExactInput(map(recipient), amountIn, amountOutMin, path, payer, minHopPriceX36); } else if (command == Commands.V3_SWAP_EXACT_OUT) { - checkInputLength(inputs, 0xc0); // equivalent: abi.decode(inputs, (address, uint256, uint256, bytes, bool, uint256[])) address recipient; uint256 amountOut; @@ -104,7 +100,6 @@ abstract contract Dispatcher is address payer = payerIsUser ? msgSender() : address(this); v3SwapExactOutput(map(recipient), amountOut, amountInMax, path, payer, minHopPriceX36); } else if (command == Commands.PERMIT2_TRANSFER_FROM) { - checkInputLength(inputs, 0x60); // equivalent: abi.decode(inputs, (address, address, uint160)) address token; address recipient; @@ -116,7 +111,12 @@ abstract contract Dispatcher is } permit2TransferFrom(token, msgSender(), map(recipient), amount); } else if (command == Commands.PERMIT2_PERMIT_BATCH) { - IAllowanceTransfer.PermitBatch calldata permitBatch = decodePermitBatch(inputs); + IAllowanceTransfer.PermitBatch calldata permitBatch; + assembly { + // this is a variable length struct, so calldataload(inputs.offset) contains the + // offset from inputs.offset at which the struct begins + permitBatch := add(inputs.offset, calldataload(inputs.offset)) + } bytes calldata data = inputs.toBytes(1); (success, output) = address(PERMIT2) .call( @@ -128,11 +128,10 @@ abstract contract Dispatcher is ) ); } else if (command == Commands.SWEEP) { - checkInputLength(inputs, 0x60); // equivalent: abi.decode(inputs, (address, address, uint256)) address token; address recipient; - uint256 amountMin; + uint160 amountMin; assembly { token := calldataload(inputs.offset) recipient := calldataload(add(inputs.offset, 0x20)) @@ -148,7 +147,6 @@ abstract contract Dispatcher is } Payments.sweep(token, map(recipient), amountMin); } else if (command == Commands.TRANSFER) { - checkInputLength(inputs, 0x60); // equivalent: abi.decode(inputs, (address, address, uint256)) address token; address recipient; @@ -160,7 +158,6 @@ abstract contract Dispatcher is } Payments.pay(token, map(recipient), value); } else if (command == Commands.PAY_PORTION) { - checkInputLength(inputs, 0x60); // equivalent: abi.decode(inputs, (address, address, uint256)) address token; address recipient; @@ -172,7 +169,6 @@ abstract contract Dispatcher is } Payments.payPortion(token, map(recipient), bips); } else if (command == Commands.PAY_PORTION_FULL_PRECISION) { - checkInputLength(inputs, 0x60); // equivalent: abi.decode(inputs, (address, address, uint256)) address token; address recipient; @@ -183,11 +179,12 @@ abstract contract Dispatcher is portion := calldataload(add(inputs.offset, 0x40)) } Payments.payPortionFullPrecision(token, map(recipient), portion); + } else { + revert InvalidCommandType(command); } } else { // 0x08 <= command < 0x10 if (command == Commands.V2_SWAP_EXACT_IN) { - checkInputLength(inputs, 0xc0); // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], bool, uint256[])) address recipient; uint256 amountIn; @@ -205,7 +202,6 @@ abstract contract Dispatcher is address payer = payerIsUser ? msgSender() : address(this); v2SwapExactInput(map(recipient), amountIn, amountOutMin, path, payer, minHopPriceX36); } else if (command == Commands.V2_SWAP_EXACT_OUT) { - checkInputLength(inputs, 0xc0); // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], bool, uint256[])) address recipient; uint256 amountOut; @@ -223,7 +219,6 @@ abstract contract Dispatcher is address payer = payerIsUser ? msgSender() : address(this); v2SwapExactOutput(map(recipient), amountOut, amountInMax, path, payer, minHopPriceX36); } else if (command == Commands.PERMIT2_PERMIT) { - checkInputLength(inputs, 0xe0); // equivalent: abi.decode(inputs, (IAllowanceTransfer.PermitSingle, bytes)) IAllowanceTransfer.PermitSingle calldata permitSingle; assembly { @@ -240,7 +235,6 @@ abstract contract Dispatcher is ) ); } else if (command == Commands.WRAP_ETH) { - checkInputLength(inputs, 0x40); // equivalent: abi.decode(inputs, (address, uint256)) address recipient; uint256 amount; @@ -250,7 +244,6 @@ abstract contract Dispatcher is } Payments.wrapETH(map(recipient), amount); } else if (command == Commands.UNWRAP_WETH) { - checkInputLength(inputs, 0x40); // equivalent: abi.decode(inputs, (address, uint256)) address recipient; uint256 amountMin; @@ -261,14 +254,13 @@ abstract contract Dispatcher is Payments.unwrapWETH9(map(recipient), amountMin); } else if (command == Commands.PERMIT2_TRANSFER_FROM_BATCH) { IAllowanceTransfer.AllowanceTransferDetails[] calldata batchDetails; - (uint256 length, uint256 offset) = inputs.toLengthOffset(0, 0x80); + (uint256 length, uint256 offset) = inputs.toLengthOffset(0); assembly { batchDetails.length := length batchDetails.offset := offset } permit2TransferFrom(batchDetails, msgSender()); } else if (command == Commands.BALANCE_CHECK_ERC20) { - checkInputLength(inputs, 0x60); // equivalent: abi.decode(inputs, (address, address, uint256)) address owner; address token; @@ -280,31 +272,16 @@ abstract contract Dispatcher is } success = (ERC20(token).balanceOf(owner) >= minBalance); if (!success) output = abi.encodePacked(BalanceTooLow.selector); - } else if (command == Commands.UNWRAP_WETH_EXACT) { - checkInputLength(inputs, 0x40); - // equivalent: abi.decode(inputs, (address, uint256)) - address recipient; - uint256 amount; - assembly { - recipient := calldataload(inputs.offset) - amount := calldataload(add(inputs.offset, 0x20)) - } - Payments.unwrapWETH9Exact(map(recipient), amount); + } else { + // placeholder area for command 0x0f + revert InvalidCommandType(command); } } } else { // 0x10 <= command < 0x21 if (command == Commands.V4_SWAP) { - // If the PoolManager is already unlocked, this contract is executing inside another - // contract's unlock callback. Opening a new lock via _executeActions would revert with - // AlreadyUnlocked, so instead run the v4 actions within the existing lock. - if (poolManager.isUnlocked()) { - (bytes calldata actions, bytes[] calldata params) = inputs.decodeActionsRouterParams(); - _executeActionsWithoutUnlock(actions, params); - } else { - // pass the calldata provided to V4SwapRouter._executeActions (defined in BaseActionsRouter) - _executeActions(inputs); - } + // pass the calldata provided to V4SwapRouter._executeActions (defined in BaseActionsRouter) + _executeActions(inputs); // This contract MUST be approved to spend the token since its going to be doing the call on the position manager } else if (command == Commands.V3_POSITION_MANAGER_PERMIT) { _checkV3PermitCall(inputs); @@ -313,7 +290,6 @@ abstract contract Dispatcher is _checkV3PositionManagerCall(inputs, msgSender()); (success, output) = address(V3_POSITION_MANAGER).call(inputs); } else if (command == Commands.V4_INITIALIZE_POOL) { - checkInputLength(inputs, 0xc0); PoolKey calldata poolKey; uint160 sqrtPriceX96; assembly { @@ -350,55 +326,6 @@ abstract contract Dispatcher is } } - /// @dev Reverts if an input cannot contain the complete static ABI head consumed by a command. - function checkInputLength(bytes calldata input, uint256 minimumLength) private pure { - if (input.length < minimumLength) revert CalldataDecoder.SliceOutOfBounds(); - } - - /// @dev Decodes a Permit2 batch after validating its dynamic details array against the input bounds. - function decodePermitBatch(bytes calldata input) - private - pure - returns (IAllowanceTransfer.PermitBatch calldata permitBatch) - { - // PermitBatch has a three-word head: details offset, spender, and signature deadline. - checkInputLength(input, 0x60); - - assembly ('memory-safe') { - let inputLength := input.length - let permitBatchOffset := calldataload(input.offset) - - // The PermitBatch head must be contained in the input. - if gt(permitBatchOffset, sub(inputLength, 0x60)) { - mstore(0, 0x3b99b53d) // SliceOutOfBounds() - revert(0x1c, 0x04) - } - - let permitBatchPointer := add(input.offset, permitBatchOffset) - let detailsOffset := calldataload(permitBatchPointer) - let permitBatchLength := sub(inputLength, permitBatchOffset) - - // The PermitDetails array length word must be contained in the PermitBatch slice. - if gt(detailsOffset, sub(permitBatchLength, 0x20)) { - mstore(0, 0x3b99b53d) // SliceOutOfBounds() - revert(0x1c, 0x04) - } - - let detailsLengthPointer := add(permitBatchPointer, detailsOffset) - let detailsLength := calldataload(detailsLengthPointer) - let detailsDataOffset := add(detailsOffset, 0x20) - let remainingLength := sub(permitBatchLength, detailsDataOffset) - - // Each PermitDetails element occupies four words. - if gt(detailsLength, div(remainingLength, 0x80)) { - mstore(0, 0x3b99b53d) // SliceOutOfBounds() - revert(0x1c, 0x04) - } - - permitBatch := permitBatchPointer - } - } - /// @notice Calculates the recipient address for a command /// @param recipient The recipient or recipient-flag for the command /// @return output The resultant recipient for the command diff --git a/contracts/interfaces/IUniversalRouter.sol b/contracts/interfaces/IUniversalRouter.sol index cf3bac434..aab785ba9 100644 --- a/contracts/interfaces/IUniversalRouter.sol +++ b/contracts/interfaces/IUniversalRouter.sol @@ -70,8 +70,8 @@ interface IUniversalRouter { /// @return intent The intent value from the signed execution, or bytes32(0) if not in a signed execution /// @return data The data value from the signed execution, or bytes32(0) if not in a signed execution /// @dev This reads from transient storage which is only set during executeSigned(). - /// @dev When consuming this context from a V4 hook, the hook MUST verify that msg.sender is the PoolManager - /// and that the hook's sender parameter is the UniversalRouter. Otherwise, a malicious contract in the - /// execution chain could abuse the legitimate signed context by initiating its own swap. + /// @dev When consuming this context from a hook, the hook MUST verify that msg.sender is the + /// UniversalRouter contract. Otherwise, a malicious contract in the execution chain could abuse the + /// legitimate signed context by calling other contracts with it, causing unintended side effects. function signedRouteContext() external view returns (address signer, bytes32 intent, bytes32 data); } diff --git a/contracts/libraries/Commands.sol b/contracts/libraries/Commands.sol index ea8d7fb44..7861feacc 100644 --- a/contracts/libraries/Commands.sol +++ b/contracts/libraries/Commands.sol @@ -29,7 +29,7 @@ library Commands { uint256 constant UNWRAP_WETH = 0x0c; uint256 constant PERMIT2_TRANSFER_FROM_BATCH = 0x0d; uint256 constant BALANCE_CHECK_ERC20 = 0x0e; - uint256 constant UNWRAP_WETH_EXACT = 0x0f; + // COMMAND_PLACEHOLDER = 0x0f; // Command Types where 0x10<=value<=0x20, executed in the third nested-if block uint256 constant V4_SWAP = 0x10; diff --git a/contracts/modules/Payments.sol b/contracts/modules/Payments.sol index a3ffbb009..cca7f831a 100644 --- a/contracts/modules/Payments.sol +++ b/contracts/modules/Payments.sol @@ -117,17 +117,4 @@ abstract contract Payments is PaymentsImmutables { } } } - - /// @notice Unwraps an exact amount of the contract's WETH into ETH - /// @param recipient The recipient of the ETH - /// @param amount The exact amount of WETH to unwrap - function unwrapWETH9Exact(address recipient, uint256 amount) internal { - if (WETH9.balanceOf(address(this)) < amount) revert InsufficientETH(); - if (amount > 0) { - WETH9.withdraw(amount); - if (recipient != address(this)) { - recipient.safeTransferETH(amount); - } - } - } } diff --git a/contracts/modules/V3ToV4Migrator.sol b/contracts/modules/V3ToV4Migrator.sol index b720452b4..8604f3029 100644 --- a/contracts/modules/V3ToV4Migrator.sol +++ b/contracts/modules/V3ToV4Migrator.sol @@ -32,8 +32,6 @@ abstract contract V3ToV4Migrator is MigratorImmutables { /// @dev check that a call is to the ERC721 permit function function _checkV3PermitCall(bytes calldata inputs) internal pure { - if (inputs.length < 0x04) revert CalldataDecoder.SliceOutOfBounds(); - bytes4 selector; assembly { selector := calldataload(inputs.offset) @@ -46,8 +44,6 @@ abstract contract V3ToV4Migrator is MigratorImmutables { /// @dev check that the v3 position manager call is a safe call function _checkV3PositionManagerCall(bytes calldata inputs, address caller) internal view { - if (inputs.length < 0x24) revert CalldataDecoder.SliceOutOfBounds(); - bytes4 selector; assembly { selector := calldataload(inputs.offset) @@ -76,8 +72,6 @@ abstract contract V3ToV4Migrator is MigratorImmutables { /// this is because, if a user could be tricked into approving the UniversalRouter for /// their position, an attacker could take their fees, or drain their entire position function _checkV4PositionManagerCall(bytes calldata inputs) internal view { - if (inputs.length < 0x04) revert CalldataDecoder.SliceOutOfBounds(); - bytes4 selector; assembly { selector := calldataload(inputs.offset) diff --git a/contracts/modules/uniswap/v3/BytesLib.sol b/contracts/modules/uniswap/v3/BytesLib.sol index 51658b670..3e1f89a3c 100644 --- a/contracts/modules/uniswap/v3/BytesLib.sol +++ b/contracts/modules/uniswap/v3/BytesLib.sol @@ -50,43 +50,14 @@ library BytesLib { pure returns (uint256 length, uint256 offset) { - return toLengthOffset(_bytes, _arg, 0x20); - } - - /// @notice Decode the `_arg`-th element in `_bytes` as a dynamic array with fixed-size elements. - /// @param _bytes The input bytes string to slice - /// @param _arg The index of the argument to extract - /// @param _elementSize The ABI-encoded size of each array element - /// @return length Length of the array - /// @return offset Pointer to the data part of the array - function toLengthOffset(bytes calldata _bytes, uint256 _arg, uint256 _elementSize) - internal - pure - returns (uint256 length, uint256 offset) - { - assembly ('memory-safe') { + assembly { // The offset of the `_arg`-th element is `32 * arg`, which stores the offset of the length pointer. // shl(5, x) is equivalent to mul(32, x) - let headOffset := shl(5, _arg) - if or(iszero(_elementSize), gt(add(headOffset, 0x20), _bytes.length)) { - mstore(0, 0x3b99b53d) // SliceOutOfBounds() - revert(0x1c, 0x04) - } - - let relativeLengthPtr := calldataload(add(_bytes.offset, headOffset)) - if gt(relativeLengthPtr, sub(_bytes.length, 0x20)) { - mstore(0, 0x3b99b53d) // SliceOutOfBounds() - revert(0x1c, 0x04) - } - - let lengthPtr := add(_bytes.offset, relativeLengthPtr) + let lengthPtr := add(_bytes.offset, calldataload(add(_bytes.offset, shl(5, _arg)))) length := calldataload(lengthPtr) offset := add(lengthPtr, 0x20) - let relativeOffset := add(relativeLengthPtr, 0x20) - let remainingLength := sub(_bytes.length, relativeOffset) - - // Comparing before multiplication prevents length * elementSize from overflowing. - if gt(length, div(remainingLength, _elementSize)) { + let relativeOffset := sub(offset, _bytes.offset) + if lt(_bytes.length, add(shl(5, length), relativeOffset)) { mstore(0, 0x3b99b53d) // SliceOutOfBounds() revert(0x1c, 0x04) } diff --git a/lib/v4-periphery b/lib/v4-periphery index 07336f214..a7af5b345 160000 --- a/lib/v4-periphery +++ b/lib/v4-periphery @@ -1 +1 @@ -Subproject commit 07336f2144f522874e2c3c85e04d1d3f8d5fa471 +Subproject commit a7af5b345b479b05fde9182d7e40913a73b3e18f diff --git a/snapshots/UniversalRouterTest.json b/snapshots/UniversalRouterTest.json index 5ef191ad9..8b8c2f5a9 100644 --- a/snapshots/UniversalRouterTest.json +++ b/snapshots/UniversalRouterTest.json @@ -1,3 +1,3 @@ { - "UniversalRouter bytecode size": "23705" + "UniversalRouter bytecode size": "23233" } \ No newline at end of file diff --git a/test/foundry-tests/RouteSigner.t.sol b/test/foundry-tests/RouteSigner.t.sol index ab181fae9..58ffc8d2c 100644 --- a/test/foundry-tests/RouteSigner.t.sol +++ b/test/foundry-tests/RouteSigner.t.sol @@ -9,7 +9,6 @@ import {Lock} from '../../contracts/base/Lock.sol'; import {Constants} from '../../contracts/libraries/Constants.sol'; import {Commands} from '../../contracts/libraries/Commands.sol'; import {RouterParameters} from '../../contracts/types/RouterParameters.sol'; -import {BytesLib} from '../../contracts/modules/uniswap/v3/BytesLib.sol'; contract ContextCapture { UniversalRouter public router; @@ -133,92 +132,6 @@ contract RouteSignerTest is Test { signature = abi.encodePacked(r, s, v); } - function encodeExecuteSignedWithSmuggledTransfer( - bytes memory commands, - bytes32 intent, - bytes32 data, - bytes32 nonce, - bytes memory signature, - uint256 deadline, - address token, - address recipient, - uint256 value - ) internal pure returns (bytes memory callData) { - bytes[] memory executionInputs = new bytes[](1); - executionInputs[0] = abi.encode(token, recipient, value); - - callData = abi.encodeWithSelector( - IUniversalRouter.executeSigned.selector, - commands, - executionInputs, - intent, - data, - true, - nonce, - signature, - deadline - ); - - uint256 argsStart; - assembly { - argsStart := add(add(callData, 0x20), 0x04) - } - - uint256 inputsOffset; - assembly { - inputsOffset := mload(add(argsStart, 0x20)) - } - uint256 inputsStart = argsStart + inputsOffset; - - uint256 firstInputOffset; - assembly { - firstInputOffset := mload(add(inputsStart, 0x20)) - } - uint256 firstInputLengthPointer = inputsStart + 0x20 + firstInputOffset; - - assembly { - mstore(firstInputLengthPointer, 0) - } - } - - function testExecuteSignedRejectsSmuggledTransferInput() public { - bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.TRANSFER))); - bytes[] memory signedInputs = new bytes[](1); - signedInputs[0] = hex''; - - bytes32 intent = keccak256('zero-length-smuggled-input-intent'); - bytes32 data = keccak256('zero-length-smuggled-input-data'); - bytes32 nonce = keccak256('zero-length-smuggled-input-nonce'); - uint256 deadline = block.timestamp + 1000; - - bytes memory signature = signExecution(commands, signedInputs, intent, data, address(this), nonce, deadline); - bytes memory callData = encodeExecuteSignedWithSmuggledTransfer( - commands, intent, data, nonce, signature, deadline, Constants.ETH, address(capturer), AMOUNT - ); - - (bool success, bytes memory returnData) = address(router).call{value: AMOUNT}(callData); - - assertFalse(success, 'smuggled transfer should revert'); - assertEq( - keccak256(returnData), - keccak256(abi.encodeWithSelector(BytesLib.SliceOutOfBounds.selector)), - 'unexpected revert' - ); - assertEq(address(capturer).balance, 0, 'smuggled recipient should not receive ETH'); - assertFalse(router.noncesUsed(signer, nonce), 'reverted route should not consume nonce'); - } - - function testFuzzTransferRejectsShortInput(uint256 inputLength) public { - inputLength = bound(inputLength, 0, 0x5f); - - bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.TRANSFER))); - bytes[] memory inputs = new bytes[](1); - inputs[0] = new bytes(inputLength); - - vm.expectRevert(BytesLib.SliceOutOfBounds.selector); - router.execute(commands, inputs); - } - function testExecuteSigned() public { bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.TRANSFER))); bytes[] memory inputs = new bytes[](1); diff --git a/test/foundry-tests/UniversalRouter.t.sol b/test/foundry-tests/UniversalRouter.t.sol index cdb5308d6..32cd0367c 100644 --- a/test/foundry-tests/UniversalRouter.t.sol +++ b/test/foundry-tests/UniversalRouter.t.sol @@ -9,7 +9,6 @@ import {Commands} from '../../contracts/libraries/Commands.sol'; import {MockERC20} from './mock/MockERC20.sol'; import {ExampleModule} from '../../contracts/test/ExampleModule.sol'; import {RouterParameters} from '../../contracts/types/RouterParameters.sol'; -import {BytesLib} from '../../contracts/modules/uniswap/v3/BytesLib.sol'; import {ERC20} from 'solmate/src/tokens/ERC20.sol'; import 'permit2/src/interfaces/IAllowanceTransfer.sol'; import {IERC165} from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; @@ -56,106 +55,6 @@ contract UniversalRouterTest is Test { emit log_uint(bytecodeSize); } - function testRejectsShortStaticCommandInputs() public { - uint256[] memory commandTypes = new uint256[](15); - uint256[] memory minimumLengths = new uint256[](15); - - commandTypes[0] = Commands.V3_SWAP_EXACT_IN; - commandTypes[1] = Commands.V3_SWAP_EXACT_OUT; - commandTypes[2] = Commands.PERMIT2_TRANSFER_FROM; - commandTypes[3] = Commands.SWEEP; - commandTypes[4] = Commands.TRANSFER; - commandTypes[5] = Commands.PAY_PORTION; - commandTypes[6] = Commands.PAY_PORTION_FULL_PRECISION; - commandTypes[7] = Commands.V2_SWAP_EXACT_IN; - commandTypes[8] = Commands.V2_SWAP_EXACT_OUT; - commandTypes[9] = Commands.PERMIT2_PERMIT; - commandTypes[10] = Commands.WRAP_ETH; - commandTypes[11] = Commands.UNWRAP_WETH; - commandTypes[12] = Commands.BALANCE_CHECK_ERC20; - commandTypes[13] = Commands.UNWRAP_WETH_EXACT; - commandTypes[14] = Commands.V4_INITIALIZE_POOL; - - minimumLengths[0] = 0xc0; - minimumLengths[1] = 0xc0; - minimumLengths[2] = 0x60; - minimumLengths[3] = 0x60; - minimumLengths[4] = 0x60; - minimumLengths[5] = 0x60; - minimumLengths[6] = 0x60; - minimumLengths[7] = 0xc0; - minimumLengths[8] = 0xc0; - minimumLengths[9] = 0xe0; - minimumLengths[10] = 0x40; - minimumLengths[11] = 0x40; - minimumLengths[12] = 0x60; - minimumLengths[13] = 0x40; - minimumLengths[14] = 0xc0; - - bytes[] memory inputs = new bytes[](1); - for (uint256 i; i < commandTypes.length; ++i) { - bytes memory commands = abi.encodePacked(bytes1(uint8(commandTypes[i]))); - inputs[0] = new bytes(minimumLengths[i] - 1); - - vm.expectRevert(BytesLib.SliceOutOfBounds.selector); - router.execute(commands, inputs); - } - } - - function testRejectsShortDynamicCommandInputs() public { - uint256[] memory commandTypes = new uint256[](5); - commandTypes[0] = Commands.PERMIT2_PERMIT_BATCH; - commandTypes[1] = Commands.PERMIT2_TRANSFER_FROM_BATCH; - commandTypes[2] = Commands.V3_POSITION_MANAGER_PERMIT; - commandTypes[3] = Commands.V3_POSITION_MANAGER_CALL; - commandTypes[4] = Commands.V4_POSITION_MANAGER_CALL; - - bytes[] memory inputs = new bytes[](1); - inputs[0] = hex''; - - for (uint256 i; i < commandTypes.length; ++i) { - bytes memory commands = abi.encodePacked(bytes1(uint8(commandTypes[i]))); - - vm.expectRevert(BytesLib.SliceOutOfBounds.selector); - router.execute(commands, inputs); - } - } - - function testPermit2PermitBatchDecodesBoundedDetails() public { - IAllowanceTransfer.PermitDetails[] memory details = new IAllowanceTransfer.PermitDetails[](1); - details[0] = IAllowanceTransfer.PermitDetails({ - token: address(erc20), amount: 1 ether, expiration: type(uint48).max, nonce: 0 - }); - IAllowanceTransfer.PermitBatch memory permitBatch = - IAllowanceTransfer.PermitBatch({details: details, spender: address(router), sigDeadline: block.timestamp}); - - bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.PERMIT2_PERMIT_BATCH))); - bytes[] memory inputs = new bytes[](1); - inputs[0] = abi.encode(permitBatch, hex''); - - router.execute(commands, inputs); - } - - function testPermit2PermitBatchRejectsTruncatedDetails() public { - IAllowanceTransfer.PermitDetails[] memory details = new IAllowanceTransfer.PermitDetails[](1); - details[0] = IAllowanceTransfer.PermitDetails({ - token: address(erc20), amount: 1 ether, expiration: type(uint48).max, nonce: 0 - }); - IAllowanceTransfer.PermitBatch memory permitBatch = - IAllowanceTransfer.PermitBatch({details: details, spender: address(router), sigDeadline: block.timestamp}); - - bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.PERMIT2_PERMIT_BATCH))); - bytes[] memory inputs = new bytes[](1); - inputs[0] = abi.encode(permitBatch, hex''); - - assembly { - mstore(mload(add(inputs, 0x20)), 0x120) - } - - vm.expectRevert(BytesLib.SliceOutOfBounds.selector); - router.execute(commands, inputs); - } - function testSweepToken() public { bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.SWEEP))); bytes[] memory inputs = new bytes[](1); @@ -181,44 +80,6 @@ contract UniversalRouterTest is Test { router.execute(commands, inputs); } - /// @dev SWEEP declares a uint256 amountMinimum. A narrower decode would enforce only the - /// low 160 bits, silently reducing minima at or above 2**160 modulo 2**160. - function testSweepTokenEnforcesFullWidthMinimum() public { - uint256[] memory minimums = new uint256[](4); - minimums[0] = uint256(type(uint160).max) + 1; // 2**160, truncates to 0 - minimums[1] = uint256(type(uint160).max) + 1 + AMOUNT - 1; // truncates to AMOUNT - 1 - minimums[2] = uint256(type(uint160).max) + 1 + AMOUNT; // truncates to AMOUNT - minimums[3] = type(uint256).max; - - bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.SWEEP))); - - for (uint256 i = 0; i < minimums.length; i++) { - uint256 state = vm.snapshotState(); - - bytes[] memory inputs = new bytes[](1); - inputs[0] = abi.encode(address(erc20), RECIPIENT, minimums[i]); - erc20.mint(address(router), AMOUNT); - - vm.expectRevert(Payments.InsufficientToken.selector); - router.execute(commands, inputs); - - assertEq(erc20.balanceOf(RECIPIENT), 0); - assertTrue(vm.revertToState(state)); - } - } - - function testSweepTokenAtUint160MaxMinimum() public { - bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.SWEEP))); - bytes[] memory inputs = new bytes[](1); - inputs[0] = abi.encode(address(erc20), RECIPIENT, uint256(type(uint160).max)); - - erc20.mint(address(router), type(uint160).max); - - router.execute(commands, inputs); - - assertEq(erc20.balanceOf(RECIPIENT), type(uint160).max); - } - function testSweepETH() public { bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.SWEEP))); bytes[] memory inputs = new bytes[](1); diff --git a/test/foundry-tests/V4SwapWithinUnlock.t.sol b/test/foundry-tests/V4SwapWithinUnlock.t.sol deleted file mode 100644 index 0340a3b29..000000000 --- a/test/foundry-tests/V4SwapWithinUnlock.t.sol +++ /dev/null @@ -1,667 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {Test} from 'forge-std/Test.sol'; -import {Deployers} from '@uniswap/v4-core/test/utils/Deployers.sol'; -import {IPoolManager} from '@uniswap/v4-core/src/interfaces/IPoolManager.sol'; -import {IUnlockCallback} from '@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol'; -import {Currency, CurrencyLibrary} from '@uniswap/v4-core/src/types/Currency.sol'; -import {IHooks} from '@uniswap/v4-core/src/interfaces/IHooks.sol'; -import {TransientStateLibrary} from '@uniswap/v4-core/src/libraries/TransientStateLibrary.sol'; -import {MockERC20} from 'solmate/src/test/utils/mocks/MockERC20.sol'; -import {IAllowanceTransfer} from 'permit2/src/interfaces/IAllowanceTransfer.sol'; - -import {Plan, Planner} from '@uniswap/v4-periphery/test/shared/Planner.sol'; -import {Actions} from '@uniswap/v4-periphery/src/libraries/Actions.sol'; -import {ActionConstants} from '@uniswap/v4-periphery/src/libraries/ActionConstants.sol'; -import {IV4Router} from '@uniswap/v4-periphery/src/interfaces/IV4Router.sol'; - -import {UniversalRouter} from '../../contracts/UniversalRouter.sol'; -import {RouterParameters} from '../../contracts/types/RouterParameters.sol'; -import {Commands} from '../../contracts/libraries/Commands.sol'; - -contract Permit2AllowanceMock { - struct Allowance { - uint160 amount; - uint48 expiration; - } - - mapping(address owner => mapping(address token => mapping(address spender => Allowance))) internal allowances; - - function approve(address token, address spender, uint160 amount, uint48 expiration) external { - allowances[msg.sender][token][spender] = Allowance({amount: amount, expiration: expiration}); - } - - function allowance(address user, address token, address spender) - external - view - returns (uint160 amount, uint48 expiration, uint48 nonce) - { - Allowance memory allowed = allowances[user][token][spender]; - return (allowed.amount, allowed.expiration, 0); - } - - function transferFrom(address from, address to, uint160 amount, address token) external { - Allowance storage allowed = allowances[from][token][msg.sender]; - if (block.timestamp > allowed.expiration) revert IAllowanceTransfer.AllowanceExpired(allowed.expiration); - if (allowed.amount != type(uint160).max) { - if (amount > allowed.amount) revert IAllowanceTransfer.InsufficientAllowance(allowed.amount); - allowed.amount -= amount; - } - MockERC20(token).transferFrom(from, to, amount); - } -} - -/// @notice Holds the v4 PoolManager lock and calls UniversalRouter.execute from inside the unlock callback, -/// simulating a contract (e.g. a swap-and-add zap) that composes a UR swap within its own unlock. -contract LockHolder is IUnlockCallback { - IPoolManager public immutable manager; - UniversalRouter public immutable router; - - bytes internal commands; - bytes[] internal inputs; - - constructor(IPoolManager _manager, UniversalRouter _router) { - manager = _manager; - router = _router; - } - - /// @dev unlocks the manager, then (in the callback) runs the UR command set within the existing lock - function executeWithinUnlock(bytes calldata _commands, bytes[] calldata _inputs) external { - commands = _commands; - inputs = _inputs; - manager.unlock(hex''); - } - - /// @dev calls UR directly as a contract caller. This is the pre-existing non-nested composition shape. - function executeDirect(bytes calldata _commands, bytes[] calldata _inputs) external { - router.execute(_commands, _inputs); - } - - function approvePermit2(address token, IAllowanceTransfer permit2, address spender) external { - MockERC20(token).approve(address(permit2), type(uint256).max); - permit2.approve(token, spender, type(uint160).max, type(uint48).max); - } - - /// @dev unlocks, then attempts a second (nested) unlock inside the callback -> AlreadyUnlocked - function nestedUnlock() external { - manager.unlock(hex'01'); - } - - function unlockCallback(bytes calldata data) external returns (bytes memory) { - require(msg.sender == address(manager), 'only manager'); - if (data.length == 0) { - router.execute(commands, inputs); - } else { - manager.unlock(hex''); - } - return hex''; - } -} - -/// @notice Opens the v4 lock itself, then (in the callback) hands off to a DIFFERENT contract that calls -/// UniversalRouter.execute. This makes the outer lock owner (OuterLockOpener) distinct from the -/// execute caller / router locker (the LockHolder), exercising the caller-isolation invariant. -contract OuterLockOpener is IUnlockCallback { - IPoolManager public immutable manager; - - LockHolder internal caller; - bytes internal commands; - bytes[] internal inputs; - - constructor(IPoolManager _manager) { - manager = _manager; - } - - /// @dev opens the lock as owner, then delegates the UR call to `_caller` inside the callback - function openAndDelegate(LockHolder _caller, bytes calldata _commands, bytes[] calldata _inputs) external { - caller = _caller; - commands = _commands; - inputs = _inputs; - manager.unlock(hex''); - } - - function unlockCallback(bytes calldata) external returns (bytes memory) { - require(msg.sender == address(manager), 'only manager'); - caller.executeDirect(commands, inputs); - return hex''; - } -} - -/// @notice An OuterLockOpener variant that FIRST creates an open credit (positive delta) for itself in -/// `creditCurrency` via sync + transfer + settle, THEN delegates a nested UR swap whose input is that -/// same currency. It records its own credit delta before and after the nested swap so the test can -/// prove the router's nested settlement did NOT draw on the outer owner's credit. -contract CreditOuterLockOpener is IUnlockCallback { - using TransientStateLibrary for IPoolManager; - - IPoolManager public immutable manager; - - LockHolder internal caller; - bytes internal commands; - bytes[] internal inputs; - Currency internal creditCurrency; - uint256 internal creditAmount; - bool internal settleCredit; - - int256 public deltaAfterCredit; - int256 public deltaAfterSwap; - uint256 public takenBack; - - constructor(IPoolManager _manager) { - manager = _manager; - } - - function openCreateCreditAndDelegate( - LockHolder _caller, - Currency _creditCurrency, - uint256 _creditAmount, - bool _settleCredit, - bytes calldata _commands, - bytes[] calldata _inputs - ) external { - caller = _caller; - creditCurrency = _creditCurrency; - creditAmount = _creditAmount; - settleCredit = _settleCredit; - commands = _commands; - inputs = _inputs; - manager.unlock(hex''); - } - - function unlockCallback(bytes calldata) external returns (bytes memory) { - require(msg.sender == address(manager), 'only manager'); - - // 1. Move creditAmount of creditCurrency into the manager. When settleCredit is true this is a full, - // correctly-formed OPEN CREDIT (sync + transfer + settle) that gives us a +creditAmount per-address - // delta. When false, we sync + transfer but SKIP settle: the tokens are "in flight" (synced into the - // reserves baseline but not yet credited to anyone) — the "settle credits whoever calls it next" case. - manager.sync(creditCurrency); - MockERC20(Currency.unwrap(creditCurrency)).transfer(address(manager), creditAmount); - if (settleCredit) manager.settle(); - - deltaAfterCredit = manager.currencyDelta(address(this), creditCurrency); - - // 2. Run a normal, balanced nested V4_SWAP whose input is the SAME currency. Funded by the execute - // caller / router — never by this opener. - caller.executeDirect(commands, inputs); - - // 3. Observe our own credit AFTER the nested swap+settle. If the router drained it, this drops. - deltaAfterSwap = manager.currencyDelta(address(this), creditCurrency); - - // 4. Take our credit back so the unlock can close with zero deltas for us. - if (deltaAfterSwap > 0) { - takenBack = uint256(deltaAfterSwap); - manager.take(creditCurrency, address(this), takenBack); - } - return hex''; - } -} - -contract V4SwapWithinUnlockTest is Test, Deployers { - UniversalRouter router; - LockHolder lockHolder; - OuterLockOpener outerLockOpener; - CreditOuterLockOpener creditOpener; - IAllowanceTransfer permit2; - - address constant RECIPIENT = address(0xBEEF); - uint128 constant AMOUNT_IN = 1e18; - uint256 constant CREDIT_AMOUNT = 7e17; // distinct from AMOUNT_IN so a cross-draw would show as a wrong number - - function setUp() public { - deployFreshManagerAndRouters(); - (currency0, currency1) = deployMintAndApprove2Currencies(); - (key,) = initPoolAndAddLiquidity(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1); - permit2 = IAllowanceTransfer(address(new Permit2AllowanceMock())); - - RouterParameters memory params = RouterParameters({ - permit2: address(permit2), - weth9: address(0), - v2Factory: address(0), - v3Factory: address(0), - pairInitCodeHash: bytes32(0), - poolInitCodeHash: bytes32(0), - v4PoolManager: address(manager), - permissionsAdapterFactory: address(0), - v3NFTPositionManager: address(0), - v4PositionManager: address(0), - spokePool: address(0) - }); - router = new UniversalRouter(params); - lockHolder = new LockHolder(IPoolManager(address(manager)), router); - outerLockOpener = new OuterLockOpener(IPoolManager(address(manager))); - creditOpener = new CreditOuterLockOpener(IPoolManager(address(manager))); - } - - /// @dev builds a single V4_SWAP command: exact-in currency0 -> currency1, paying the input from the - /// router's own balance (payerIsUser = false) and sending the output to RECIPIENT. - function _v4SwapCommand() internal view returns (bytes memory commands, bytes[] memory inputs) { - return _v4SwapCommand(RECIPIENT, false); - } - - function _v4SwapCommand(address recipient, bool payerIsUser) - internal - view - returns (bytes memory commands, bytes[] memory inputs) - { - IV4Router.ExactInputSingleParams memory sp = IV4Router.ExactInputSingleParams({ - poolKey: key, zeroForOne: true, amountIn: AMOUNT_IN, amountOutMinimum: 0, minHopPriceX36: 0, hookData: hex'' - }); - - Plan memory plan = Planner.init(); - plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(sp)); - plan = plan.add(Actions.SETTLE, abi.encode(currency0, ActionConstants.OPEN_DELTA, payerIsUser)); - plan = plan.add(Actions.TAKE, abi.encode(currency1, recipient, ActionConstants.OPEN_DELTA)); - - commands = abi.encodePacked(bytes1(uint8(Commands.V4_SWAP))); - inputs = new bytes[](1); - inputs[0] = plan.encode(); - } - - function _fundRouter() internal { - MockERC20(Currency.unwrap(currency0)).transfer(address(router), AMOUNT_IN); - } - - function _fundLockHolderAndApprovePermit2() internal { - MockERC20(Currency.unwrap(currency0)).transfer(address(lockHolder), AMOUNT_IN); - lockHolder.approvePermit2(Currency.unwrap(currency0), permit2, address(router)); - } - - /// @dev an intentionally unbalanced plan: swaps currency0 -> currency1 and takes the output, but never - /// settles the currency0 input debt, leaving a dangling delta at unlock close. - function _v4SwapCommandMissingSettle() internal view returns (bytes memory commands, bytes[] memory inputs) { - IV4Router.ExactInputSingleParams memory sp = IV4Router.ExactInputSingleParams({ - poolKey: key, zeroForOne: true, amountIn: AMOUNT_IN, amountOutMinimum: 0, minHopPriceX36: 0, hookData: hex'' - }); - - Plan memory plan = Planner.init(); - plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(sp)); - plan = plan.add(Actions.TAKE, abi.encode(currency1, RECIPIENT, ActionConstants.OPEN_DELTA)); - - commands = abi.encodePacked(bytes1(uint8(Commands.V4_SWAP))); - inputs = new bytes[](1); - inputs[0] = plan.encode(); - } - - /// @dev a balanced plan that composes the swap with the *_ALL settle/take variants (payer/recipient bound - /// to msgSender), proving the nested path handles multi-action plans other than the OPEN_DELTA shape. - function _v4SwapCommandSettleAllTakeAll() internal view returns (bytes memory commands, bytes[] memory inputs) { - IV4Router.ExactInputSingleParams memory sp = IV4Router.ExactInputSingleParams({ - poolKey: key, zeroForOne: true, amountIn: AMOUNT_IN, amountOutMinimum: 0, minHopPriceX36: 0, hookData: hex'' - }); - - Plan memory plan = Planner.init(); - plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(sp)); - plan = plan.add(Actions.SETTLE_ALL, abi.encode(currency0, type(uint256).max)); - plan = plan.add(Actions.TAKE_ALL, abi.encode(currency1, uint256(0))); - - commands = abi.encodePacked(bytes1(uint8(Commands.V4_SWAP))); - inputs = new bytes[](1); - inputs[0] = plan.encode(); - } - - /// @notice The new behavior: a V4_SWAP runs within an already-open lock via _executeActionsWithoutUnlock. - function test_v4Swap_withinExistingUnlock_succeeds() public { - _fundRouter(); - (bytes memory commands, bytes[] memory inputs) = _v4SwapCommand(); - - assertEq(MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT), 0); - lockHolder.executeWithinUnlock(commands, inputs); - assertGt(MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT), 0, 'swap did not execute in lock'); - } - - /// @notice Regression: top-level (manager not yet unlocked) still opens its own lock as before. - function test_v4Swap_topLevel_stillWorks() public { - _fundRouter(); - (bytes memory commands, bytes[] memory inputs) = _v4SwapCommand(); - - assertEq(MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT), 0); - router.execute(commands, inputs); - assertGt(MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT), 0, 'top-level swap failed'); - } - - /// @notice In nested execution, MSG_SENDER resolves to the lock-holder contract, not the EOA/test contract. - function test_v4Swap_withinExistingUnlock_msgSenderRecipientIsLockHolder() public { - _fundRouter(); - (bytes memory commands, bytes[] memory inputs) = _v4SwapCommand(ActionConstants.MSG_SENDER, false); - - uint256 lockHolderBalanceBefore = MockERC20(Currency.unwrap(currency1)).balanceOf(address(lockHolder)); - uint256 testBalanceBefore = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); - lockHolder.executeWithinUnlock(commands, inputs); - - assertGt( - MockERC20(Currency.unwrap(currency1)).balanceOf(address(lockHolder)), - lockHolderBalanceBefore, - 'lock-holder did not receive' - ); - assertEq( - MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)), - testBalanceBefore, - 'EOA/test received output' - ); - } - - /// @notice Direct contract-mediated UR calls had the same MSG_SENDER semantics before this branch. - function test_v4Swap_topLevelContractCaller_msgSenderRecipientIsLockHolder() public { - _fundRouter(); - (bytes memory commands, bytes[] memory inputs) = _v4SwapCommand(ActionConstants.MSG_SENDER, false); - - uint256 lockHolderBalanceBefore = MockERC20(Currency.unwrap(currency1)).balanceOf(address(lockHolder)); - uint256 testBalanceBefore = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); - lockHolder.executeDirect(commands, inputs); - - assertGt( - MockERC20(Currency.unwrap(currency1)).balanceOf(address(lockHolder)), - lockHolderBalanceBefore, - 'lock-holder did not receive' - ); - assertEq( - MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)), - testBalanceBefore, - 'EOA/test received output' - ); - } - - /// @notice In nested execution, payerIsUser pulls from the lock-holder through Permit2. - function test_v4Swap_withinExistingUnlock_payerIsUserPullsFromLockHolder() public { - _fundLockHolderAndApprovePermit2(); - (bytes memory commands, bytes[] memory inputs) = _v4SwapCommand(RECIPIENT, true); - - uint256 lockHolderBalanceBefore = MockERC20(Currency.unwrap(currency0)).balanceOf(address(lockHolder)); - uint256 recipientBalanceBefore = MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT); - lockHolder.executeWithinUnlock(commands, inputs); - - assertLt( - MockERC20(Currency.unwrap(currency0)).balanceOf(address(lockHolder)), - lockHolderBalanceBefore, - 'lock-holder did not pay' - ); - assertGt( - MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT), - recipientBalanceBefore, - 'recipient did not receive output' - ); - } - - /// @notice Direct contract-mediated UR calls had the same payerIsUser semantics before this branch. - function test_v4Swap_topLevelContractCaller_payerIsUserPullsFromLockHolder() public { - _fundLockHolderAndApprovePermit2(); - (bytes memory commands, bytes[] memory inputs) = _v4SwapCommand(RECIPIENT, true); - - uint256 lockHolderBalanceBefore = MockERC20(Currency.unwrap(currency0)).balanceOf(address(lockHolder)); - uint256 recipientBalanceBefore = MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT); - lockHolder.executeDirect(commands, inputs); - - assertLt( - MockERC20(Currency.unwrap(currency0)).balanceOf(address(lockHolder)), - lockHolderBalanceBefore, - 'lock-holder did not pay' - ); - assertGt( - MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT), - recipientBalanceBefore, - 'recipient did not receive output' - ); - } - - /// @notice Security invariant: when a foreign contract owns the lock, output still binds to the execute - /// caller (the router locker), NOT the lock owner. Recipient MSG_SENDER resolves to the LockHolder. - function test_v4Swap_withinExistingUnlock_callerIsolation_recipientBindsToExecuteCaller() public { - _fundRouter(); - (bytes memory commands, bytes[] memory inputs) = _v4SwapCommand(ActionConstants.MSG_SENDER, false); - - uint256 lockHolderBalanceBefore = MockERC20(Currency.unwrap(currency1)).balanceOf(address(lockHolder)); - uint256 openerBalanceBefore = MockERC20(Currency.unwrap(currency1)).balanceOf(address(outerLockOpener)); - outerLockOpener.openAndDelegate(lockHolder, commands, inputs); - - assertGt( - MockERC20(Currency.unwrap(currency1)).balanceOf(address(lockHolder)), - lockHolderBalanceBefore, - 'execute caller did not receive output' - ); - assertEq( - MockERC20(Currency.unwrap(currency1)).balanceOf(address(outerLockOpener)), - openerBalanceBefore, - 'lock owner received output' - ); - } - - /// @notice Security invariant: a foreign contract calling execute inside someone else's lock cannot spend the - /// lock owner's funds. Input is pulled from the execute caller (LockHolder), opener stays untouched. - function test_v4Swap_withinExistingUnlock_callerIsolation_payerBindsToExecuteCaller() public { - _fundLockHolderAndApprovePermit2(); - (bytes memory commands, bytes[] memory inputs) = _v4SwapCommand(ActionConstants.MSG_SENDER, true); - - uint256 lockHolderInputBefore = MockERC20(Currency.unwrap(currency0)).balanceOf(address(lockHolder)); - uint256 lockHolderOutputBefore = MockERC20(Currency.unwrap(currency1)).balanceOf(address(lockHolder)); - uint256 openerInputBefore = MockERC20(Currency.unwrap(currency0)).balanceOf(address(outerLockOpener)); - uint256 openerOutputBefore = MockERC20(Currency.unwrap(currency1)).balanceOf(address(outerLockOpener)); - outerLockOpener.openAndDelegate(lockHolder, commands, inputs); - - assertLt( - MockERC20(Currency.unwrap(currency0)).balanceOf(address(lockHolder)), - lockHolderInputBefore, - 'execute caller did not pay' - ); - assertGt( - MockERC20(Currency.unwrap(currency1)).balanceOf(address(lockHolder)), - lockHolderOutputBefore, - 'execute caller did not receive output' - ); - assertEq( - MockERC20(Currency.unwrap(currency0)).balanceOf(address(outerLockOpener)), - openerInputBefore, - 'lock owner input was spent' - ); - assertEq( - MockERC20(Currency.unwrap(currency1)).balanceOf(address(outerLockOpener)), - openerOutputBefore, - 'lock owner received output' - ); - } - - /// @notice Security invariant: the nested path cannot leave a dangling delta silently covered by the outer - /// lock; an unsettled input debt reverts the whole unlock with CurrencyNotSettled. - function test_v4Swap_withinExistingUnlock_unbalancedDeltaReverts() public { - _fundRouter(); - (bytes memory commands, bytes[] memory inputs) = _v4SwapCommandMissingSettle(); - - vm.expectRevert(IPoolManager.CurrencyNotSettled.selector); - lockHolder.executeWithinUnlock(commands, inputs); - } - - /// @notice The nested path composes a multi-action plan (swap + SETTLE_ALL + TAKE_ALL) correctly. - function test_v4Swap_withinExistingUnlock_composedWithSecondAction() public { - _fundLockHolderAndApprovePermit2(); - (bytes memory commands, bytes[] memory inputs) = _v4SwapCommandSettleAllTakeAll(); - - uint256 inputBefore = MockERC20(Currency.unwrap(currency0)).balanceOf(address(lockHolder)); - uint256 outputBefore = MockERC20(Currency.unwrap(currency1)).balanceOf(address(lockHolder)); - lockHolder.executeWithinUnlock(commands, inputs); - - assertLt(MockERC20(Currency.unwrap(currency0)).balanceOf(address(lockHolder)), inputBefore, 'input not settled'); - assertGt(MockERC20(Currency.unwrap(currency1)).balanceOf(address(lockHolder)), outputBefore, 'output not taken'); - } - - /// @notice Native ETH as one side of a nested V4_SWAP: swaps native -> currency1 inside an already-open - /// lock. The native input is settled from the router's own ETH balance (payerIsUser = false, so no - /// msg.value forwarding through the LockHolder entrypoint) and the ERC20 output is taken to RECIPIENT. - /// Proves native settlement and take resolve correctly in the nested (reused-lock) path. - function test_v4Swap_withinExistingUnlock_nativeEth() public { - // stand up a native <-> currency1 pool (native is currency0 == ADDRESS_ZERO) - vm.deal(address(this), 1 ether); - (nativeKey,) = initPoolAndAddLiquidityETH( - CurrencyLibrary.ADDRESS_ZERO, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, 1 ether - ); - - // pre-fund the router so the nested SETTLE pays the native input from the router's own balance - vm.deal(address(router), AMOUNT_IN); - - IV4Router.ExactInputSingleParams memory sp = IV4Router.ExactInputSingleParams({ - poolKey: nativeKey, - zeroForOne: true, - amountIn: AMOUNT_IN, - amountOutMinimum: 0, - minHopPriceX36: 0, - hookData: hex'' - }); - - Plan memory plan = Planner.init(); - plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(sp)); - plan = plan.add(Actions.SETTLE, abi.encode(nativeKey.currency0, ActionConstants.OPEN_DELTA, false)); - plan = plan.add(Actions.TAKE, abi.encode(nativeKey.currency1, RECIPIENT, ActionConstants.OPEN_DELTA)); - - bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.V4_SWAP))); - bytes[] memory inputs = new bytes[](1); - inputs[0] = plan.encode(); - - uint256 recipientBefore = MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT); - uint256 routerEthBefore = address(router).balance; - - lockHolder.executeWithinUnlock(commands, inputs); - - assertLt(address(router).balance, routerEthBefore, 'native input not settled from router balance'); - assertGt( - MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT), - recipientBefore, - 'recipient did not receive erc20 output for native-input nested swap' - ); - } - - /// @notice Documents the constraint the branch works around: a raw nested unlock reverts. - function test_rawNestedUnlock_reverts() public { - vm.expectRevert(); - lockHolder.nestedUnlock(); - } - - /// @notice SECURITY: nested settlement is per-address. The outer lock owner holds an open credit in the - /// swap-input currency; a nested UR swap in that same currency, funded from the ROUTER's own balance, - /// settles only its own debt and must NEVER consume the outer owner's open credit. - function test_v4Swap_withinExistingUnlock_doesNotDrainOuterOwnerCredit_routerFunded() public { - _fundRouter(); - // fund the outer opener with the tokens it will lock up as its own credit - MockERC20(Currency.unwrap(currency0)).transfer(address(creditOpener), CREDIT_AMOUNT); - - (bytes memory commands, bytes[] memory inputs) = _v4SwapCommand(RECIPIENT, false); - - uint256 openerC0Before = MockERC20(Currency.unwrap(currency0)).balanceOf(address(creditOpener)); - uint256 openerC1Before = MockERC20(Currency.unwrap(currency1)).balanceOf(address(creditOpener)); - uint256 routerC0Before = MockERC20(Currency.unwrap(currency0)).balanceOf(address(router)); - uint256 recipientC1Before = MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT); - - creditOpener.openCreateCreditAndDelegate(lockHolder, currency0, CREDIT_AMOUNT, true, commands, inputs); - - // The opener created exactly CREDIT_AMOUNT of credit ... - assertEq(creditOpener.deltaAfterCredit(), int256(CREDIT_AMOUNT), 'credit not created as expected'); - // ... and after the nested router swap+settle, the credit is STILL exactly CREDIT_AMOUNT (untouched). - assertEq(creditOpener.deltaAfterSwap(), int256(CREDIT_AMOUNT), 'router drained the outer owner credit'); - assertEq(creditOpener.takenBack(), CREDIT_AMOUNT, 'outer owner recovered a different amount than its credit'); - - // Outer owner net token flow: locked CREDIT_AMOUNT, took the same back -> exactly whole, no gain/loss. - assertEq( - MockERC20(Currency.unwrap(currency0)).balanceOf(address(creditOpener)), - openerC0Before, - 'outer owner currency0 balance changed' - ); - assertEq( - MockERC20(Currency.unwrap(currency1)).balanceOf(address(creditOpener)), - openerC1Before, - 'outer owner received swap output' - ); - - // The router funded the input from its own balance (thin-liquidity pool truncates the exact-in swap at - // the price limit, so only part of AMOUNT_IN is consumed; assert directionally), RECIPIENT got the output. - assertLt( - MockERC20(Currency.unwrap(currency0)).balanceOf(address(router)), - routerC0Before, - 'router did not fund the swap input from its own balance' - ); - assertGt( - MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT), - recipientC1Before, - 'recipient did not receive swap output' - ); - } - - /// @notice SECURITY: same per-address invariant, but the nested swap input is pulled from the EXECUTE CALLER - /// (LockHolder) via Permit2 (payerIsUser = true). The outer lock owner's open credit is fully - /// preserved regardless of the funding source; nested settlement never draws on it. - function test_v4Swap_withinExistingUnlock_doesNotDrainOuterOwnerCredit_callerFunded() public { - _fundLockHolderAndApprovePermit2(); - // fund the outer opener with its own credit tokens - MockERC20(Currency.unwrap(currency0)).transfer(address(creditOpener), CREDIT_AMOUNT); - - (bytes memory commands, bytes[] memory inputs) = _v4SwapCommand(RECIPIENT, true); - - uint256 openerC0Before = MockERC20(Currency.unwrap(currency0)).balanceOf(address(creditOpener)); - uint256 lockHolderC0Before = MockERC20(Currency.unwrap(currency0)).balanceOf(address(lockHolder)); - uint256 recipientC1Before = MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT); - - creditOpener.openCreateCreditAndDelegate(lockHolder, currency0, CREDIT_AMOUNT, true, commands, inputs); - - assertEq(creditOpener.deltaAfterCredit(), int256(CREDIT_AMOUNT), 'credit not created as expected'); - assertEq(creditOpener.deltaAfterSwap(), int256(CREDIT_AMOUNT), 'router drained the outer owner credit'); - assertEq(creditOpener.takenBack(), CREDIT_AMOUNT, 'outer owner recovered a different amount than its credit'); - - // Outer owner whole; input came from the locker, not the outer owner. - assertEq( - MockERC20(Currency.unwrap(currency0)).balanceOf(address(creditOpener)), - openerC0Before, - 'outer owner currency0 balance changed' - ); - assertLt( - MockERC20(Currency.unwrap(currency0)).balanceOf(address(lockHolder)), - lockHolderC0Before, - 'locker did not fund the swap input' - ); - assertGt( - MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT), - recipientC1Before, - 'recipient did not receive swap output' - ); - } - - /// @notice SECURITY (sync/reserves flash-accounting angle): nested settlement is per-address even for - /// "in-flight" tokens. The outer owner syncs + transfers the swap-input currency into the manager but - /// does NOT settle, leaving tokens in the reserves baseline that a naive settle() would credit to - /// whoever calls it next. The nested UR swap still funds its own settlement from the ROUTER's balance - /// and never free-rides on those in-flight tokens (DeltaResolver._settle re-syncs before its transfer). - function test_v4Swap_withinExistingUnlock_doesNotCaptureOuterOwnerInFlightSyncedTokens() public { - _fundRouter(); - MockERC20(Currency.unwrap(currency0)).transfer(address(creditOpener), CREDIT_AMOUNT); - - (bytes memory commands, bytes[] memory inputs) = _v4SwapCommand(RECIPIENT, false); - - uint256 routerC0Before = MockERC20(Currency.unwrap(currency0)).balanceOf(address(router)); - uint256 routerC1Before = MockERC20(Currency.unwrap(currency1)).balanceOf(address(router)); - uint256 recipientC1Before = MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT); - - // settleCredit = false: opener syncs + transfers but does not settle (tokens left in flight). - creditOpener.openCreateCreditAndDelegate(lockHolder, currency0, CREDIT_AMOUNT, false, commands, inputs); - - // The opener never settled, so it holds no positive delta before or after the swap, and takes nothing back. - assertEq(creditOpener.deltaAfterCredit(), int256(0), 'unsettled transfer wrongly credited the opener'); - assertEq(creditOpener.deltaAfterSwap(), int256(0), 'opener delta changed across the nested swap'); - assertEq(creditOpener.takenBack(), 0, 'opener took tokens it never had credit for'); - - // The router funded its settlement from its OWN balance (dropped): it did not free-ride on the in-flight - // tokens. If settle() had captured the outer owner's in-flight tokens, the router would not have paid. - assertLt( - MockERC20(Currency.unwrap(currency0)).balanceOf(address(router)), - routerC0Before, - 'router did not fund settlement from its own balance (may have captured in-flight tokens)' - ); - // The router did not walk away with any of the outer owner's currency0 either. - assertEq( - MockERC20(Currency.unwrap(currency1)).balanceOf(address(router)), - routerC1Before, - 'router unexpectedly accrued currency1' - ); - assertGt( - MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT), - recipientC1Before, - 'recipient did not receive swap output' - ); - } -} diff --git a/test/integration-tests/UniversalRouter.test.ts b/test/integration-tests/UniversalRouter.test.ts index 34299d66f..217a3017c 100644 --- a/test/integration-tests/UniversalRouter.test.ts +++ b/test/integration-tests/UniversalRouter.test.ts @@ -103,78 +103,6 @@ describe('UniversalRouter', () => { .withArgs(parseInt(invalidCommand)) }) - describe('UNWRAP_WETH_EXACT', () => { - it('unwraps an exact partial amount and leaves the remainder as WETH', async () => { - const total = expandTo18DecimalsBN(3) - const amount = expandTo18DecimalsBN(1) - await wethContract.transfer(router.address, total) - - planner.addCommand(CommandType.UNWRAP_WETH_EXACT, [ADDRESS_THIS, amount]) - const { commands, inputs } = planner - - const ethBalanceBefore = await ethers.provider.getBalance(router.address) - await router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE) - - expect(await wethContract.balanceOf(router.address)).to.eq(total.sub(amount)) - expect(await ethers.provider.getBalance(router.address)).to.eq(ethBalanceBefore.add(amount)) - }) - - it('sends ETH to an external recipient', async () => { - const total = expandTo18DecimalsBN(3) - const amount = expandTo18DecimalsBN(1) - const recipient = (await ethers.getSigners())[1] - await wethContract.transfer(router.address, total) - - planner.addCommand(CommandType.UNWRAP_WETH_EXACT, [recipient.address, amount]) - const { commands, inputs } = planner - - const ethBalanceBefore = await ethers.provider.getBalance(recipient.address) - await router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE) - - expect(await wethContract.balanceOf(router.address)).to.eq(total.sub(amount)) - expect(await ethers.provider.getBalance(recipient.address)).to.eq(ethBalanceBefore.add(amount)) - }) - - it('unwraps the full balance when amount equals the WETH balance', async () => { - const total = expandTo18DecimalsBN(3) - await wethContract.transfer(router.address, total) - - planner.addCommand(CommandType.UNWRAP_WETH_EXACT, [ADDRESS_THIS, total]) - const { commands, inputs } = planner - - const ethBalanceBefore = await ethers.provider.getBalance(router.address) - await router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE) - - expect(await wethContract.balanceOf(router.address)).to.eq(0) - expect(await ethers.provider.getBalance(router.address)).to.eq(ethBalanceBefore.add(total)) - }) - - it('does nothing when amount is zero', async () => { - const total = expandTo18DecimalsBN(3) - await wethContract.transfer(router.address, total) - - planner.addCommand(CommandType.UNWRAP_WETH_EXACT, [ADDRESS_THIS, 0]) - const { commands, inputs } = planner - - const ethBalanceBefore = await ethers.provider.getBalance(router.address) - await router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE) - - expect(await wethContract.balanceOf(router.address)).to.eq(total) - expect(await ethers.provider.getBalance(router.address)).to.eq(ethBalanceBefore) - }) - - it('reverts when the amount exceeds the router WETH balance', async () => { - await wethContract.transfer(router.address, expandTo18DecimalsBN(3)) - - planner.addCommand(CommandType.UNWRAP_WETH_EXACT, [ADDRESS_THIS, expandTo18DecimalsBN(4)]) - const { commands, inputs } = planner - - await expect( - router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE) - ).to.be.revertedWithCustomError(router, 'InsufficientETH') - }) - }) - it('reverts if paying a portion over 100% of contract balance', async () => { await daiContract.transfer(router.address, expandTo18DecimalsBN(1)) planner.addCommand(CommandType.PAY_PORTION, [WETH.address, alice.address, 11_000]) diff --git a/test/integration-tests/gas-tests/Payments.gas.test.ts b/test/integration-tests/gas-tests/Payments.gas.test.ts index 9be0f020c..b861a2bf3 100644 --- a/test/integration-tests/gas-tests/Payments.gas.test.ts +++ b/test/integration-tests/gas-tests/Payments.gas.test.ts @@ -61,17 +61,6 @@ describe('Payments Gas Tests', () => { await snapshotGasCost(router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE)) }) - it('gas: UNWRAP_WETH_EXACT partial amount', async () => { - const total: BigNumber = expandTo18DecimalsBN(3) - const amount: BigNumber = expandTo18DecimalsBN(1) - await wethContract.transfer(router.address, total) - - planner.addCommand(CommandType.UNWRAP_WETH_EXACT, [alice.address, amount]) - const { commands, inputs } = planner - - await snapshotGasCost(router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE)) - }) - it('gas: TRANSFER with ETH', async () => { // seed router with WETH and unwrap it into the router const amount: BigNumber = expandTo18DecimalsBN(3) diff --git a/test/integration-tests/gas-tests/__snapshots__/CheckOwnership.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/CheckOwnership.gas.test.ts.snap index be7aae4b0..45c827d4d 100644 --- a/test/integration-tests/gas-tests/__snapshots__/CheckOwnership.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/CheckOwnership.gas.test.ts.snap @@ -3,6 +3,6 @@ exports[`Check Ownership Gas gas: balance check ERC20 1`] = ` Object { "calldataByteLength": 356, - "gasUsed": 38007, + "gasUsed": 37904, } `; diff --git a/test/integration-tests/gas-tests/__snapshots__/Payments.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/Payments.gas.test.ts.snap index 1aeb7a882..30211bacf 100644 --- a/test/integration-tests/gas-tests/__snapshots__/Payments.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/Payments.gas.test.ts.snap @@ -3,55 +3,48 @@ exports[`Payments Gas Tests Individual Command Tests gas: SWEEP with ERC20 1`] = ` Object { "calldataByteLength": 356, - "gasUsed": 37446, + "gasUsed": 37406, } `; exports[`Payments Gas Tests Individual Command Tests gas: SWEEP_WITH_FEE 1`] = ` Object { "calldataByteLength": 516, - "gasUsed": 66240, + "gasUsed": 66186, } `; exports[`Payments Gas Tests Individual Command Tests gas: TRANSFER with ERC20 1`] = ` Object { "calldataByteLength": 356, - "gasUsed": 36360, + "gasUsed": 36205, } `; exports[`Payments Gas Tests Individual Command Tests gas: TRANSFER with ETH 1`] = ` Object { "calldataByteLength": 356, - "gasUsed": 31922, + "gasUsed": 31764, } `; exports[`Payments Gas Tests Individual Command Tests gas: UNWRAP_WETH 1`] = ` Object { "calldataByteLength": 324, - "gasUsed": 44972, -} -`; - -exports[`Payments Gas Tests Individual Command Tests gas: UNWRAP_WETH_EXACT partial amount 1`] = ` -Object { - "calldataByteLength": 324, - "gasUsed": 49898, + "gasUsed": 44998, } `; exports[`Payments Gas Tests Individual Command Tests gas: UNWRAP_WETH_WITH_FEE 1`] = ` Object { "calldataByteLength": 644, - "gasUsed": 51551, + "gasUsed": 51496, } `; exports[`Payments Gas Tests Individual Command Tests gas: WRAP_ETH 1`] = ` Object { "calldataByteLength": 324, - "gasUsed": 53748, + "gasUsed": 53675, } `; diff --git a/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap index 98646802b..a0520f2ef 100644 --- a/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap @@ -3,105 +3,105 @@ exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, both fail but the transaction succeeds 1`] = ` Object { "calldataByteLength": 1956, - "gasUsed": 277806, + "gasUsed": 279274, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, neither fails 1`] = ` Object { "calldataByteLength": 1956, - "gasUsed": 253499, + "gasUsed": 254932, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, second sub plan fails 1`] = ` Object { "calldataByteLength": 1956, - "gasUsed": 253499, + "gasUsed": 254932, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, the first fails 1`] = ` Object { "calldataByteLength": 1956, - "gasUsed": 277806, + "gasUsed": 279274, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Interleaving routes gas: V2, then V3 1`] = ` Object { "calldataByteLength": 964, - "gasUsed": 193723, + "gasUsed": 195036, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Interleaving routes gas: V3, then V2 1`] = ` Object { "calldataByteLength": 964, - "gasUsed": 181260, + "gasUsed": 182301, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V2 different routes, different input tokens, each two hop, with batch permit 1`] = ` Object { "calldataByteLength": 1668, - "gasUsed": 301746, + "gasUsed": 303496, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V2 different routes, each two hop, with explicit permit 1`] = ` Object { "calldataByteLength": 1348, - "gasUsed": 312299, + "gasUsed": 313888, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V2 different routes, each two hop, with explicit permit transfer from batch 1`] = ` Object { "calldataByteLength": 1412, - "gasUsed": 313394, + "gasUsed": 315213, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V2 different routes, each two hop, without explicit permit 1`] = ` Object { "calldataByteLength": 1028, - "gasUsed": 308591, + "gasUsed": 310470, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V3, one hop 1`] = ` Object { "calldataByteLength": 1124, - "gasUsed": 181437, + "gasUsed": 182591, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V3, one hop, ADDRESS_THIS flag 1`] = ` Object { "calldataByteLength": 1124, - "gasUsed": 181212, + "gasUsed": 182366, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ETH split V2 and V3, exactOut, one hop 1`] = ` Object { "calldataByteLength": 1092, - "gasUsed": 196771, + "gasUsed": 197587, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ETH split V2 and V3, one hop 1`] = ` Object { "calldataByteLength": 1092, - "gasUsed": 189270, + "gasUsed": 190490, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ETH --> ERC20 split V2 and V3, one hop 1`] = ` Object { "calldataByteLength": 1252, - "gasUsed": 196186, + "gasUsed": 197284, } `; @@ -143,98 +143,98 @@ Object { exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn trade, where an output fee is taken 1`] = ` Object { "calldataByteLength": 900, - "gasUsed": 128705, + "gasUsed": 129343, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, one hop 1`] = ` Object { "calldataByteLength": 580, - "gasUsed": 108605, + "gasUsed": 109217, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, three hops 1`] = ` Object { "calldataByteLength": 644, - "gasUsed": 244183, + "gasUsed": 245370, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, three hops, no deadline 1`] = ` Object { "calldataByteLength": 612, - "gasUsed": 243925, + "gasUsed": 245112, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, two hops 1`] = ` Object { "calldataByteLength": 612, - "gasUsed": 176431, + "gasUsed": 177333, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, two hops, MSG_SENDER flag 1`] = ` Object { "calldataByteLength": 612, - "gasUsed": 176431, + "gasUsed": 177333, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, one hop 1`] = ` Object { "calldataByteLength": 580, - "gasUsed": 108157, + "gasUsed": 108370, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, three hops 1`] = ` Object { "calldataByteLength": 644, - "gasUsed": 249492, + "gasUsed": 250378, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, two hops 1`] = ` Object { "calldataByteLength": 612, - "gasUsed": 178895, + "gasUsed": 179439, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ETH gas: exactIn, one trade, one hop 1`] = ` Object { "calldataByteLength": 708, - "gasUsed": 125019, + "gasUsed": 125737, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ETH gas: exactOut, one trade, one hop 1`] = ` Object { "calldataByteLength": 868, - "gasUsed": 129994, + "gasUsed": 130389, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ETH gas: exactOut, with ETH fee 1`] = ` Object { "calldataByteLength": 1028, - "gasUsed": 137977, + "gasUsed": 138227, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ETH --> ERC20 gas: exactIn, one trade, one hop 1`] = ` Object { "calldataByteLength": 708, - "gasUsed": 108525, + "gasUsed": 109045, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ETH --> ERC20 gas: exactOut, one trade, one hop 1`] = ` Object { "calldataByteLength": 836, - "gasUsed": 126974, + "gasUsed": 127269, } `; @@ -283,69 +283,69 @@ Object { exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, one hop 1`] = ` Object { "calldataByteLength": 580, - "gasUsed": 108360, + "gasUsed": 108782, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, three hops 1`] = ` Object { "calldataByteLength": 612, - "gasUsed": 260089, + "gasUsed": 261915, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, two hops 1`] = ` Object { "calldataByteLength": 612, - "gasUsed": 181602, + "gasUsed": 182727, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, one hop 1`] = ` Object { "calldataByteLength": 580, - "gasUsed": 116023, + "gasUsed": 116428, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, three hops 1`] = ` Object { "calldataByteLength": 612, - "gasUsed": 255199, + "gasUsed": 256770, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, two hops 1`] = ` Object { "calldataByteLength": 612, - "gasUsed": 177310, + "gasUsed": 178298, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ETH gas: exactIn swap 1`] = ` Object { "calldataByteLength": 708, - "gasUsed": 124822, + "gasUsed": 125350, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ETH gas: exactOut swap 1`] = ` Object { "calldataByteLength": 708, - "gasUsed": 132557, + "gasUsed": 133068, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ETH --> ERC20 gas: exactIn swap 1`] = ` Object { "calldataByteLength": 708, - "gasUsed": 218192, + "gasUsed": 218591, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ETH --> ERC20 gas: exactOut swap 1`] = ` Object { "calldataByteLength": 836, - "gasUsed": 127718, + "gasUsed": 128205, } `; diff --git a/test/integration-tests/gas-tests/__snapshots__/UniversalVSSwapRouter.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/UniversalVSSwapRouter.gas.test.ts.snap index 85326d7cd..b0e05cb22 100644 --- a/test/integration-tests/gas-tests/__snapshots__/UniversalVSSwapRouter.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/UniversalVSSwapRouter.gas.test.ts.snap @@ -7,32 +7,32 @@ Object { } `; -exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Max Approval Swap 1`] = `1129380`; +exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Max Approval Swap 1`] = `1136520`; -exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Sign Per Swap 1`] = `1164107`; +exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Sign Per Swap 1`] = `1171212`; exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps SwapRouter02 1`] = `1124979`; -exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Max Approval Swap 1`] = `3155203`; +exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Max Approval Swap 1`] = `3175891`; -exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Sign Per Swap 1`] = `3310169`; +exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Sign Per Swap 1`] = `3330699`; exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps SwapRouter02 1`] = `3195011`; -exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Max Approval Swap 1`] = `4199806`; +exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Max Approval Swap 1`] = `4226083`; -exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Sign Per Swap 1`] = `4405716`; +exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Sign Per Swap 1`] = `4431781`; exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions SwapRouter02 1`] = `4282374`; -exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Max Approval Swap 1`] = `519192`; +exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Max Approval Swap 1`] = `522191`; -exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Sign Per Swap 1`] = `519510`; +exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Sign Per Swap 1`] = `522509`; exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap SwapRouter02 1`] = `500008`; -exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Max Approval Swap 1`] = `304289`; +exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Max Approval Swap 1`] = `305396`; -exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Sign Per Swap 1`] = `304225`; +exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Sign Per Swap 1`] = `305332`; exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap SwapRouter02 1`] = `270033`; diff --git a/test/integration-tests/gas-tests/__snapshots__/V3ToV4Migration.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/V3ToV4Migration.gas.test.ts.snap index 62d1b5800..16405652c 100644 --- a/test/integration-tests/gas-tests/__snapshots__/V3ToV4Migration.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/V3ToV4Migration.gas.test.ts.snap @@ -3,55 +3,55 @@ exports[`V3 to V4 Migration Gas Tests V3 Commands burn gas: erc721permit + decreaseLiquidity + collect + burn 1`] = ` Object { "calldataByteLength": 1092, - "gasUsed": 256923, + "gasUsed": 257003, } `; exports[`V3 to V4 Migration Gas Tests V3 Commands collect gas: erc721permit + decreaseLiquidity + collect 1`] = ` Object { "calldataByteLength": 964, - "gasUsed": 224582, + "gasUsed": 224616, } `; exports[`V3 to V4 Migration Gas Tests V3 Commands decrease liquidity gas: erc721permit + decreaseLiquidity 1`] = ` Object { "calldataByteLength": 740, - "gasUsed": 202673, + "gasUsed": 202641, } `; exports[`V3 to V4 Migration Gas Tests V3 Commands erc721permit gas: erc721permit 1`] = ` Object { "calldataByteLength": 484, - "gasUsed": 66342, + "gasUsed": 66244, } `; exports[`V3 to V4 Migration Gas Tests V4 Commands initialize pool gas: initialize a pool 1`] = ` Object { "calldataByteLength": 452, - "gasUsed": 59262, + "gasUsed": 59119, } `; exports[`V3 to V4 Migration Gas Tests V4 Commands mint gas: migrate and mint 1`] = ` Object { "calldataByteLength": 2500, - "gasUsed": 600514, + "gasUsed": 601021, } `; exports[`V3 to V4 Migration Gas Tests V4 Commands mint gas: migrate weth position into eth position with forwarding 1`] = ` Object { "calldataByteLength": 2788, - "gasUsed": 577038, + "gasUsed": 577516, } `; exports[`V3 to V4 Migration Gas Tests V4 Commands mint gas: mint 1`] = ` Object { "calldataByteLength": 1604, - "gasUsed": 446398, + "gasUsed": 446724, } `; diff --git a/test/integration-tests/shared/planner.ts b/test/integration-tests/shared/planner.ts index b2336d821..0c1bb64fc 100644 --- a/test/integration-tests/shared/planner.ts +++ b/test/integration-tests/shared/planner.ts @@ -22,7 +22,6 @@ export enum CommandType { UNWRAP_WETH = 0x0c, PERMIT2_TRANSFER_FROM_BATCH = 0x0d, BALANCE_CHECK_ERC20 = 0x0e, - UNWRAP_WETH_EXACT = 0x0f, V4_SWAP = 0x10, V3_POSITION_MANAGER_PERMIT = 0x11, @@ -71,7 +70,6 @@ const ABI_DEFINITION: { [key in CommandType]: string[] } = { // Token Actions and Checks [CommandType.WRAP_ETH]: ['address', 'uint256'], [CommandType.UNWRAP_WETH]: ['address', 'uint256'], - [CommandType.UNWRAP_WETH_EXACT]: ['address', 'uint256'], [CommandType.SWEEP]: ['address', 'address', 'uint256'], [CommandType.TRANSFER]: ['address', 'address', 'uint256'], [CommandType.PAY_PORTION]: ['address', 'address', 'uint256'], From 64027f3372aa235734207c4e05667204f6f927e1 Mon Sep 17 00:00:00 2001 From: diana Date: Fri, 18 Sep 2026 10:59:36 -0400 Subject: [PATCH 2/3] chore: record 2.1.2 deployment addresses (#516) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: record 2.1.2 deployment addresses Record the UniversalRouter 2.1.2 addresses deployed across all 24 chains. Every address was verified onchain before recording: 24,380-byte runtime, all ten constructor parameters matching the deploy script, immutables embedded in the deployed bytecode, and source verified (Etherscan V2 where available, Sourcify otherwise; Tempo on its own Sourcify instance). Four chains additionally record an UnsupportedProtocolV2_1_2 entry. Their deploy scripts left `unsupported` unset, so the script deployed a throwaway revert stub first, which consumed a nonce and shifted the router address. Recording it explains why those routers do not share an address with the rest of their nonce group. arc.json, megaeth.json and robinhood.json are new; these chains had never been recorded. Their pre-existing routers were read from the SDK and confirmed onchain before being included. Co-Authored-By: Claude Opus 5 (1M context) * fix(script): point base-sepolia and unichain-sepolia at live v4 deployments Both scripts referenced PoolManager and PositionManager addresses that have no recent onchain activity. A router deployed against them cannot execute any v4 command. Confirmed by log activity rather than by documentation, which has been unreliable here. On Base Sepolia the previous PoolManager (0xf7F5aB3D) has produced no logs in the last 50k blocks, while 0x05E73354 is actively emitting. The same holds on Unichain Sepolia for 0x9cB26A71 versus 0x00B036B5. Sepolia needed no change; it was corrected on main in 020e1b7. These addresses are what UniversalRouter 2.1.2 was actually deployed with on both chains, so main's scripts now describe what is live. Co-Authored-By: Claude Opus 5 (1M context) * chore(script): add arc, megaeth and robinhood params; fill in unsupported Two gaps surfaced while deploying 2.1.2. Ink, Unichain, Worldchain and Zora left `unsupported` unset, so each deploy created a fresh UnsupportedProtocol stub before the router. That consumed a nonce and shifted the router off the address its nonce group would otherwise share, and left an orphaned stub that nothing references. Filling in the stubs deployed during 2.1.2 stops the next deploy repeating it. Each address was confirmed onchain to be a 60-byte contract that reverts with UnsupportedProtocolError. Arc, MegaETH and Robinhood had no deploy parameters on main at all; they were added on release/2.1.x and never ported. Without them a deploy from main silently skips those chains. Ported with permissionsAdapterFactory added to match the RouterParameters shape on main. All 24 chains' parameters now match the constructor arguments of the routers actually deployed onchain. Co-Authored-By: Claude Opus 5 (1M context) * style: fix forge fmt violation in PermissionedV4 test Pre-existing on main and unrelated to this branch, but it fails `yarn lint:check` and so would show a red CI run on any PR. Co-Authored-By: Claude Opus 5 (1M context) * Revert "style: fix forge fmt violation in PermissionedV4 test" This reverts commit 8ec8ca859daf63cc15be5b864cc8b211f38e5da1. * chore: drop the mislabelled 2.1.1 entry for megaeth 0x47837eb80db5908eabba9105626d9b348bea7b02 was recorded as UniversalRouterV2_1_1, taken from universal-router-sdk's CHAIN_CONFIGS. It cannot be 2.1.1: it was deployed 2026-01-30, and tag 2.1.1 was not cut until 2026-05-22. It is on the 2.1 line — SPOKE_POOL() resolves, which the 2.0 router on that chain does not — but at 21,738 bytes it is well short of 2.1.1's 24,546, consistent with a build predating the per-hop slippage work. The 2.1.1 release notes warn about exactly this: "Do not identify existing 2.1.0 bytecode as 2.1.1." There is no 2.1.0 tag to bytecode-match against, so rather than invent a label the entry is dropped. UniversalRouterV2 (0x48fd0352...) is kept: verified on Etherscan as UniversalRouter, no SPOKE_POOL(), and 19,499 bytes matching the 2.0 routers on ink, tempo, unichain and zora. Co-Authored-By: Claude Opus 5 (1M context) --------- Co-authored-by: Claude Opus 5 (1M context) --- deploy-addresses/arbitrum.json | 3 ++- deploy-addresses/arc.json | 4 +++ deploy-addresses/avalanche.json | 3 ++- deploy-addresses/base-sepolia.json | 3 ++- deploy-addresses/base.json | 3 ++- deploy-addresses/blast.json | 3 ++- deploy-addresses/bsc.json | 3 ++- deploy-addresses/celo.json | 3 ++- deploy-addresses/ink.json | 4 ++- deploy-addresses/linea.json | 3 ++- deploy-addresses/mainnet.json | 3 ++- deploy-addresses/megaeth.json | 4 +++ deploy-addresses/monad.json | 3 ++- deploy-addresses/optimism.json | 3 ++- deploy-addresses/polygon.json | 3 ++- deploy-addresses/robinhood.json | 4 +++ deploy-addresses/sepolia.json | 3 ++- deploy-addresses/soneium.json | 3 ++- deploy-addresses/tempo.json | 3 ++- deploy-addresses/unichain-sepolia.json | 3 ++- deploy-addresses/unichain.json | 4 ++- deploy-addresses/worldchain.json | 4 ++- deploy-addresses/xlayer.json | 3 ++- deploy-addresses/zora.json | 4 ++- script/deployParameters/DeployArc.s.sol | 27 +++++++++++++++++++ .../deployParameters/DeployBaseSepolia.s.sol | 4 +-- script/deployParameters/DeployInk.s.sol | 2 ++ script/deployParameters/DeployMegaETH.s.sol | 26 ++++++++++++++++++ script/deployParameters/DeployRobinhood.s.sol | 25 +++++++++++++++++ script/deployParameters/DeployUnichain.s.sol | 2 ++ .../DeployUnichainSepolia.s.sol | 4 +-- .../deployParameters/DeployWorldchain.s.sol | 2 ++ script/deployParameters/DeployZora.s.sol | 2 ++ 33 files changed, 148 insertions(+), 25 deletions(-) create mode 100644 deploy-addresses/arc.json create mode 100644 deploy-addresses/megaeth.json create mode 100644 deploy-addresses/robinhood.json create mode 100644 script/deployParameters/DeployArc.s.sol create mode 100644 script/deployParameters/DeployMegaETH.s.sol create mode 100644 script/deployParameters/DeployRobinhood.s.sol diff --git a/deploy-addresses/arbitrum.json b/deploy-addresses/arbitrum.json index 173dff784..7aae2597e 100644 --- a/deploy-addresses/arbitrum.json +++ b/deploy-addresses/arbitrum.json @@ -4,5 +4,6 @@ "UniversalRouterV1_2_NoV2Support": "0xeC8B0F7Ffe3ae75d7FfAb09429e3675bb63503e4", "UniversalRouterV1_2_V2Support": "0x5E325eDA8064b456f4781070C0738d849c824258", "UniversalRouterV2": "0xa51afafe0263b40edaef0df8781ea9aa03e381a3", - "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b" + "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b", + "UniversalRouterV2_1_2": "0x2d01411773c8C24805306E89A41F7855C3c4Fe65" } diff --git a/deploy-addresses/arc.json b/deploy-addresses/arc.json new file mode 100644 index 000000000..7e2f1267f --- /dev/null +++ b/deploy-addresses/arc.json @@ -0,0 +1,4 @@ +{ + "UniversalRouterV2_1_1": "0x4fca4a51ab4f23a7447b3284fbd7d73289a89fb1", + "UniversalRouterV2_1_2": "0x8702463e73f74d0b6765aBceb314Ef07aCb92650" +} diff --git a/deploy-addresses/avalanche.json b/deploy-addresses/avalanche.json index a23c8a7ca..2c1d91dd5 100644 --- a/deploy-addresses/avalanche.json +++ b/deploy-addresses/avalanche.json @@ -2,5 +2,6 @@ "UniversalRouterV1_2_NoV2Support": "0x82635AF6146972cD6601161c4472ffe97237D292", "UnsupportedProtocol": "0x5302086A3a25d473aAbBd0356eFf8Dd811a4d89B", "UniversalRouterV1_2_V2Support": "0x4Dae2f939ACf50408e13d58534Ff8c2776d45265", - "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b" + "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b", + "UniversalRouterV2_1_2": "0x661E93cca42AfacB172121EF892830cA3b70F08d" } diff --git a/deploy-addresses/base-sepolia.json b/deploy-addresses/base-sepolia.json index c2ab59511..5b9d08286 100644 --- a/deploy-addresses/base-sepolia.json +++ b/deploy-addresses/base-sepolia.json @@ -1,5 +1,6 @@ { "UniversalRouterV2": "0x95273d871c8156636e114b63797d78D7E1720d81", "UnsupportedProtocol": "0x76870DEbef0BE25589A5CddCe9B1D99276C73B4e", - "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b" + "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b", + "UniversalRouterV2_1_2": "0x8702463e73f74d0b6765aBceb314Ef07aCb92650" } diff --git a/deploy-addresses/base.json b/deploy-addresses/base.json index c6aba156d..9325a62a2 100644 --- a/deploy-addresses/base.json +++ b/deploy-addresses/base.json @@ -3,5 +3,6 @@ "UnsupportedProtocol": "0x9E18Efb3BE848940b0C92D300504Fb08C287FE85", "UniversalRouterV1_2_V2Support": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", "UniversalRouterV2": "0x6ff5693b99212da76ad316178a184ab56d299b43", - "UniversalRouterV2_1_1": "0xFdf682F51FE81Aa4898F0AE2163d8A55c127fbC7" + "UniversalRouterV2_1_1": "0xFdf682F51FE81Aa4898F0AE2163d8A55c127fbC7", + "UniversalRouterV2_1_2": "0xd6145b2D3F379919E8CdEda7B97e37c4b2Ca9c40" } diff --git a/deploy-addresses/blast.json b/deploy-addresses/blast.json index 0a846e6db..4da0b4665 100644 --- a/deploy-addresses/blast.json +++ b/deploy-addresses/blast.json @@ -3,5 +3,6 @@ "UnsupportedProtocol": "0x5ab1B56FB16238dB874258FB7847EFe248eb8496", "UniversalRouterV1_2_V2Support": "0x643770E279d5D0733F21d6DC03A8efbABf3255B4", "UniversalRouterV2": "0xeabbcb3e8e415306207ef514f660a3f820025be3", - "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b" + "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b", + "UniversalRouterV2_1_2": "0x661E93cca42AfacB172121EF892830cA3b70F08d" } diff --git a/deploy-addresses/bsc.json b/deploy-addresses/bsc.json index 2bdde6718..2e0a35115 100644 --- a/deploy-addresses/bsc.json +++ b/deploy-addresses/bsc.json @@ -4,5 +4,6 @@ "UniversalRouterV1_2_NoV2Support": "0xeC8B0F7Ffe3ae75d7FfAb09429e3675bb63503e4", "UniversalRouterV1_2_V2Support": "0x4Dae2f939ACf50408e13d58534Ff8c2776d45265", "UniversalRouterV2": "0x1906c1d672b88cd1b9ac7593301ca990f94eae07", - "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b" + "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b", + "UniversalRouterV2_1_2": "0xDc264714F68d84CF29BC605589405E78bDBE7C9f" } diff --git a/deploy-addresses/celo.json b/deploy-addresses/celo.json index 8e4516f01..ff503c626 100644 --- a/deploy-addresses/celo.json +++ b/deploy-addresses/celo.json @@ -2,5 +2,6 @@ "UniversalRouterV1": "0xC73d61d192FB994157168Fb56730FdEc64C9Cb8F", "UnsupportedProtocol": "0x5Dc88340E1c5c6366864Ee415d6034cadd1A9897", "UniversalRouterV1_2_V2Support": "0x643770e279d5d0733f21d6dc03a8efbabf3255b4", - "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b" + "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b", + "UniversalRouterV2_1_2": "0xe2023F3FA515cF070e07fD9d51c1d236e07843f4" } diff --git a/deploy-addresses/ink.json b/deploy-addresses/ink.json index 3b5ec39cc..a581d5242 100644 --- a/deploy-addresses/ink.json +++ b/deploy-addresses/ink.json @@ -1,3 +1,5 @@ { - "UniversalRouterV2": "0x112908dac86e20e7241b0927479ea3bf935d1fa0" + "UniversalRouterV2": "0x112908dac86e20e7241b0927479ea3bf935d1fa0", + "UnsupportedProtocolV2_1_2": "0xD1b797D92d87B688193A2B976eFc8D577D204343", + "UniversalRouterV2_1_2": "0x661E93cca42AfacB172121EF892830cA3b70F08d" } diff --git a/deploy-addresses/linea.json b/deploy-addresses/linea.json index 4dcb969d4..512f5d761 100644 --- a/deploy-addresses/linea.json +++ b/deploy-addresses/linea.json @@ -1,3 +1,4 @@ { - "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b" + "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b", + "UniversalRouterV2_1_2": "0xDc264714F68d84CF29BC605589405E78bDBE7C9f" } diff --git a/deploy-addresses/mainnet.json b/deploy-addresses/mainnet.json index ee10edbf4..4d46b4b32 100644 --- a/deploy-addresses/mainnet.json +++ b/deploy-addresses/mainnet.json @@ -3,5 +3,6 @@ "UnsupportedProtocol": "0x76D631990d505E4e5b432EEDB852A60897824D68", "UniversalRouterV1_2_V2Support": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", "UniversalRouterV2": "0x66a9893cc07d91d95644aedd05d03f95e1dba8af", - "UniversalRouterV2_1_1": "0x4C82D1fBFe28C977cBB58D8C7FF8FCF9F70a2cCA" + "UniversalRouterV2_1_1": "0x4C82D1fBFe28C977cBB58D8C7FF8FCF9F70a2cCA", + "UniversalRouterV2_1_2": "0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85" } diff --git a/deploy-addresses/megaeth.json b/deploy-addresses/megaeth.json new file mode 100644 index 000000000..1c4234153 --- /dev/null +++ b/deploy-addresses/megaeth.json @@ -0,0 +1,4 @@ +{ + "UniversalRouterV2": "0x48fd03529d2a91be835f07f6b72f53b4aad6093d", + "UniversalRouterV2_1_2": "0xAedd1CF4C14e833140A61E9C0d2b73a64795c823" +} diff --git a/deploy-addresses/monad.json b/deploy-addresses/monad.json index 99e584d25..a8cad4d60 100644 --- a/deploy-addresses/monad.json +++ b/deploy-addresses/monad.json @@ -1,3 +1,4 @@ { - "UniversalRouterV2_1_1": "0xFdf682F51FE81Aa4898F0AE2163d8A55c127fbC7" + "UniversalRouterV2_1_1": "0xFdf682F51FE81Aa4898F0AE2163d8A55c127fbC7", + "UniversalRouterV2_1_2": "0xa6CE4F10d83dBdDAc17E68e1837ca9cE6a1b596e" } diff --git a/deploy-addresses/optimism.json b/deploy-addresses/optimism.json index caa2714fa..6a732bcb1 100644 --- a/deploy-addresses/optimism.json +++ b/deploy-addresses/optimism.json @@ -4,5 +4,6 @@ "UniversalRouterV1_2_NoV2Support": "0xeC8B0F7Ffe3ae75d7FfAb09429e3675bb63503e4", "UniversalRouterV1_2_V2Support": "0xCb1355ff08Ab38bBCE60111F1bb2B784bE25D7e8", "UniversalRouterV2": "0x851116d9223fabed8e56c0e6b8ad0c31d98b3507", - "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b" + "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b", + "UniversalRouterV2_1_2": "0xC09255D86DB563cBc11C2fCf4a0C512e160111B4" } diff --git a/deploy-addresses/polygon.json b/deploy-addresses/polygon.json index 17ca399bc..7d1b31fb0 100644 --- a/deploy-addresses/polygon.json +++ b/deploy-addresses/polygon.json @@ -4,5 +4,6 @@ "UniversalRouterV1_2_NoV2Support": "0x643770E279d5D0733F21d6DC03A8efbABf3255B4", "UniversalRouterV1_2_V2Support": "0xec7BE89e9d109e7e3Fec59c222CF297125FEFda2", "UniversalRouterV2": "0x1095692a6237d83c6a72f3f5efedb9a670c49223", - "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b" + "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b", + "UniversalRouterV2_1_2": "0xDc264714F68d84CF29BC605589405E78bDBE7C9f" } diff --git a/deploy-addresses/robinhood.json b/deploy-addresses/robinhood.json new file mode 100644 index 000000000..9cdc1b7d5 --- /dev/null +++ b/deploy-addresses/robinhood.json @@ -0,0 +1,4 @@ +{ + "UniversalRouterV2_1_1": "0x8876789976decbfcbbbe364623c63652db8c0904", + "UniversalRouterV2_1_2": "0x204FAca1764B154221e35c0d20aBb3c525710498" +} diff --git a/deploy-addresses/sepolia.json b/deploy-addresses/sepolia.json index c3319bbd2..07cf2f1d1 100644 --- a/deploy-addresses/sepolia.json +++ b/deploy-addresses/sepolia.json @@ -2,5 +2,6 @@ "UniversalRouterV1_2_V2Support": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", "UnsupportedProtocol": "0x5302086A3a25d473aAbBd0356eFf8Dd811a4d89B", "UniversalRouterV2": "0x3a9d48ab9751398bbfa63ad67599bb04e4bdf98b", - "UniversalRouterV2_1_1": "0x7DfD4F31be6814D2906BDE155c3e1B146EAc1468" + "UniversalRouterV2_1_1": "0x7DfD4F31be6814D2906BDE155c3e1B146EAc1468", + "UniversalRouterV2_1_2": "0x7E4f6c5e954Da5c61B3423D81E2277431Ac043f3" } diff --git a/deploy-addresses/soneium.json b/deploy-addresses/soneium.json index 28370bb9d..58ded8dad 100644 --- a/deploy-addresses/soneium.json +++ b/deploy-addresses/soneium.json @@ -1,5 +1,6 @@ { "UniversalRouterV2_NoV2V3Support": "0x4cded7edf52c8aa5259a54ec6a3ce7c6d2a455df", "UnsupportedProtocol": "0x8702463e73f74d0b6765aBceb314Ef07aCb92650", - "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b" + "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b", + "UniversalRouterV2_1_2": "0x661E93cca42AfacB172121EF892830cA3b70F08d" } diff --git a/deploy-addresses/tempo.json b/deploy-addresses/tempo.json index 99e584d25..a7c9d80b5 100644 --- a/deploy-addresses/tempo.json +++ b/deploy-addresses/tempo.json @@ -1,3 +1,4 @@ { - "UniversalRouterV2_1_1": "0xFdf682F51FE81Aa4898F0AE2163d8A55c127fbC7" + "UniversalRouterV2_1_1": "0xFdf682F51FE81Aa4898F0AE2163d8A55c127fbC7", + "UniversalRouterV2_1_2": "0x182a927119D56008d921126764bF884221b10f59" } diff --git a/deploy-addresses/unichain-sepolia.json b/deploy-addresses/unichain-sepolia.json index b3ad92912..fbc62ab66 100644 --- a/deploy-addresses/unichain-sepolia.json +++ b/deploy-addresses/unichain-sepolia.json @@ -1,4 +1,5 @@ { "UniversalRouterV2": "0x986dadb82491834f6d17bd3287eb84be0b4d4cc7", - "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b" + "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b", + "UniversalRouterV2_1_2": "0xDf38F24fE153761634Be942F9d859f3DBA857E95" } diff --git a/deploy-addresses/unichain.json b/deploy-addresses/unichain.json index 64b9097f0..b907f7ae5 100644 --- a/deploy-addresses/unichain.json +++ b/deploy-addresses/unichain.json @@ -1,4 +1,6 @@ { "UniversalRouterV2": "0xef740bf23acae26f6492b10de645d6b98dc8eaf3", - "UniversalRouterV2_1_1": "0xFdf682F51FE81Aa4898F0AE2163d8A55c127fbC7" + "UniversalRouterV2_1_1": "0xFdf682F51FE81Aa4898F0AE2163d8A55c127fbC7", + "UnsupportedProtocolV2_1_2": "0xe2023F3FA515cF070e07fD9d51c1d236e07843f4", + "UniversalRouterV2_1_2": "0xD1b797D92d87B688193A2B976eFc8D577D204343" } diff --git a/deploy-addresses/worldchain.json b/deploy-addresses/worldchain.json index 9c9084b24..451f8d628 100644 --- a/deploy-addresses/worldchain.json +++ b/deploy-addresses/worldchain.json @@ -2,5 +2,7 @@ "UniversalRouterV1_2_V2Support": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", "UnsupportedProtocol": "0x16D4F26C15f3658ec65B1126ff27DD3dF2a2996b", "UniversalRouterV2": "0x8ac7bee993bb44dab564ea4bc9ea67bf9eb5e743", - "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b" + "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b", + "UnsupportedProtocolV2_1_2": "0x661E93cca42AfacB172121EF892830cA3b70F08d", + "UniversalRouterV2_1_2": "0xF025e0Fe9E331A0eF05c2ad3C4E9C64b625cda6f" } diff --git a/deploy-addresses/xlayer.json b/deploy-addresses/xlayer.json index 4dcb969d4..f2727cd51 100644 --- a/deploy-addresses/xlayer.json +++ b/deploy-addresses/xlayer.json @@ -1,3 +1,4 @@ { - "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b" + "UniversalRouterV2_1_1": "0x8B844f885672f333Bc0042cB669255f93a4C1E6b", + "UniversalRouterV2_1_2": "0x1cd182C94fcF42277B80DBB9060F88024809E61E" } diff --git a/deploy-addresses/zora.json b/deploy-addresses/zora.json index 7a93e11a9..e7a2fe02c 100644 --- a/deploy-addresses/zora.json +++ b/deploy-addresses/zora.json @@ -1,4 +1,6 @@ { "UniversalRouterV2": "0x3315ef7ca28db74abadc6c44570efdf06b04b020", - "UniversalRouterV2_1_1": "0xFdf682F51FE81Aa4898F0AE2163d8A55c127fbC7" + "UniversalRouterV2_1_1": "0xFdf682F51FE81Aa4898F0AE2163d8A55c127fbC7", + "UnsupportedProtocolV2_1_2": "0x661E93cca42AfacB172121EF892830cA3b70F08d", + "UniversalRouterV2_1_2": "0xF025e0Fe9E331A0eF05c2ad3C4E9C64b625cda6f" } diff --git a/script/deployParameters/DeployArc.s.sol b/script/deployParameters/DeployArc.s.sol new file mode 100644 index 000000000..de054d90c --- /dev/null +++ b/script/deployParameters/DeployArc.s.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {DeployUniversalRouter} from '../DeployUniversalRouter.s.sol'; +import {RouterParameters} from 'contracts/types/RouterParameters.sol'; + +contract DeployArc is DeployUniversalRouter { + function setUp() public override { + params = RouterParameters({ + permit2: 0x000000000022D473030F116dDEE9F6B43aC78BA3, + // Arc has no canonical WETH + weth9: UNSUPPORTED_PROTOCOL, + v2Factory: 0x89e5DB8B5aA49aA85AC63f691524311AEB649eba, + v3Factory: 0xf0db7b58379503491d857dB50AC9ece64c653918, + pairInitCodeHash: 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f, + poolInitCodeHash: 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54, + v4PoolManager: 0x8366a39CC670B4001A1121B8F6A443A643e40951, + permissionsAdapterFactory: address(0), // ToDo: Add permissions adapter factory + v3NFTPositionManager: 0x39654A85A4C05127f5Fd6ED22CAeC077A0fB1377, + v4PositionManager: 0x6049c9a0e26405C0985f9E3685C87d0aE917f82B, + // no Across SpokePool on Arc + spokePool: UNSUPPORTED_PROTOCOL + }); + + unsupported = 0x8bcEaA40B9AcdfAedF85AdF4FF01F5Ad6517937f; + } +} diff --git a/script/deployParameters/DeployBaseSepolia.s.sol b/script/deployParameters/DeployBaseSepolia.s.sol index 9cf07f440..84c080522 100644 --- a/script/deployParameters/DeployBaseSepolia.s.sol +++ b/script/deployParameters/DeployBaseSepolia.s.sol @@ -13,10 +13,10 @@ contract DeployBaseSepolia is DeployUniversalRouter { v3Factory: 0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24, pairInitCodeHash: 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f, poolInitCodeHash: 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54, - v4PoolManager: 0xf7F5aB3DcA35e17dE187b459159BC643853B3c67, + v4PoolManager: 0x05E73354cFDd6745C338b50BcFDfA3Aa6fA03408, permissionsAdapterFactory: address(0), // ToDo: Add permissions adapter factory v3NFTPositionManager: 0x27F971cb582BF9E50F397e4d29a5C7A34f11faA2, - v4PositionManager: 0x0B32f74f8365d535783949E014B7754047B64e31, + v4PositionManager: 0x4B2C77d209D3405F41a037Ec6c77F7F5b8e2ca80, spokePool: 0x82B564983aE7274c86695917BBf8C99ECb6F0F8F }); diff --git a/script/deployParameters/DeployInk.s.sol b/script/deployParameters/DeployInk.s.sol index 63c71466c..e54f5861a 100644 --- a/script/deployParameters/DeployInk.s.sol +++ b/script/deployParameters/DeployInk.s.sol @@ -19,5 +19,7 @@ contract DeployInk is DeployUniversalRouter { v4PositionManager: 0x1b35d13a2E2528f192637F14B05f0Dc0e7dEB566, spokePool: 0xeF684C38F94F48775959ECf2012D7E864ffb9dd4 }); + + unsupported = 0xD1b797D92d87B688193A2B976eFc8D577D204343; } } diff --git a/script/deployParameters/DeployMegaETH.s.sol b/script/deployParameters/DeployMegaETH.s.sol new file mode 100644 index 000000000..bc912daaa --- /dev/null +++ b/script/deployParameters/DeployMegaETH.s.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {DeployUniversalRouter} from '../DeployUniversalRouter.s.sol'; +import {RouterParameters} from 'contracts/types/RouterParameters.sol'; + +contract DeployMegaETH is DeployUniversalRouter { + function setUp() public override { + params = RouterParameters({ + permit2: 0x000000000022D473030F116dDEE9F6B43aC78BA3, + weth9: 0x4200000000000000000000000000000000000006, + v2Factory: 0xbf56488c857A881ae7e3BED27Cf99c10A7Ab7e50, + v3Factory: 0x3a5F0CD7d62452b7f899B2A5758BFa57be0dE478, + pairInitCodeHash: 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f, + poolInitCodeHash: 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54, + v4PoolManager: 0xaCB7e78fa05D562e0A5D3089ec896D57D057d38E, + permissionsAdapterFactory: address(0), // ToDo: Add permissions adapter factory + v3NFTPositionManager: 0xCDc86e98184e96436F733a8Bf31BD4F0214E6D7d, + v4PositionManager: 0x9AE0921E981AAa7308f176F8d4F9129b9247C89D, + // no Across SpokePool on MegaETH + spokePool: UNSUPPORTED_PROTOCOL + }); + + unsupported = 0xd6145b2D3F379919E8CdEda7B97e37c4b2Ca9c40; + } +} diff --git a/script/deployParameters/DeployRobinhood.s.sol b/script/deployParameters/DeployRobinhood.s.sol new file mode 100644 index 000000000..16a70196b --- /dev/null +++ b/script/deployParameters/DeployRobinhood.s.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {DeployUniversalRouter} from '../DeployUniversalRouter.s.sol'; +import {RouterParameters} from 'contracts/types/RouterParameters.sol'; + +contract DeployRobinhood is DeployUniversalRouter { + function setUp() public override { + params = RouterParameters({ + permit2: 0x000000000022D473030F116dDEE9F6B43aC78BA3, + weth9: 0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73, + v2Factory: 0x8bcEaA40B9AcdfAedF85AdF4FF01F5Ad6517937f, + v3Factory: 0x1f7d7550B1b028f7571E69A784071F0205FD2EfA, + pairInitCodeHash: 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f, + poolInitCodeHash: 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54, + v4PoolManager: 0x8366a39CC670B4001A1121B8F6A443A643e40951, + permissionsAdapterFactory: address(0), // ToDo: Add permissions adapter factory + v3NFTPositionManager: 0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3, + v4PositionManager: 0x58daec3116aae6D93017bAAea7749052E8a04fA7, + spokePool: 0xD29C85F15DF544bA632C9E25829fd29d767d7978 + }); + + unsupported = 0x7332D11BD10d18A04B119Cd4671a96f3148002c4; + } +} diff --git a/script/deployParameters/DeployUnichain.s.sol b/script/deployParameters/DeployUnichain.s.sol index 6b6573d9a..e59d59f51 100644 --- a/script/deployParameters/DeployUnichain.s.sol +++ b/script/deployParameters/DeployUnichain.s.sol @@ -19,5 +19,7 @@ contract DeployUnichain is DeployUniversalRouter { v4PositionManager: 0x4529A01c7A0410167c5740C487A8DE60232617bf, spokePool: 0x09aea4b2242abC8bb4BB78D537A67a245A7bEC64 }); + + unsupported = 0xe2023F3FA515cF070e07fD9d51c1d236e07843f4; } } diff --git a/script/deployParameters/DeployUnichainSepolia.s.sol b/script/deployParameters/DeployUnichainSepolia.s.sol index 62c34d1e0..5afbe2335 100644 --- a/script/deployParameters/DeployUnichainSepolia.s.sol +++ b/script/deployParameters/DeployUnichainSepolia.s.sol @@ -13,10 +13,10 @@ contract DeployUnichainSepolia is DeployUniversalRouter { v3Factory: 0x1F98431c8aD98523631AE4a59f267346ea31F984, pairInitCodeHash: 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f, poolInitCodeHash: 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54, - v4PoolManager: 0x9cB26A7183B2F4515945Dc52CB4195B0d2D06C95, + v4PoolManager: 0x00B036B58a818B1BC34d502D3fE730Db729e62AC, permissionsAdapterFactory: address(0), // ToDo: Add permissions adapter factory v3NFTPositionManager: 0xB7F724d6dDDFd008eFf5cc2834edDE5F9eF0d075, - v4PositionManager: 0x12A98709BB5D0641D61458f85dcAFbE17AC2d05c, + v4PositionManager: 0xf969Aee60879C54bAAed9F3eD26147Db216Fd664, spokePool: 0x6999526e507Cc3b03b180BbE05E1Ff938259A874 }); diff --git a/script/deployParameters/DeployWorldchain.s.sol b/script/deployParameters/DeployWorldchain.s.sol index ffcccc948..25e99446d 100644 --- a/script/deployParameters/DeployWorldchain.s.sol +++ b/script/deployParameters/DeployWorldchain.s.sol @@ -19,5 +19,7 @@ contract DeployWorldchain is DeployUniversalRouter { v4PositionManager: 0xC585E0f504613b5fBf874F21Af14c65260fB41fA, spokePool: 0x09aea4b2242abC8bb4BB78D537A67a245A7bEC64 }); + + unsupported = 0x661E93cca42AfacB172121EF892830cA3b70F08d; } } diff --git a/script/deployParameters/DeployZora.s.sol b/script/deployParameters/DeployZora.s.sol index 4399a0a91..5189b4140 100644 --- a/script/deployParameters/DeployZora.s.sol +++ b/script/deployParameters/DeployZora.s.sol @@ -19,5 +19,7 @@ contract DeployZora is DeployUniversalRouter { v4PositionManager: 0xf66C7b99e2040f0D9b326B3b7c152E9663543D63, spokePool: 0x13fDac9F9b4777705db45291bbFF3c972c6d1d97 }); + + unsupported = 0x661E93cca42AfacB172121EF892830cA3b70F08d; } } From b67f125a772e674376918bab91c0877b96db208c Mon Sep 17 00:00:00 2001 From: David <274080779+david-uniswap@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:57:30 -0700 Subject: [PATCH 3/3] feat(script): add HyperEVM (999) deploy parameters (#517) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Used for the 2.2.0 deployment at 0x9aFe3C497e19501DB228F28CdBdD29bC98F65DBa on 2026-09-18 (tx 0xd9eda6c912e49c0ce7e15bfd7a6d5dc91de796cc08231aec80df191341244f34), built at tag 2.2.0 (64027f3). Across SpokePool and PermissionsAdapterFactory wired; reuses the UnsupportedProtocol at 0xEEE3…4BcF. Co-authored-by: Claude Fable 5.1 --- script/deployParameters/DeployHyperEVM.s.sol | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 script/deployParameters/DeployHyperEVM.s.sol diff --git a/script/deployParameters/DeployHyperEVM.s.sol b/script/deployParameters/DeployHyperEVM.s.sol new file mode 100644 index 000000000..0783d6567 --- /dev/null +++ b/script/deployParameters/DeployHyperEVM.s.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {DeployUniversalRouter} from '../DeployUniversalRouter.s.sol'; +import {RouterParameters} from 'contracts/types/RouterParameters.sol'; + +/// @notice HyperEVM mainnet (chain id 999). Addresses from Uniswap/contracts deployments/json/999.json. +contract DeployHyperEVM is DeployUniversalRouter { + function setUp() public override { + params = RouterParameters({ + permit2: 0x000000000022D473030F116dDEE9F6B43aC78BA3, + // WHYPE, the canonical wrapped native on HyperEVM + weth9: 0x5555555555555555555555555555555555555555, + v2Factory: 0x89e5DB8B5aA49aA85AC63f691524311AEB649eba, + v3Factory: 0xf0db7b58379503491d857dB50AC9ece64c653918, + pairInitCodeHash: 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f, + poolInitCodeHash: 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54, + v4PoolManager: 0x12D4Fd9C5DeDd00ab8a0bCe2CF0167bbf94b6B1F, + permissionsAdapterFactory: 0x2a9049F60a1a42E82ba340eFe02b9Ba468Aff803, + v3NFTPositionManager: 0x39654A85A4C05127f5Fd6ED22CAeC077A0fB1377, + v4PositionManager: 0x0d7Ab5B3db668128Aff6F70C4eBC71D7d4DA9bf9, + // Across SpokePool on HyperEVM (chainId()=999, wrappedNativeToken()=WHYPE) + spokePool: 0x35E63eA3eb0fb7A3bc543C71FB66412e1F6B0E04 + }); + + // deployed alongside the 2.1.2 router on 2026-09-17 + unsupported = 0xEEE3Aa3c0d6D6f4E702748DeAcb42991A0094BcF; + } +}