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.
436 lines
15 KiB
436 lines
15 KiB
// 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
|
|
);
|
|
}
|
|
}
|
|
|