You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
541 lines
20 KiB
541 lines
20 KiB
// SPDX-License-Identifier: Apache-2.0
|
|
pragma solidity ^0.8.13;
|
|
|
|
// ============ Internal Imports ============
|
|
import {OwnableMulticall} from "../OwnableMulticall.sol";
|
|
import {HyperlaneConnectionClient} from "../HyperlaneConnectionClient.sol";
|
|
import {IRouter} from "../interfaces/IRouter.sol";
|
|
import {IInterchainAccountRouter} from "../interfaces/middleware/IInterchainAccountRouter.sol";
|
|
import {InterchainAccountMessage} from "../libs/middleware/InterchainAccountMessage.sol";
|
|
import {MinimalProxy} from "../libs/MinimalProxy.sol";
|
|
import {CallLib} from "../libs/Call.sol";
|
|
import {TypeCasts} from "../libs/TypeCasts.sol";
|
|
import {EnumerableMapExtended} from "../libs/EnumerableMapExtended.sol";
|
|
|
|
// ============ External Imports ============
|
|
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
|
|
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
|
|
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
|
|
|
/*
|
|
* @title A contract that allows accounts on chain A to call contracts via a
|
|
* proxy contract on chain B.
|
|
*/
|
|
contract InterchainAccountRouter is
|
|
HyperlaneConnectionClient,
|
|
IRouter,
|
|
IInterchainAccountRouter
|
|
{
|
|
// ============ Libraries ============
|
|
|
|
using TypeCasts for address;
|
|
using TypeCasts for bytes32;
|
|
|
|
// ============ Constants ============
|
|
|
|
uint32 internal immutable localDomain;
|
|
address internal immutable implementation;
|
|
bytes32 internal immutable bytecodeHash;
|
|
|
|
// ============ Private Storage ============
|
|
uint32[] private _domains;
|
|
|
|
// ============ Public Storage ============
|
|
mapping(uint32 => bytes32) public routers;
|
|
mapping(uint32 => bytes32) public isms;
|
|
|
|
// ============ Upgrade Gap ============
|
|
|
|
// gap for upgrade safety
|
|
uint256[47] private __GAP;
|
|
|
|
// ============ Events ============
|
|
|
|
/**
|
|
* @notice Emitted when a default router is set for a remote domain
|
|
* @param domain The remote domain
|
|
* @param router The address of the remote router
|
|
*/
|
|
event RemoteRouterEnrolled(uint32 indexed domain, bytes32 router);
|
|
|
|
/**
|
|
* @notice Emitted when a default ISM is set for a remote domain
|
|
* @param domain The remote domain
|
|
* @param ism The address of the remote ISM
|
|
*/
|
|
event RemoteIsmEnrolled(uint32 indexed domain, bytes32 ism);
|
|
|
|
/**
|
|
* @notice Emitted when an interchain call is dispatched to a remote domain
|
|
* @param destination The destination domain on which to make the call
|
|
* @param owner The local owner of the remote ICA
|
|
* @param router The address of the remote router
|
|
* @param ism The address of the remote ISM
|
|
*/
|
|
event RemoteCallDispatched(
|
|
uint32 indexed destination,
|
|
address indexed owner,
|
|
bytes32 router,
|
|
bytes32 ism
|
|
);
|
|
|
|
/**
|
|
* @notice Emitted when an interchain account contract is deployed
|
|
* @param origin The domain of the chain where the message was sent from
|
|
* @param owner The address of the account that sent the message
|
|
* @param ism The address of the local ISM
|
|
* @param account The address of the proxy account that was created
|
|
*/
|
|
event InterchainAccountCreated(
|
|
uint32 indexed origin,
|
|
bytes32 indexed owner,
|
|
address ism,
|
|
address account
|
|
);
|
|
|
|
// ============ Constructor ============
|
|
|
|
/**
|
|
* @notice Constructor deploys a relay (OwnableMulticall.sol) contract that
|
|
* will be cloned for each interchain account.
|
|
* @param _localDomain The Hyperlane domain ID on which this contract is
|
|
* deployed.
|
|
* @param _proxy The address of a proxy contract that delegates calls to
|
|
* this contract. Used by OwnableMulticall for access control.
|
|
* @dev Set proxy to address(0) to use this contract without a proxy.
|
|
*/
|
|
constructor(uint32 _localDomain, address _proxy) {
|
|
localDomain = _localDomain;
|
|
// TODO: always proxy and remove this sentinel
|
|
if (_proxy == address(0)) {
|
|
_proxy = address(this);
|
|
}
|
|
implementation = address(new OwnableMulticall(_proxy));
|
|
// cannot be stored immutably because it is dynamically sized
|
|
bytes memory _bytecode = MinimalProxy.bytecode(implementation);
|
|
bytecodeHash = keccak256(_bytecode);
|
|
}
|
|
|
|
// ============ Initializers ============
|
|
|
|
/**
|
|
* @notice Initializes the contract with HyperlaneConnectionClient contracts
|
|
* @param _mailbox The address of the mailbox contract
|
|
* @param _interchainGasPaymaster Unused but required by HyperlaneConnectionClient
|
|
* @param _interchainSecurityModule The address of the local ISM contract
|
|
* @param _owner The address with owner privileges
|
|
*/
|
|
function initialize(
|
|
address _mailbox,
|
|
address _interchainGasPaymaster,
|
|
address _interchainSecurityModule,
|
|
address _owner
|
|
) external initializer {
|
|
__HyperlaneConnectionClient_initialize(
|
|
_mailbox,
|
|
_interchainGasPaymaster,
|
|
_interchainSecurityModule,
|
|
_owner
|
|
);
|
|
require(localDomain == mailbox.localDomain(), "domain mismatch");
|
|
}
|
|
|
|
// ============ External Functions ============
|
|
|
|
/**
|
|
* @notice Registers the address of many remote InterchainAccountRouter
|
|
* contracts to use as a default when making interchain calls
|
|
* @param _destinations The remote domains
|
|
* @param _routers The addresses of the remote InterchainAccountRouters
|
|
*/
|
|
function enrollRemoteRouters(
|
|
uint32[] calldata _destinations,
|
|
bytes32[] calldata _routers
|
|
) external onlyOwner {
|
|
require(_destinations.length == _routers.length, "!length");
|
|
for (uint256 i = 0; i < _destinations.length; i += 1) {
|
|
_enrollRemoteRouterAndIsm(
|
|
_destinations[i],
|
|
_routers[i],
|
|
bytes32(0)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice Dispatches a single remote call to be made by an owner's
|
|
* interchain account on the destination domain
|
|
* @dev Uses the default router and ISM addresses for the destination
|
|
* domain, reverting if none have been configured
|
|
* @param _destination The remote domain of the chain to make calls on
|
|
* @param _to The address of the contract to call
|
|
* @param _value The value to include in the call
|
|
* @param _data The calldata
|
|
* @return The Hyperlane message ID
|
|
*/
|
|
function callRemote(
|
|
uint32 _destination,
|
|
address _to,
|
|
uint256 _value,
|
|
bytes memory _data
|
|
) external returns (bytes32) {
|
|
bytes32 _router = routers[_destination];
|
|
bytes32 _ism = isms[_destination];
|
|
bytes memory _body = InterchainAccountMessage.encode(
|
|
msg.sender,
|
|
_ism,
|
|
_to,
|
|
_value,
|
|
_data
|
|
);
|
|
return _dispatchMessage(_destination, _router, _ism, _body);
|
|
}
|
|
|
|
/**
|
|
* @notice Dispatches a sequence of remote calls to be made by an owner's
|
|
* interchain account on the destination domain
|
|
* @dev Uses the default router and ISM addresses for the destination
|
|
* domain, reverting if none have been configured
|
|
* @dev Recommend using CallLib.build to format the interchain calls.
|
|
* @param _destination The remote domain of the chain to make calls on
|
|
* @param _calls The sequence of calls to make
|
|
* @return The Hyperlane message ID
|
|
*/
|
|
function callRemote(uint32 _destination, CallLib.Call[] calldata _calls)
|
|
external
|
|
returns (bytes32)
|
|
{
|
|
bytes32 _router = routers[_destination];
|
|
bytes32 _ism = isms[_destination];
|
|
return callRemoteWithOverrides(_destination, _router, _ism, _calls);
|
|
}
|
|
|
|
/**
|
|
* @notice Handles dispatched messages by relaying calls to the interchain account
|
|
* @param _origin The origin domain of the interchain account
|
|
* @param _sender The sender of the interchain message
|
|
* @param _message The InterchainAccountMessage containing the account
|
|
* owner, ISM, and sequence of calls to be relayed
|
|
* @dev Does not need to be onlyRemoteRouter, as this application is designed
|
|
* to receive messages from untrusted remote contracts.
|
|
*/
|
|
function handle(
|
|
uint32 _origin,
|
|
bytes32 _sender,
|
|
bytes calldata _message
|
|
) external onlyMailbox {
|
|
(
|
|
bytes32 _owner,
|
|
bytes32 _ism,
|
|
CallLib.Call[] memory _calls
|
|
) = InterchainAccountMessage.decode(_message);
|
|
|
|
OwnableMulticall _interchainAccount = getDeployedInterchainAccount(
|
|
_origin,
|
|
_owner,
|
|
_sender,
|
|
TypeCasts.bytes32ToAddress(_ism)
|
|
);
|
|
_interchainAccount.multicall(_calls);
|
|
}
|
|
|
|
/**
|
|
* @notice Returns the local address of an interchain account
|
|
* @dev This interchain account is not guaranteed to have been deployed
|
|
* @param _origin The remote origin domain of the interchain account
|
|
* @param _router The remote origin InterchainAccountRouter
|
|
* @param _owner The remote owner of the interchain account
|
|
* @param _ism The local address of the ISM
|
|
* @return The local address of the interchain account
|
|
*/
|
|
function getLocalInterchainAccount(
|
|
uint32 _origin,
|
|
address _owner,
|
|
address _router,
|
|
address _ism
|
|
) external view returns (OwnableMulticall) {
|
|
bytes32 _routerAsBytes32 = TypeCasts.addressToBytes32(_router);
|
|
bytes32 _ownerAsBytes32 = TypeCasts.addressToBytes32(_owner);
|
|
return
|
|
getLocalInterchainAccount(
|
|
_origin,
|
|
_ownerAsBytes32,
|
|
_routerAsBytes32,
|
|
_ism
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @notice Returns the remote address of a locally owned interchain account
|
|
* @dev This interchain account is not guaranteed to have been deployed
|
|
* @dev This function will only work if the destination domain is
|
|
* EVM compatible
|
|
* @param _destination The remote destination domain of the interchain account
|
|
* @param _owner The local owner of the interchain account
|
|
* @return The remote address of the interchain account
|
|
*/
|
|
function getRemoteInterchainAccount(uint32 _destination, address _owner)
|
|
external
|
|
view
|
|
returns (address)
|
|
{
|
|
address _router = TypeCasts.bytes32ToAddress(routers[_destination]);
|
|
address _ism = TypeCasts.bytes32ToAddress(isms[_destination]);
|
|
return getRemoteInterchainAccount(_owner, _router, _ism);
|
|
}
|
|
|
|
function domains() external view returns (uint32[] memory) {
|
|
return _domains;
|
|
}
|
|
|
|
// ============ Public Functions ============
|
|
|
|
/**
|
|
* @notice Registers the address of a remote InterchainAccountRouter
|
|
* contract to use as a default when making interchain calls
|
|
* @param _destination The remote domain
|
|
* @param _router The address of the remote InterchainAccountRouter
|
|
*/
|
|
function enrollRemoteRouter(uint32 _destination, bytes32 _router)
|
|
public
|
|
onlyOwner
|
|
{
|
|
_enrollRemoteRouterAndIsm(_destination, _router, bytes32(0));
|
|
}
|
|
|
|
/**
|
|
* @notice Registers the address of remote InterchainAccountRouter
|
|
* and ISM contracts to use as a default when making interchain calls
|
|
* @param _destination The remote domain
|
|
* @param _router The address of the remote InterchainAccountRouter
|
|
* @param _ism The address of the remote ISM
|
|
*/
|
|
function enrollRemoteRouterAndIsm(
|
|
uint32 _destination,
|
|
bytes32 _router,
|
|
bytes32 _ism
|
|
) public onlyOwner {
|
|
_enrollRemoteRouterAndIsm(_destination, _router, _ism);
|
|
}
|
|
|
|
/**
|
|
* @notice Dispatches a sequence of remote calls to be made by an owner's
|
|
* interchain account on the destination domain
|
|
* @dev Recommend using CallLib.build to format the interchain calls
|
|
* @param _destination The remote domain of the chain to make calls on
|
|
* @param _router The remote router address
|
|
* @param _ism The remote ISM address
|
|
* @param _calls The sequence of calls to make
|
|
* @return The Hyperlane message ID
|
|
*/
|
|
function callRemoteWithOverrides(
|
|
uint32 _destination,
|
|
bytes32 _router,
|
|
bytes32 _ism,
|
|
CallLib.Call[] calldata _calls
|
|
) public returns (bytes32) {
|
|
bytes memory _body = InterchainAccountMessage.encode(
|
|
msg.sender,
|
|
_ism,
|
|
_calls
|
|
);
|
|
return _dispatchMessage(_destination, _router, _ism, _body);
|
|
}
|
|
|
|
/**
|
|
* @notice Returns and deploys (if not already) an interchain account
|
|
* @param _origin The remote origin domain of the interchain account
|
|
* @param _owner The remote owner of the interchain account
|
|
* @param _router The remote origin InterchainAccountRouter
|
|
* @param _ism The local address of the ISM
|
|
* @return The address of the interchain account
|
|
*/
|
|
function getDeployedInterchainAccount(
|
|
uint32 _origin,
|
|
address _owner,
|
|
address _router,
|
|
address _ism
|
|
) public returns (OwnableMulticall) {
|
|
return
|
|
getDeployedInterchainAccount(
|
|
_origin,
|
|
TypeCasts.addressToBytes32(_owner),
|
|
TypeCasts.addressToBytes32(_router),
|
|
_ism
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @notice Returns and deploys (if not already) an interchain account
|
|
* @param _origin The remote origin domain of the interchain account
|
|
* @param _owner The remote owner of the interchain account
|
|
* @param _router The remote origin InterchainAccountRouter
|
|
* @param _ism The local address of the ISM
|
|
* @return The address of the interchain account
|
|
*/
|
|
function getDeployedInterchainAccount(
|
|
uint32 _origin,
|
|
bytes32 _owner,
|
|
bytes32 _router,
|
|
address _ism
|
|
) public returns (OwnableMulticall) {
|
|
bytes32 _salt = _getSalt(
|
|
_origin,
|
|
_owner,
|
|
_router,
|
|
TypeCasts.addressToBytes32(_ism)
|
|
);
|
|
address payable _account = _getLocalInterchainAccount(_salt);
|
|
if (!Address.isContract(_account)) {
|
|
bytes memory _bytecode = MinimalProxy.bytecode(implementation);
|
|
_account = payable(Create2.deploy(0, _salt, _bytecode));
|
|
emit InterchainAccountCreated(_origin, _owner, _ism, _account);
|
|
}
|
|
return OwnableMulticall(_account);
|
|
}
|
|
|
|
/**
|
|
* @notice Returns the local address of a remotely owned interchain account
|
|
* @dev This interchain account is not guaranteed to have been deployed
|
|
* @param _origin The remote origin domain of the interchain account
|
|
* @param _owner The remote owner of the interchain account
|
|
* @param _router The remote InterchainAccountRouter
|
|
* @param _ism The local address of the ISM
|
|
* @return The local address of the interchain account
|
|
*/
|
|
function getLocalInterchainAccount(
|
|
uint32 _origin,
|
|
bytes32 _owner,
|
|
bytes32 _router,
|
|
address _ism
|
|
) public view returns (OwnableMulticall) {
|
|
return
|
|
OwnableMulticall(
|
|
_getLocalInterchainAccount(
|
|
_getSalt(
|
|
_origin,
|
|
_owner,
|
|
_router,
|
|
TypeCasts.addressToBytes32(_ism)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @notice Returns the remote address of a locally owned interchain account
|
|
* @dev This interchain account is not guaranteed to have been deployed
|
|
* @dev This function will only work if the destination domain is
|
|
* EVM compatible
|
|
* @param _owner The local owner of the interchain account
|
|
* @param _router The remote InterchainAccountRouter
|
|
* @param _ism The remote address of the ISM
|
|
* @return The remote address of the interchain account
|
|
*/
|
|
function getRemoteInterchainAccount(
|
|
address _owner,
|
|
address _router,
|
|
address _ism
|
|
) public view returns (address) {
|
|
require(_router != address(0), "no router specified for destination");
|
|
// Derives the address of the first contract deployed by _router using
|
|
// the CREATE opcode.
|
|
address _implementation = address(
|
|
uint160(
|
|
uint256(
|
|
keccak256(
|
|
abi.encodePacked(
|
|
bytes1(0xd6),
|
|
bytes1(0x94),
|
|
_router,
|
|
bytes1(0x01)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
);
|
|
bytes memory _proxyBytecode = MinimalProxy.bytecode(_implementation);
|
|
bytes32 _bytecodeHash = keccak256(_proxyBytecode);
|
|
bytes32 _salt = _getSalt(
|
|
localDomain,
|
|
TypeCasts.addressToBytes32(_owner),
|
|
TypeCasts.addressToBytes32(address(this)),
|
|
TypeCasts.addressToBytes32(_ism)
|
|
);
|
|
return Create2.computeAddress(_salt, _bytecodeHash, _router);
|
|
}
|
|
|
|
// ============ Private Functions ============
|
|
|
|
/**
|
|
* @notice Registers the address of remote InterchainAccountRouter
|
|
* and ISM contracts to use as a default when making interchain calls
|
|
* @param _destination The remote domain
|
|
* @param _router The address of the remote InterchainAccountRouter
|
|
* @param _ism The address of the remote ISM
|
|
*/
|
|
function _enrollRemoteRouterAndIsm(
|
|
uint32 _destination,
|
|
bytes32 _router,
|
|
bytes32 _ism
|
|
) private {
|
|
require(_router != bytes32(0), "invalid router address");
|
|
require(
|
|
routers[_destination] == bytes32(0),
|
|
"router and ISM defaults are immutable once set"
|
|
);
|
|
_domains.push(_destination);
|
|
routers[_destination] = _router;
|
|
isms[_destination] = _ism;
|
|
emit RemoteRouterEnrolled(_destination, _router);
|
|
emit RemoteIsmEnrolled(_destination, _ism);
|
|
}
|
|
|
|
/**
|
|
* @notice Dispatches an InterchainAccountMessage to the remote router
|
|
* @param _destination The remote domain
|
|
* @param _router The address of the remote InterchainAccountRouter
|
|
* @param _ism The address of the remote ISM
|
|
* @param _body The InterchainAccountMessage body
|
|
*/
|
|
function _dispatchMessage(
|
|
uint32 _destination,
|
|
bytes32 _router,
|
|
bytes32 _ism,
|
|
bytes memory _body
|
|
) private returns (bytes32) {
|
|
require(_router != bytes32(0), "no router specified for destination");
|
|
emit RemoteCallDispatched(_destination, msg.sender, _router, _ism);
|
|
return mailbox.dispatch(_destination, _router, _body);
|
|
}
|
|
|
|
/**
|
|
* @notice Returns the salt used to deploy an interchain account
|
|
* @param _origin The remote origin domain of the interchain account
|
|
* @param _owner The remote owner of the interchain account
|
|
* @param _router The remote origin InterchainAccountRouter
|
|
* @param _ism The local address of the ISM
|
|
* @return The CREATE2 salt used for deploying the interchain account
|
|
*/
|
|
function _getSalt(
|
|
uint32 _origin,
|
|
bytes32 _owner,
|
|
bytes32 _router,
|
|
bytes32 _ism
|
|
) private pure returns (bytes32) {
|
|
return keccak256(abi.encodePacked(_origin, _owner, _router, _ism));
|
|
}
|
|
|
|
/**
|
|
* @notice Returns the address of the interchain account on the local chain
|
|
* @param _salt The CREATE2 salt used for deploying the interchain account
|
|
* @return The address of the interchain account
|
|
*/
|
|
function _getLocalInterchainAccount(bytes32 _salt)
|
|
private
|
|
view
|
|
returns (address payable)
|
|
{
|
|
return payable(Create2.computeAddress(_salt, bytecodeHash));
|
|
}
|
|
}
|
|
|