Hyperlane v2 to v3 Migration Guide
Now in v3, we’ve improved the Mailbox to unlock modularity in how you transport messages, while simplifying the interface.
Note: v3 is a breaking change to v2.
Mailbox Interface
A major version change of the Mailbox, v3, has arrived with these improvements:
- modularity of domain specific transport and security layers
- simplifying the Hyperlane API to a single call (and address)
- configurable dispatch time requirements such as fee payments
v3 unifies interchain gas payments, protocol fees, and any message dispatch time behavior into a streamlined interface.
interface IMailbox {
function quoteDispatch(
uint32 destination,
bytes32 recipient,
bytes body
) external returns (uint256 fee);
function dispatch(
uint32 destination,
bytes32 recipient,
bytes body
) external payable; // will revert if msg.value < quoted fee
}
This is a departure from the existing interchain gas payment and hook interfaces but should be almost entirely backwards compatible. Mirroring the interchain security modules pre-handle lifecycle hook, the architectural change is introducing post-dispatch lifecycle hooks which receive authenticated message content from the mailbox and perform some additional task, like paying interchain gas to the relayer or using a different transport layer.
The mailbox will have a required hook needed by the protocol and a default hook that can be overridden by the message sender, further modularizing the mailbox interface. We expect the required hook to include enforcing protocol fees. These hooks will be governable, enabling Mailbox operators to add message-specific capabilities as default or required behavior incrementally without mutating message content. This mirrors default ISM governance and post-dispatch hook behavior should reflect requirements of the message destination’s ISM, default or otherwise.
Post Dispatch Hook Interface
The post dispatch hook interface is designed for maximal generality and extensibility of implementation (quite similar to the ISM verification interface). In addition to the entire message content, metadata can be provided by the dispatcher for further expression of preferences. For hooks that charge for use, a quote interface is necessary to inform the message sender how much value must be passed to the mailbox dispatch call.
interface IPostDispatchHook {
function postDispatch(
bytes metadata,
bytes message
) external payable;
function quoteDispatch(
bytes metadata,
bytes message
) external returns (uint256 fee);
}
Usage Patterns
This new interface greatly simplifies the integration experience and prevents confusion for first time users.
Before
// lookup gas amount, igp, and hook for destination
uint256 gasAmount = ...;
IInterchainGasPaymaster igp = ...;
IMessageHook hook = ...;
uint256 quote = igp.quoteGasPayment(destination, gasAmount);
bytes32 messageId = mailbox.dispatch(destination, recipient, body);
igp.payForGas{value: quote}(messageId, destination, gasAmount, msg.sender);
hook.postDispatch(destination, messageId); // old interface that does not allow for behavior dynamic with message content
After
uint256 quote = mailbox.quoteDispatch(destination, recipient, body);
mailbox.dispatch{value: quote}(destination, recipient, body);
Dispatch may revert if payment is insufficient. If you are composing hooks together, overpayment may not be refunded.
mailbox.dispatch{value: msg.value}(destination, recipient, body);
If you want post-dispatch behavior on a non-default chain, such as using an OpStack bridge on messages outbound from ethereum, simply pass the corresponding hook.
// overrides default hook with opStackHook
mailbox.dispatch{value: msg.value}(destination, recipient, body, opStackHook);
Permissionless Interoperability Composition
Implementation Guide
We have implemented these changes for the EVM implementation of Hyperlane and are sending them off for audit, targeting mid-September for mainnet launch. The following summarizes the logical changes (in pseudocode) as a reference for other execution environments.
param?
indicates an optional parameter
optional ?? fallback
resolves to optional
if some value is specified, fallback
if it is not
Mailbox Implementation
The incremental merkle tree has been migrated out of the mailbox and replaced by nonce
and latestDispatchedId
storage variables. The nonce is necessary for maintaining message uniqueness. The latest dispatched message ID is useful for hooks which need to authenticate message content independent of the caller (msg.sender
) address.
contract Mailbox {
// monotonically increasing outbound message nonce
uint256 nonce;
// commits to latest dispatched message content
bytes32 latestDispatchedId;
// implements defaults that can be overridden (overhead IGP, etc)
IPostDispatchHook public defaultHook;
// enforces protocol fees or any future required hooks
IPostDispatchHook public requiredHook;
function dispatch(
uint32 destination,
bytes32 recipient,
bytes calldata body,
IPostDispatchHook customHook?, // optional
bytes calldata hookMetadata? // optional
) external payable returns (bytes32 messageId) {
...
// effects
nonce += 1;
bytes32 messageId = message.id();
latestDispatchedId = messageId;
emit Dispatch(message);
emit DispatchId(messageId);
// interactions
requiredHook.postDispatch{value: msg.value}(hookMetadata, message);
(customHook ?? defaultHook).postDispatch{value: msg.value}(hookMetadata, message);
return messageId;
}
function quoteDispatch(
uint32 destination,
bytes32 recipient,
bytes calldata body,
IPostDispatchHook customHook?, // optional
bytes calldata hookMetadata? // optional
) external view returns (uint256 fee) {
fee = (customHook ?? defaultHook).quoteDispatch(hookMetadata ?? "", message)
+ requiredHook.quoteDispatch(hookMetadata ?? "", message);
}
}
Interchain Gas Paymaster Hook Implementation
As a motivating example, the default hook may contain an interchain gas payment hook with defaults for gas limit and refund address that may be overridden via metadata.
contract IgpHook is InterchainGasPaymaster {
uint256 immutable DEFAULT_GAS_LIMIT;
function postDispatch(
bytes metadata,
bytes message
) external payable {
uint256 gasLimit = metadata.gasLimit() ?? DEFAULT_GAS_LIMIT;
address refundAddress = metadata.refundAddress() ?? message.sender();
payGasFor(
message.id(),
message.destination(),
gasLimit,
refundAddress
);
}
}