Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/forge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
# Must install node modules for forge to reference in remappings
- uses: useblacksmith/setup-node@v5
with:
node-version: 18.x
node-version: 22.x
registry-url: https://registry.npmjs.org

- name: Install Yarn
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint-gas.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Set up node
uses: useblacksmith/setup-node@v5
with:
node-version: 20
node-version: 22

- name: Install Yarn
run: npm install -g yarn
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:

- uses: useblacksmith/setup-node@v5
with:
node-version: 18.x
node-version: 22.x
registry-url: https://registry.npmjs.org

- name: Install Yarn
Expand Down
4 changes: 0 additions & 4 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/hyperlane-monorepo"]
path = lib/hyperlane-monorepo
url = https://github.com/hyperlane-xyz/hyperlane-monorepo
branch = audit-q2-2025-prerelease
[submodule "lib/uniswap-v4-periphery"]
path = lib/uniswap-v4-periphery
url = https://github.com/velodrome-finance/uniswap-v4-periphery
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v16
v22
58 changes: 52 additions & 6 deletions contracts/UniversalRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
pragma solidity ^0.8.24;

// Command implementations
import {Quote} from '@hyperlane/core/contracts/interfaces/ITokenBridge.sol';
import {IAllowanceTransfer} from 'permit2/src/interfaces/IAllowanceTransfer.sol';
import {QuotedCalls} from '@hyperlane/core/contracts/token/QuotedCalls.sol';
import {Dispatcher} from './base/Dispatcher.sol';
import {RouterDeployParameters} from './types/RouterDeployParameters.sol';
import {PaymentsImmutables, PaymentsParameters} from './modules/PaymentsImmutables.sol';
Expand All @@ -12,22 +15,28 @@ import {Locker} from './libraries/Locker.sol';
import {IUniversalRouter} from './interfaces/IUniversalRouter.sol';

contract UniversalRouter is IUniversalRouter, Dispatcher {
address internal immutable _quotedCallsModule;

constructor(RouterDeployParameters memory params)
RouterImmutables(
RouterParameters(
RouterImmutables(RouterParameters(
params.v2Factory,
params.v3Factory,
params.pairInitCodeHash,
params.poolInitCodeHash,
params.veloV2Factory,
params.veloCLFactory,
params.veloV2InitCodeHash,
params.veloCLInitCodeHash
)
)
params.veloCLInitCodeHash,
params.veloCLFactory2,
params.veloCLInitCodeHash2,
params.veloCLFactory3,
params.veloCLInitCodeHash3
))
V4SwapRouter(params.v4PoolManager)
PaymentsImmutables(PaymentsParameters(params.permit2, params.weth9))
{}
{
_quotedCallsModule = address(new QuotedCalls(IAllowanceTransfer(params.permit2)));
}

modifier checkDeadline(uint256 deadline) {
if (block.timestamp > deadline) revert TransactionDeadlinePassed();
Expand Down Expand Up @@ -71,7 +80,44 @@ contract UniversalRouter is IUniversalRouter, Dispatcher {
}
}

/// @inheritdoc IUniversalRouter
function quoteExecute(bytes calldata commands, bytes[] calldata inputs)
external
override
returns (Quote[][] memory results)
{
bool success;
bytes memory output;
address module = _quotedCallsModule;
bytes4 selector = QuotedCalls.quoteExecute.selector;
assembly ("memory-safe") {
let argsSize := sub(calldatasize(), 0x04)
let payloadSize := add(0x04, argsSize)
let payload := mload(0x40)
mstore(payload, payloadSize)
mstore(add(payload, 0x20), shl(224, selector))
calldatacopy(add(payload, 0x24), 0x04, argsSize)

success := delegatecall(gas(), module, add(payload, 0x20), payloadSize, 0, 0)

let returnSize := returndatasize()
output := mload(0x40)
mstore(output, returnSize)
returndatacopy(add(output, 0x20), 0, returnSize)
mstore(0x40, and(add(add(output, 0x20), add(returnSize, 0x1f)), not(0x1f)))
}
if (!success) assembly ("memory-safe") {
revert(add(output, 0x20), mload(output))
}
results = abi.decode(output, (Quote[][]));
}

function successRequired(bytes1 command) internal pure returns (bool) {
return command & Commands.FLAG_ALLOW_REVERT == 0;
}

/// @inheritdoc Dispatcher
function quotedCallsModule() internal view override returns (address) {
return _quotedCallsModule;
}
}
159 changes: 46 additions & 113 deletions contracts/base/Dispatcher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,36 @@ 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 {BaseActionsRouter} from '@uniswap/v4-periphery/src/base/BaseActionsRouter.sol';

import {IInterchainAccountRouter} from '../interfaces/external/IInterchainAccountRouter.sol';
import {V2SwapRouter} from '../modules/uniswap/v2/V2SwapRouter.sol';
import {V3SwapRouter} from '../modules/uniswap/v3/V3SwapRouter.sol';
import {V4SwapRouter} from '../modules/uniswap/v4/V4SwapRouter.sol';
import {BytesLib} from '../modules/uniswap/v3/BytesLib.sol';
import {Payments} from '../modules/Payments.sol';
import {BridgeRouter} from '../modules/bridge/BridgeRouter.sol';
import {Commands} from '../libraries/Commands.sol';
import {Constants} from '../libraries/Constants.sol';
import {Lock} from './Lock.sol';

/// @title Decodes and Executes Commands
/// @notice Called by the UniversalRouter contract to efficiently decode and execute a singular command
abstract contract Dispatcher is Payments, V2SwapRouter, V3SwapRouter, V4SwapRouter, BridgeRouter, Lock {
abstract contract Dispatcher is Payments, V2SwapRouter, V3SwapRouter, V4SwapRouter, Lock {
using BytesLib for bytes;
using CalldataDecoder for bytes;

error InvalidCommandType(uint256 commandType);
error BalanceTooLow();
error QuotedCallsNestedNotSupported();

event UniversalRouterSwap(address indexed sender, address indexed recipient);
event UniversalRouterBridge(
address indexed sender, address indexed recipient, address indexed token, uint256 amount, uint32 domain
);

event CrossChainSwap(
address indexed caller, address indexed localRouter, uint32 indexed destinationDomain, bytes32 commitment
);

/// @notice Executes encoded commands along with provided inputs.
/// @param commands A set of concatenated commands, each 1 byte in length
/// @param inputs An array of byte strings containing abi encoded inputs for each command
function execute(bytes calldata commands, bytes[] calldata inputs) external payable virtual;

function quotedCallsModule() internal view virtual returns (address);

/// @notice Public view function to be used instead of msg.sender, as the contract performs self-reentrancy and at
/// times msg.sender == address(this). Instead msgSender() returns the initiator of the lock
/// @dev overrides BaseActionsRouter.msgSender in V4Router
Expand Down Expand Up @@ -141,14 +136,15 @@ abstract contract Dispatcher is Payments, V2SwapRouter, V3SwapRouter, V4SwapRout
permitBatch := add(inputs.offset, calldataload(inputs.offset))
}
bytes calldata data = inputs.toBytes(1);
(success, output) = address(PERMIT2).call(
abi.encodeWithSignature(
'permit(address,((address,uint160,uint48,uint48)[],address,uint256),bytes)',
msgSender(),
permitBatch,
data
)
);
(success, output) = address(PERMIT2)
.call(
abi.encodeWithSignature(
'permit(address,((address,uint160,uint48,uint48)[],address,uint256),bytes)',
msgSender(),
permitBatch,
data
)
);
} else if (command == Commands.SWEEP) {
// equivalent: abi.decode(inputs, (address, address, uint256))
address token;
Expand Down Expand Up @@ -260,14 +256,15 @@ abstract contract Dispatcher is Payments, V2SwapRouter, V3SwapRouter, V4SwapRout
permitSingle := inputs.offset
}
bytes calldata data = inputs.toBytes(6); // PermitSingle takes first 6 slots (0..5)
(success, output) = address(PERMIT2).call(
abi.encodeWithSignature(
'permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)',
msgSender(),
permitSingle,
data
)
);
(success, output) = address(PERMIT2)
.call(
abi.encodeWithSignature(
'permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)',
msgSender(),
permitSingle,
data
)
);
} else if (command == Commands.WRAP_ETH) {
// equivalent: abi.decode(inputs, (address, uint256))
address recipient;
Expand Down Expand Up @@ -326,94 +323,11 @@ abstract contract Dispatcher is Payments, V2SwapRouter, V3SwapRouter, V4SwapRout
}
(success, output) =
address(poolManager).call(abi.encodeCall(IPoolManager.initialize, (poolKey, sqrtPriceX96)));
} else if (command == Commands.BRIDGE_TOKEN) {
// equivalent: abi.decode(inputs, (uint8, address, address, address, uint256, uint256, uint256, uint32, bool))
uint8 bridgeType;
address recipient;
address token;
address bridge;
uint256 amount;
uint256 msgFee;
uint256 maxTokenFee;
uint32 domain;
bool payerIsUser;
assembly {
bridgeType := calldataload(inputs.offset)
recipient := calldataload(add(inputs.offset, 0x20))
token := calldataload(add(inputs.offset, 0x40))
bridge := calldataload(add(inputs.offset, 0x60))
amount := calldataload(add(inputs.offset, 0x80))
msgFee := calldataload(add(inputs.offset, 0xA0))
maxTokenFee := calldataload(add(inputs.offset, 0xC0))
domain := calldataload(add(inputs.offset, 0xE0))
payerIsUser := calldataload(add(inputs.offset, 0x100))
}
address sender = msgSender();
address payer = payerIsUser ? sender : address(this);
recipient = recipient == ActionConstants.MSG_SENDER ? sender : recipient;
if (amount == ActionConstants.CONTRACT_BALANCE) amount = ERC20(token).balanceOf(address(this));
bridgeToken({
bridgeType: bridgeType,
sender: sender,
recipient: recipient,
token: token,
bridge: bridge,
amount: amount,
msgFee: msgFee,
maxTokenFee: maxTokenFee,
domain: domain,
payer: payer
});
emit UniversalRouterBridge({
sender: sender,
recipient: recipient,
token: token,
amount: amount,
domain: domain
});
} else if (command == Commands.EXECUTE_CROSS_CHAIN) {
// equivalent: abi.decode(inputs, (uint32, address, bytes32, bytes32, bytes32, uint256, address, uint256, address, bytes))
uint32 domain;
address icaRouter;
bytes32 remoteRouter;
bytes32 ism;
bytes32 commitment;
uint256 msgFee;
address token;
uint256 tokenFee;
address hook;
assembly {
domain := calldataload(inputs.offset)
icaRouter := calldataload(add(inputs.offset, 0x20))
remoteRouter := calldataload(add(inputs.offset, 0x40))
ism := calldataload(add(inputs.offset, 0x60))
commitment := calldataload(add(inputs.offset, 0x80))
msgFee := calldataload(add(inputs.offset, 0xA0))
token := calldataload(add(inputs.offset, 0xC0))
tokenFee := calldataload(add(inputs.offset, 0xE0))
hook := calldataload(add(inputs.offset, 0x100))
// 0x120 offset contains the hook metadata, decoded below
}
bytes calldata hookMetadata = inputs.toBytes(9);

if (token != address(0)) ERC20(token).approve(icaRouter, tokenFee);
IInterchainAccountRouter(icaRouter).callRemoteCommitReveal{value: msgFee}({
_destination: domain,
_router: remoteRouter,
_ism: ism,
_hookMetadata: hookMetadata,
_hook: IPostDispatchHook(hook),
_salt: TypeCasts.addressToBytes32(msgSender()),
_commitment: commitment
});
emit CrossChainSwap({
caller: msgSender(),
localRouter: icaRouter,
destinationDomain: domain,
commitment: commitment
});
} else if (command == Commands.QUOTED_CALLS) {
if (msg.sender == address(this)) revert QuotedCallsNestedNotSupported();
(success, output) = _delegateQuotedCalls(inputs);
} else {
// placeholder area for commands 0x14-0x20
// placeholder area for commands 0x15-0x20
revert InvalidCommandType(command);
}
}
Expand Down Expand Up @@ -441,4 +355,23 @@ abstract contract Dispatcher is Payments, V2SwapRouter, V3SwapRouter, V4SwapRout
return recipient;
}
}

function _delegateQuotedCalls(bytes calldata inputs) internal returns (bool success, bytes memory output) {
address module = quotedCallsModule();

assembly ("memory-safe") {
let payloadSize := inputs.length
let payload := mload(0x40)
mstore(payload, payloadSize)
calldatacopy(add(payload, 0x20), inputs.offset, inputs.length)

success := delegatecall(gas(), module, add(payload, 0x20), payloadSize, 0, 0)

let returnSize := returndatasize()
output := mload(0x40)
mstore(output, returnSize)
returndatacopy(add(output, 0x20), 0, returnSize)
mstore(0x40, and(add(add(output, 0x20), add(returnSize, 0x1f)), not(0x1f)))
}
}
}
12 changes: 12 additions & 0 deletions contracts/interfaces/IRouterImmutables.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,16 @@ interface IRouterImmutables {

/// @notice The Velodrome CLPool initcodehash
function VELODROME_CL_POOL_INIT_CODE_HASH() external returns (bytes32);

/// @notice The address of Velodrome CL PoolFactory 2, or address(0) if not available
function VELODROME_CL_FACTORY_2() external returns (address);

/// @notice The Velodrome CLPool 2 initcodehash
function VELODROME_CL_POOL_INIT_CODE_HASH_2() external returns (bytes32);

/// @notice The address of Velodrome CL PoolFactory 3, or address(0) if not available
function VELODROME_CL_FACTORY_3() external returns (address);

/// @notice The Velodrome CLPool 3 initcodehash
function VELODROME_CL_POOL_INIT_CODE_HASH_3() external returns (bytes32);
}
8 changes: 8 additions & 0 deletions contracts/interfaces/IUniversalRouter.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;

import {Quote} from '@hyperlane/core/contracts/interfaces/ITokenBridge.sol';

interface IUniversalRouter {
/// @notice Thrown when a required command has failed
error ExecutionFailed(uint256 commandIndex, bytes message);
Expand All @@ -22,4 +24,10 @@ interface IUniversalRouter {
/// @param inputs An array of byte strings containing abi encoded inputs for each command
/// @param deadline The deadline by which the transaction must be executed
function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) external payable;

/// @notice Quotes router commands without executing token movements.
/// @dev Equivalent to Hyperlane `QuotedCalls.quoteExecute`.
function quoteExecute(bytes calldata commands, bytes[] calldata inputs)
external
returns (Quote[][] memory results);
}
5 changes: 1 addition & 4 deletions contracts/interfaces/external/ICreateX.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,7 @@ interface ICreateX {
pure
returns (address computedAddress);

function computeCreate2Address(bytes32 salt, bytes32 initCodeHash)
external
view
returns (address computedAddress);
function computeCreate2Address(bytes32 salt, bytes32 initCodeHash) external view returns (address computedAddress);

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CREATE3 */
Expand Down
Loading