Skip to main content

Create your own Hook & ISM

Hooks and ISMs have a complementary relationship: you can customize your behavior from origin and they use a pairwise ISM contract on the destination to verify your custom hook behavior.

You can implement and utilize your own hook and ISM pattern as per your requirements. You can use an external bridge provide like Wormhole or Chainlink's CCIP by implementing the IPostDispatchHook interface on the source chain and IInterchainSecurityModule on the destination chain.

IPostDispatchHook Interface
interface IPostDispatchHook {
enum Types {
UNUSED,
ROUTING,
AGGREGATION,
MERKLE_TREE,
INTERCHAIN_GAS_PAYMASTER,
FALLBACK_ROUTING,
ID_AUTH_ISM,
PAUSABLE,
PROTOCOL_FEE,
LAYER_ZERO_V1,
Rate_Limited_Hook
}

/**
* @notice Returns an enum that represents the type of hook
*/
function hookType() external view returns (uint8);

/**
* @notice Returns whether the hook supports metadata
* @param metadata metadata
* @return Whether the hook supports metadata
*/
function supportsMetadata(
bytes calldata metadata
) external view returns (bool);

/**
* @notice Post action after a message is dispatched via the Mailbox
* @param metadata The metadata required for the hook
* @param message The message passed from the Mailbox.dispatch() call
*/
function postDispatch(
bytes calldata metadata,
bytes calldata message
) external payable;

/**
* @notice Compute the payment required by the postDispatch call
* @param metadata The metadata required for the hook
* @param message The message passed from the Mailbox.dispatch() call
* @return Quoted payment for the postDispatch call
*/
function quoteDispatch(
bytes calldata metadata,
bytes calldata message
IInterchainSecurityModule Interface
interface IInterchainSecurityModule {
enum Types {
UNUSED,
ROUTING,
AGGREGATION,
LEGACY_MULTISIG,
MERKLE_ROOT_MULTISIG,
MESSAGE_ID_MULTISIG,
NULL, // used with relayer carrying no metadata
CCIP_READ
}

/**
* @notice Returns an enum that represents the type of security model
* encoded by this ISM.
* @dev Relayers infer how to fetch and format metadata.
*/
function moduleType() external view returns (uint8);

/**
* @notice Defines a security model responsible for verifying interchain
* messages based on the provided metadata.
* @param _metadata Off-chain metadata provided by a relayer, specific to
* the security model encoded by the module (e.g. validator signatures)
* @param _message Hyperlane encoded interchain message
* @return True if the message was verified
*/
function verify(
bytes calldata _metadata,
bytes calldata _message
) external returns (bool);
}
info

Hooks currently expect metadata to be formatted with the StandardHookMetadata library.

You can also inherit from our AbstractMessageIdAuthorizedIsm which allows for access control for a intermediate verifyMessageId function call which sets in storage the messageId to true if received from the authorized AbstractMessageIdAuthHook hook. This pattern is used currently in the OpStackHook <> OpStackIsm pattern.

Workflow

Interface

After implementing the above interfaces, you can override default hook along the hook metadata by using the overloaded dispatch call in our mailbox:

function dispatch(
uint32 destinationDomain,
bytes32 recipientAddress,
bytes calldata messageBody,
bytes calldata metadata,
IPostDispatchHook hook
) public payable virtual returns (bytes32) {

Examples

// send message from alfajores to bsctestnet TestRecipient
IMailbox mailbox = IMailbox("0xEf9F292fcEBC3848bF4bB92a96a04F9ECBb78E59");
IPostDispatchHook merkleTree = IPostDispatchHook("0x221FA9CBaFcd6c1C3d206571Cf4427703e023FFa");
mailbox.dispatch(
97,
"0x0000000000000000000000006489d13AcAd3B8dce4c5B31f375DE4f9451E7b38",
bytes("Hello, world"),
"0x", // empty metadata
merkleTree
);
  • On the source chain,

    • mailbox.dispatch() calls your custom hook via AbstractMessageIdAuthHook.postDispatch().
    • _postDispatch checks whether latestDispatchedId is the id being dispatched from the hook to make the mailbox is the contract calling the hook (since calling postDispatch isn't access controlled)
    • _sendMessageId calls your custom external bridge logic like calling the CCIP router contract.
  • On the destination chain,

    • the external bridge will call verifyMessageId function (which is access-controlled) and sets the messageId in the verifiedMessages mapping to true.
    • on receiving the message for the relayer, the mailbox will call your ISM contract (specified in your recipient address) which checks if the messageId in the verifiedMessages mapping is true and returns true to the mailbox and vice versa.
warning

AbstractMessageIdAuthorizedIsm can send msg.value through postDispatch calls and we utilize the verifiedMessages' little endian 255 bits for storing the msg.value and the top bit for the actual receipt of the messageId delivery. Therefore, you can send upto 2^255 amount of value of the native token from origin and the destination ISM can only receive 2^255 amount of value of native token on the destination chain.

Access Control

If postDispatch must only be called with a message that was just dispatched, the latestDispatchedId function on the Mailbox can be used to verify the message was actually dispatched.

info

This is used instead of some require(mailbox == msg.sender) to support composition where a hook may pass a message along to another hook.

The following utility is provided in the MailboxClient library for convenience.

}

function _isLatestDispatched(bytes32 id) internal view returns (bool) {