// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity >=0.8.0; // ============ Internal Imports ============ import {Versioned} from "./upgrade/Versioned.sol"; import {Indexed} from "./libs/Indexed.sol"; import {Message} from "./libs/Message.sol"; import {TypeCasts} from "./libs/TypeCasts.sol"; import {IInterchainSecurityModule, ISpecifiesInterchainSecurityModule} from "./interfaces/IInterchainSecurityModule.sol"; import {IPostDispatchHook} from "./interfaces/hooks/IPostDispatchHook.sol"; import {IMessageRecipient} from "./interfaces/IMessageRecipient.sol"; import {IMailbox} from "./interfaces/IMailbox.sol"; // ============ External Imports ============ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; contract Mailbox is IMailbox, Indexed, Versioned, OwnableUpgradeable { // ============ Libraries ============ using Message for bytes; using TypeCasts for bytes32; using TypeCasts for address; // ============ Constants ============ // Domain of chain on which the contract is deployed uint32 public immutable localDomain; // ============ Public Storage ============ // A monotonically increasing nonce for outbound unique message IDs. uint32 public nonce; // The latest dispatched message ID used for auth in post-dispatch hooks. bytes32 public latestDispatchedId; // The default ISM, used if the recipient fails to specify one. IInterchainSecurityModule public defaultIsm; // The default post dispatch hook, used for post processing of opting-in dispatches. IPostDispatchHook public defaultHook; // The required post dispatch hook, used for post processing of ALL dispatches. IPostDispatchHook public requiredHook; // Mapping of message ID to delivery context that processed the message. struct Delivery { address processor; uint48 blockNumber; } mapping(bytes32 => Delivery) internal deliveries; // ============ Events ============ /** * @notice Emitted when the default ISM is updated * @param module The new default ISM */ event DefaultIsmSet(address indexed module); /** * @notice Emitted when the default hook is updated * @param hook The new default hook */ event DefaultHookSet(address indexed hook); /** * @notice Emitted when the required hook is updated * @param hook The new required hook */ event RequiredHookSet(address indexed hook); // ============ Constructor ============ constructor(uint32 _localDomain) { localDomain = _localDomain; } // ============ Initializers ============ function initialize( address _owner, address _defaultIsm, address _defaultHook, address _requiredHook ) external initializer { __Ownable_init(); setDefaultIsm(_defaultIsm); setDefaultHook(_defaultHook); setRequiredHook(_requiredHook); transferOwnership(_owner); } // ============ External Functions ============ /** * @notice Dispatches a message to the destination domain & recipient * using the default hook and empty metadata. * @param _destinationDomain Domain of destination chain * @param _recipientAddress Address of recipient on destination chain as bytes32 * @param _messageBody Raw bytes content of message body * @return The message ID inserted into the Mailbox's merkle tree */ function dispatch( uint32 _destinationDomain, bytes32 _recipientAddress, bytes calldata _messageBody ) external payable override returns (bytes32) { return dispatch( _destinationDomain, _recipientAddress, _messageBody, _messageBody[0:0], defaultHook ); } /** * @notice Dispatches a message to the destination domain & recipient. * @param destinationDomain Domain of destination chain * @param recipientAddress Address of recipient on destination chain as bytes32 * @param messageBody Raw bytes content of message body * @param hookMetadata Metadata used by the post dispatch hook * @return The message ID inserted into the Mailbox's merkle tree */ function dispatch( uint32 destinationDomain, bytes32 recipientAddress, bytes calldata messageBody, bytes calldata hookMetadata ) external payable override returns (bytes32) { return dispatch( destinationDomain, recipientAddress, messageBody, hookMetadata, defaultHook ); } /** * @notice Computes quote for dipatching a message to the destination domain & recipient * using the default hook and empty metadata. * @param destinationDomain Domain of destination chain * @param recipientAddress Address of recipient on destination chain as bytes32 * @param messageBody Raw bytes content of message body * @return fee The payment required to dispatch the message */ function quoteDispatch( uint32 destinationDomain, bytes32 recipientAddress, bytes calldata messageBody ) external view returns (uint256 fee) { return quoteDispatch( destinationDomain, recipientAddress, messageBody, messageBody[0:0], defaultHook ); } /** * @notice Computes quote for dispatching a message to the destination domain & recipient. * @param destinationDomain Domain of destination chain * @param recipientAddress Address of recipient on destination chain as bytes32 * @param messageBody Raw bytes content of message body * @param defaultHookMetadata Metadata used by the default post dispatch hook * @return fee The payment required to dispatch the message */ function quoteDispatch( uint32 destinationDomain, bytes32 recipientAddress, bytes calldata messageBody, bytes calldata defaultHookMetadata ) external view returns (uint256 fee) { return quoteDispatch( destinationDomain, recipientAddress, messageBody, defaultHookMetadata, defaultHook ); } /** * @notice Attempts to deliver `_message` to its recipient. Verifies * `_message` via the recipient's ISM using the provided `_metadata`. * @param _metadata Metadata used by the ISM to verify `_message`. * @param _message Formatted Hyperlane message (refer to Message.sol). */ function process( bytes calldata _metadata, bytes calldata _message ) external payable override { /// CHECKS /// // Check that the message was intended for this mailbox. require(_message.version() == VERSION, "Mailbox: bad version"); require( _message.destination() == localDomain, "Mailbox: unexpected destination" ); // Check that the message hasn't already been delivered. bytes32 _id = _message.id(); require(delivered(_id) == false, "Mailbox: already delivered"); // Get the recipient's ISM. address recipient = _message.recipientAddress(); IInterchainSecurityModule ism = recipientIsm(recipient); /// EFFECTS /// deliveries[_id] = Delivery({ processor: msg.sender, blockNumber: uint48(block.number) }); emit Process(_message.origin(), _message.sender(), recipient); emit ProcessId(_id); /// INTERACTIONS /// // Verify the message via the interchain security module. require( ism.verify(_metadata, _message), "Mailbox: ISM verification failed" ); // Deliver the message to the recipient. IMessageRecipient(recipient).handle{value: msg.value}( _message.origin(), _message.sender(), _message.body() ); } /** * @notice Returns the account that processed the message. * @param _id The message ID to check. * @return The account that processed the message. */ function processor(bytes32 _id) external view returns (address) { return deliveries[_id].processor; } /** * @notice Returns the account that processed the message. * @param _id The message ID to check. * @return The number of the block that the message was processed at. */ function processedAt(bytes32 _id) external view returns (uint48) { return deliveries[_id].blockNumber; } // ============ Public Functions ============ /** * @notice Dispatches a message to the destination domain & recipient. * @param destinationDomain Domain of destination chain * @param recipientAddress Address of recipient on destination chain as bytes32 * @param messageBody Raw bytes content of message body * @param metadata Metadata used by the post dispatch hook * @param hook Custom hook to use instead of the default * @return The message ID inserted into the Mailbox's merkle tree */ function dispatch( uint32 destinationDomain, bytes32 recipientAddress, bytes calldata messageBody, bytes calldata metadata, IPostDispatchHook hook ) public payable virtual returns (bytes32) { if (address(hook) == address(0)) { hook = defaultHook; } /// CHECKS /// // Format the message into packed bytes. bytes memory message = _buildMessage( destinationDomain, recipientAddress, messageBody ); bytes32 id = message.id(); /// EFFECTS /// latestDispatchedId = id; nonce += 1; emit Dispatch(msg.sender, destinationDomain, recipientAddress, message); emit DispatchId(id); /// INTERACTIONS /// uint256 requiredValue = requiredHook.quoteDispatch(metadata, message); // if underpaying, defer to required hook's reverting behavior if (msg.value < requiredValue) { requiredValue = msg.value; } requiredHook.postDispatch{value: requiredValue}(metadata, message); hook.postDispatch{value: msg.value - requiredValue}(metadata, message); return id; } /** * @notice Computes quote for dispatching a message to the destination domain & recipient. * @param destinationDomain Domain of destination chain * @param recipientAddress Address of recipient on destination chain as bytes32 * @param messageBody Raw bytes content of message body * @param metadata Metadata used by the post dispatch hook * @param hook Custom hook to use instead of the default * @return fee The payment required to dispatch the message */ function quoteDispatch( uint32 destinationDomain, bytes32 recipientAddress, bytes calldata messageBody, bytes calldata metadata, IPostDispatchHook hook ) public view returns (uint256 fee) { if (address(hook) == address(0)) { hook = defaultHook; } bytes memory message = _buildMessage( destinationDomain, recipientAddress, messageBody ); return requiredHook.quoteDispatch(metadata, message) + hook.quoteDispatch(metadata, message); } /** * @notice Returns true if the message has been processed. * @param _id The message ID to check. * @return True if the message has been delivered. */ function delivered(bytes32 _id) public view override returns (bool) { return deliveries[_id].blockNumber > 0; } /** * @notice Sets the default ISM for the Mailbox. * @param _module The new default ISM. Must be a contract. */ function setDefaultIsm(address _module) public onlyOwner { require( Address.isContract(_module), "Mailbox: default ISM not contract" ); defaultIsm = IInterchainSecurityModule(_module); emit DefaultIsmSet(_module); } /** * @notice Sets the default post dispatch hook for the Mailbox. * @param _hook The new default post dispatch hook. Must be a contract. */ function setDefaultHook(address _hook) public onlyOwner { require( Address.isContract(_hook), "Mailbox: default hook not contract" ); defaultHook = IPostDispatchHook(_hook); emit DefaultHookSet(_hook); } /** * @notice Sets the required post dispatch hook for the Mailbox. * @param _hook The new default post dispatch hook. Must be a contract. */ function setRequiredHook(address _hook) public onlyOwner { require( Address.isContract(_hook), "Mailbox: required hook not contract" ); requiredHook = IPostDispatchHook(_hook); emit RequiredHookSet(_hook); } /** * @notice Returns the ISM to use for the recipient, defaulting to the * default ISM if none is specified. * @param _recipient The message recipient whose ISM should be returned. * @return The ISM to use for `_recipient`. */ function recipientIsm( address _recipient ) public view returns (IInterchainSecurityModule) { // use low-level staticcall in case of revert or empty return data (bool success, bytes memory returnData) = _recipient.staticcall( abi.encodeCall( ISpecifiesInterchainSecurityModule.interchainSecurityModule, () ) ); // check if call was successful and returned data if (success && returnData.length != 0) { // check if returnData is a valid address address ism = abi.decode(returnData, (address)); // check if the ISM is a contract if (ism != address(0)) { return IInterchainSecurityModule(ism); } } // Use the default if a valid one is not specified by the recipient. return defaultIsm; } // ============ Internal Functions ============ function _buildMessage( uint32 destinationDomain, bytes32 recipientAddress, bytes calldata messageBody ) internal view returns (bytes memory) { return Message.formatMessage( VERSION, nonce, localDomain, msg.sender.addressToBytes32(), destinationDomain, recipientAddress, messageBody ); } }