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.
537 lines
19 KiB
537 lines
19 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";
|
|
import {Router} from "../Router.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 Router, IInterchainAccountRouter {
|
|
// ============ Libraries ============
|
|
|
|
using TypeCasts for address;
|
|
using TypeCasts for bytes32;
|
|
|
|
// ============ Constants ============
|
|
|
|
uint32 internal immutable localDomain;
|
|
address internal implementation;
|
|
bytes32 internal bytecodeHash;
|
|
|
|
// ============ Public Storage ============
|
|
mapping(uint32 => bytes32) public isms;
|
|
|
|
// ============ Upgrade Gap ============
|
|
|
|
uint256[47] private __GAP;
|
|
|
|
// ============ Events ============
|
|
|
|
/**
|
|
* @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.
|
|
*/
|
|
constructor(uint32 _localDomain) {
|
|
localDomain = _localDomain;
|
|
}
|
|
|
|
// ============ 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");
|
|
|
|
implementation = address(new OwnableMulticall(address(this)));
|
|
// cannot be stored immutably because it is dynamically sized
|
|
bytes memory _bytecode = MinimalProxy.bytecode(implementation);
|
|
bytecodeHash = keccak256(_bytecode);
|
|
}
|
|
|
|
/**
|
|
* @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
|
|
) external onlyOwner {
|
|
_enrollRemoteRouterAndIsm(_destination, _router, _ism);
|
|
}
|
|
|
|
/**
|
|
* @notice Registers the address of remote InterchainAccountRouters
|
|
* and ISM contracts to use as defaults when making interchain calls
|
|
* @param _destinations The remote domains
|
|
* @param _routers The address of the remote InterchainAccountRouters
|
|
* @param _isms The address of the remote ISMs
|
|
*/
|
|
function enrollRemoteRouterAndIsms(
|
|
uint32[] calldata _destinations,
|
|
bytes32[] calldata _routers,
|
|
bytes32[] calldata _isms
|
|
) external onlyOwner {
|
|
require(
|
|
_destinations.length == _routers.length &&
|
|
_destinations.length == _isms.length,
|
|
"length mismatch"
|
|
);
|
|
for (uint256 i = 0; i < _destinations.length; i++) {
|
|
_enrollRemoteRouterAndIsm(_destinations[i], _routers[i], _isms[i]);
|
|
}
|
|
}
|
|
|
|
// ============ External Functions ============
|
|
/**
|
|
* @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 payable override 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);
|
|
}
|
|
|
|
// ============ Public Functions ============
|
|
|
|
/**
|
|
* @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);
|
|
}
|
|
|
|
/**
|
|
* @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);
|
|
}
|
|
|
|
// ============ Internal Functions ============
|
|
|
|
/**
|
|
* @dev Required for use of Router, compiler will not include this function in the bytecode
|
|
*/
|
|
function _handle(
|
|
uint32,
|
|
bytes32,
|
|
bytes calldata
|
|
) internal pure override {
|
|
assert(false);
|
|
}
|
|
|
|
/**
|
|
* @notice Overrides Router._enrollRemoteRouter to also enroll a default ISM
|
|
* @param _destination The remote domain
|
|
* @param _address The address of the remote InterchainAccountRouter
|
|
* @dev Sets the default ISM to the zero address
|
|
*/
|
|
function _enrollRemoteRouter(uint32 _destination, bytes32 _address)
|
|
internal
|
|
override
|
|
{
|
|
_enrollRemoteRouterAndIsm(_destination, _address, bytes32(0));
|
|
}
|
|
|
|
// ============ Private Functions ============
|
|
|
|
/**
|
|
* @notice Registers the address of a remote ISM contract to use as default
|
|
* @param _destination The remote domain
|
|
* @param _ism The address of the remote ISM
|
|
*/
|
|
function _enrollRemoteIsm(uint32 _destination, bytes32 _ism) private {
|
|
isms[_destination] = _ism;
|
|
emit RemoteIsmEnrolled(_destination, _ism);
|
|
}
|
|
|
|
/**
|
|
* @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(
|
|
routers(_destination) == bytes32(0) &&
|
|
isms[_destination] == bytes32(0),
|
|
"router and ISM defaults are immutable once set"
|
|
);
|
|
Router._enrollRemoteRouter(_destination, _router);
|
|
_enrollRemoteIsm(_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));
|
|
}
|
|
}
|
|
|