Hyperlane
Hyperlane is the first permissionless interoperability layer. Unlike bridge protocols that require governance votes or committee approvals to add new chains, anyone can deploy Hyperlane to any blockchain — EVM, Cosmos, Sealevel (Solana), or Move — without permission. Messages are secured by configurable Interchain Security Modules (ISMs), giving developers sovereign control over their cross-chain security model rather than trusting a single validator set.
What You Probably Got Wrong
AI agents confuse Hyperlane with traditional bridges and get ISM, Warp Route, and messaging patterns wrong. These are the critical corrections.
- Hyperlane is permissionless — you CAN deploy to chains not officially supported. Unlike LayerZero or Wormhole, Hyperlane does not require a governance vote to expand to new chains. You deploy the Mailbox, ISM, and ValidatorAnnounce contracts yourself, run your own validators, and your chain is live. This is the core differentiator.
- ISMs (Interchain Security Modules) are composable — you are not locked into one security model. You can combine MultisigISM, RoutingISM, and AggregationISM to build custom security stacks. A message can require 3-of-5 validators AND an optimistic fraud proof AND a ZK proof. Security is modular, not monolithic.
- Warp Routes are NOT the same as bridges. Warp Routes are token-specific contract pairs deployed on origin and destination chains. A Warp Route for USDC on Ethereum<->Arbitrum is a separate deployment from USDC on Ethereum<->Optimism. Each route has its own collateral/synthetic relationship and ISM configuration.
dispatch()returns a message ID, NOT a delivery confirmation. Thebytes32returned bydispatch()is a unique message identifier. It does NOT mean the message was delivered. Delivery happens asynchronously when a relayer submits the message to the destination chain's Mailbox, which then callshandle()on the recipient.- Default ISM may differ per chain — always check what ISM your messages route through. Each Mailbox has a
defaultIsm()that applies when the recipient contract does not specify its own ISM viainterchainSecurityModule(). The default ISM is set by the Mailbox owner and varies across deployments. handle()function signature is strict:handle(uint32 _origin, bytes32 _sender, bytes _body). The_senderisbytes32, notaddress. For EVM origins, the sender address is left-padded with 12 zero bytes to fill 32 bytes. If you cast incorrectly, sender validation will fail silently.- Sender addresses are
bytes32padded, notaddresstype. When dispatching from EVM,msg.senderis converted tobytes32via left-padding. When receiving, you must convert back:address(uint160(uint256(_sender))). Getting this wrong means your access control checks will always fail. - Message delivery is NOT guaranteed — relayers must be running for the destination chain. Hyperlane's default relayer infrastructure covers major chains, but if you deploy to a new chain, YOU must run a relayer. No relayer = messages sit in the origin Mailbox permanently.
- Interchain gas payment is separate from dispatch. You must pay for destination chain gas via the
InterchainGasPaymasterhook or a post-dispatch hook. If you skip this, the default relayer has no incentive to deliver your message. UsequoteDispatch()to estimate the fee. - Hyperlane V3 is the current version. V3 introduced hooks (post-dispatch hooks for gas payment, custom logic), replaced the old
InterchainGasPaymaster.payForGas()pattern with hook-based gas payment, and usesmailbox.dispatch()with metadata and hook parameters.
Quick Start
Installation
npm install @hyperlane-xyz/sdk @hyperlane-xyz/core viem
Client Setup
import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
const publicClient = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL),
});
const account = privateKeyToAccount(
process.env.PRIVATE_KEY as `0x${string}`
);
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http(process.env.RPC_URL),
});
Minimal Message Dispatch
import { type Address, encodePacked, pad, toHex } from "viem";
const MAILBOX = "0xc005dc82818d67AF737725bD4bf75435d065D239" as const;
const mailboxAbi = [
{
name: "dispatch",
type: "function",
stateMutability: "payable",
inputs: [
{ name: "_destinationDomain", type: "uint32" },
{ name: "_recipientAddress", type: "bytes32" },
{ name: "_messageBody", type: "bytes" },
],
outputs: [{ name: "messageId", type: "bytes32" }],
},
{
name: "quoteDispatch",
type: "function",
stateMutability: "view",
inputs: [
{ name: "_destinationDomain", type: "uint32" },
{ name: "_recipientAddress", type: "bytes32" },
{ name: "_messageBody", type: "bytes" },
],
outputs: [{ name: "fee", type: "uint256" }],
},
] as const;
// Hyperlane domain ID for Arbitrum
const ARBITRUM_DOMAIN = 42161;
// Recipient contract on Arbitrum (must implement IMessageRecipient)
const recipientAddress = pad(
"0xYourRecipientContractAddress" as Address,
{ size: 32 }
);
const messageBody = toHex("Hello from Ethereum");
// Quote the interchain gas fee
const fee = await publicClient.readContract({
address: MAILBOX,
abi: mailboxAbi,
functionName: "quoteDispatch",
args: [ARBITRUM_DOMAIN, recipientAddress, messageBody],
});
// Dispatch the message with gas payment
const { request } = await publicClient.simulateContract({
address: MAILBOX,
abi: mailboxAbi,
functionName: "dispatch",
args: [ARBITRUM_DOMAIN, recipientAddress, messageBody],
value: fee,
account: account.address,
});
const hash = await walletClient.writeContract(request);
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") throw new Error("Dispatch reverted");
Core Concepts
Mailbox
The Mailbox is the core contract on every Hyperlane-enabled chain. It handles message dispatch (sending) and message processing (receiving). Every chain has exactly one Mailbox. Messages dispatched through the Mailbox are assigned a unique bytes32 message ID and stored in an incremental Merkle tree.
Domain IDs
Hyperlane identifies chains by uint32 domain IDs, not chain IDs. For EVM chains, the domain ID typically matches the chain ID, but this is not guaranteed for non-EVM chains.
| Chain | Domain ID |
|---|---|
| Ethereum | 1 |
| Arbitrum | 42161 |
| Optimism | 10 |
| Base | 8453 |
| Polygon | 137 |
Interchain Security Module (ISM)
ISMs are modular contracts that verify the authenticity of inbound messages. When the Mailbox receives a message for delivery, it queries the recipient contract's interchainSecurityModule() function. If the recipient does not implement this, the Mailbox's defaultIsm() is used.
ISMs implement a single interface:
interface IInterchainSecurityModule {
function moduleType() external view returns (uint8);
function verify(
bytes calldata _metadata,
bytes calldata _message
) external returns (bool);
}
Validators
Validators watch the origin chain Mailbox for dispatched messages, sign attestations of the Merkle root, and publish signatures. Relayers collect these signatures and submit them as metadata when delivering messages on the destination chain. The MultisigISM verifies these signatures against a configured validator set.
Relayers
Relayers are off-chain agents that deliver messages from origin to destination. They watch origin Mailboxes for Dispatch events, gather validator signatures (metadata), and call process() on the destination Mailbox. Hyperlane operates default relayers for supported chains, but anyone can run a relayer.
Hooks
Post-dispatch hooks execute logic after a message is dispatched. The most important hook is the InterchainGasPaymaster, which collects payment for destination chain gas. Hooks are configured per-Mailbox and can be overridden per-dispatch.
Sending & Receiving Messages
Dispatch Interface
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
interface IMailbox {
/// @notice Dispatch a message to a destination domain
/// @param _destinationDomain The domain ID of the destination chain
/// @param _recipientAddress The recipient contract (bytes32, left-padded for EVM)
/// @param _messageBody Arbitrary bytes payload
/// @return messageId Unique message identifier
function dispatch(
uint32 _destinationDomain,
bytes32 _recipientAddress,
bytes calldata _messageBody
) external payable returns (bytes32 messageId);
/// @notice Quote the fee for dispatching a message
function quoteDispatch(
uint32 _destinationDomain,
bytes32 _recipientAddress,
bytes calldata _messageBody
) external view returns (uint256 fee);
/// @notice Process a delivered message (called by relayer)
function process(
bytes calldata _metadata,
bytes calldata _message
) external;
}
IMessageRecipient Interface
Every contract that receives Hyperlane messages must implement IMessageRecipient:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
interface IMessageRecipient {
/// @notice Handle a message delivered by the Mailbox
/// @param _origin Domain ID of the source chain
/// @param _sender Address of the sender (bytes32, left-padded for EVM)
/// @param _body The message payload
function handle(
uint32 _origin,
bytes32 _sender,
bytes calldata _body
) external payable;
}
Address Conversion Utilities
/// @notice Convert an EVM address to bytes32 (left-pad with zeros)
function addressToBytes32(address _addr) internal pure returns (bytes32) {
return bytes32(uint256(uint160(_addr)));
}
/// @notice Convert bytes32 back to an EVM address
function bytes32ToAddress(bytes32 _buf) internal pure returns (address) {
return address(uint160(uint256(_buf)));
}
TypeScript Address Conversion
import { pad, type Address } from "viem";
function addressToBytes32(addr: Address): `0x${string}` {
return pad(addr, { size: 32 });
}
function bytes32ToAddress(buf: `0x${string}`): Address {
return `0x${buf.slice(26)}` as Address;
}
Sending a Message (TypeScript)
import { pad, toHex, type Address, encodeAbiParameters } from "viem";
const MAILBOX = "0xc005dc82818d67AF737725bD4bf75435d065D239" as const;
const DESTINATION_DOMAIN = 42161; // Arbitrum
const recipient: Address = "0xYourRecipientContract";
const recipientBytes32 = pad(recipient, { size: 32 });
// Encode a structured payload
const payload = encodeAbiParameters(
[
{ name: "action", type: "uint8" },
{ name: "amount", type: "uint256" },
{ name: "recipient", type: "address" },
],
[1, 1000000000000000000n, account.address]
);
const fee = await publicClient.readContract({
address: MAILBOX,
abi: mailboxAbi,
functionName: "quoteDispatch",
args: [DESTINATION_DOMAIN, recipientBytes32, payload],
});
const { request } = await publicClient.simulateContract({
address: MAILBOX,
abi: mailboxAbi,
functionName: "dispatch",
args: [DESTINATION_DOMAIN, recipientBytes32, payload],
value: fee,
account: account.address,
});
const hash = await walletClient.writeContract(request);
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") throw new Error("Dispatch reverted");
Warp Routes
Warp Routes are Hyperlane's token bridging primitive. They consist of paired contracts on origin and destination chains that lock/burn tokens on one side and mint/unlock on the other.
Warp Route Types
| Type | Origin Behavior | Destination Behavior | Use Case |
|---|---|---|---|
HypERC20Collateral |
Locks tokens in contract | Mints synthetic HypERC20 |
Bridge existing ERC-20 |
HypERC20 |
Burns synthetic tokens | Mints synthetic tokens | Synthetic side of a collateral route |
HypNative |
Locks native ETH | Mints synthetic HypERC20 |
Bridge native gas token |
HypNativeScaled |
Locks native token (scaled) | Mints scaled synthetic | When decimals differ across chains |
Deploying a Warp Route (SDK)
import { HyperlaneCore, WarpRouteDeployConfig } from "@hyperlane-xyz/sdk";
// Warp Route config: bridge USDC from Ethereum to Arbitrum
const warpConfig: WarpRouteDeployConfig = {
ethereum: {
type: "collateral",
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
mailbox: "0xc005dc82818d67AF737725bD4bf75435d065D239",
interchainSecurityModule: "0x...", // optional: custom ISM
},
arbitrum: {
type: "synthetic",
mailbox: "0x979Ca5202784112f4738403dBec5D0F3B9daabB9",
interchainSecurityModule: "0x...", // optional: custom ISM
},
};
Transferring Tokens via Warp Route
const warpRouteAbi = [
{
name: "transferRemote",
type: "function",
stateMutability: "payable",
inputs: [
{ name: "_destination", type: "uint32" },
{ name: "_recipient", type: "bytes32" },
{ name: "_amountOrId", type: "uint256" },
],
outputs: [{ name: "messageId", type: "bytes32" }],
},
{
name: "quoteGasPayment",
type: "function",
stateMutability: "view",
inputs: [{ name: "_destinationDomain", type: "uint32" }],
outputs: [{ name: "", type: "uint256" }],
},
] as const;
const WARP_ROUTE_COLLATERAL = "0xYourWarpRouteCollateralContract" as const;
const ARBITRUM_DOMAIN = 42161;
const amount = 1000_000000n; // 1000 USDC (6 decimals)
// ERC-20 approval to Warp Route contract
const erc20Abi = [
{
name: "approve",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
] as const;
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as const;
const { request: approveRequest } = await publicClient.simulateContract({
address: USDC,
abi: erc20Abi,
functionName: "approve",
args: [WARP_ROUTE_COLLATERAL, amount],
account: account.address,
});
const approveHash = await walletClient.writeContract(approveRequest);
const approveReceipt = await publicClient.waitForTransactionReceipt({
hash: approveHash,
});
if (approveReceipt.status !== "success") throw new Error("Approval reverted");
// Quote interchain gas
const gasFee = await publicClient.readContract({
address: WARP_ROUTE_COLLATERAL,
abi: warpRouteAbi,
functionName: "quoteGasPayment",
args: [ARBITRUM_DOMAIN],
});
// Execute cross-chain transfer
const recipientBytes32 = pad(account.address, { size: 32 });
const { request: transferRequest } = await publicClient.simulateContract({
address: WARP_ROUTE_COLLATERAL,
abi: warpRouteAbi,
functionName: "transferRemote",
args: [ARBITRUM_DOMAIN, recipientBytes32, amount],
value: gasFee,
account: account.address,
});
const transferHash = await walletClient.writeContract(transferRequest);
const transferReceipt = await publicClient.waitForTransactionReceipt({
hash: transferHash,
});
if (transferReceipt.status !== "success") throw new Error("Transfer reverted");
Interchain Security Modules (ISM)
ISM Types
| Type | Module Type ID | Security Model | When to Use |
|---|---|---|---|
MultisigISM |
3 |
M-of-N validator signatures | Default for most deployments |
RoutingISM |
4 |
Routes to different ISMs per origin domain | Different security per source chain |
AggregationISM |
6 |
Requires multiple ISMs to pass (AND logic) | Defense in depth — combine models |
OptimisticISM |
Custom | Optimistic verification with fraud proofs | Lower latency, higher trust assumption |
WasmISM |
Custom | Custom verification logic in WASM | Non-EVM verification, ZK proofs |
Configuring a MultisigISM
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {StaticMultisigISMFactory} from "@hyperlane-xyz/core/contracts/isms/multisig/StaticMultisigISMFactory.sol";
// Deploy a MultisigISM that requires 3-of-5 validator signatures
// for messages from Ethereum (domain 1)
address[] memory validators = new address[](5);
validators[0] = 0x1111111111111111111111111111111111111111;
validators[1] = 0x2222222222222222222222222222222222222222;
validators[2] = 0x3333333333333333333333333333333333333333;
validators[3] = 0x4444444444444444444444444444444444444444;
validators[4] = 0x5555555555555555555555555555555555555555;
uint8 threshold = 3;
IStaticMultisigISMFactory factory = IStaticMultisigISMFactory(
MULTISIG_ISM_FACTORY_ADDRESS
);
address ism = factory.deploy(validators, threshold);
Setting a Custom ISM on Your Recipient
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {IInterchainSecurityModule} from "@hyperlane-xyz/core/contracts/interfaces/IInterchainSecurityModule.sol";
import {IMessageRecipient} from "@hyperlane-xyz/core/contracts/interfaces/IMessageRecipient.sol";
contract MyRecipient is IMessageRecipient {
IInterchainSecurityModule public interchainSecurityModule;
address public immutable mailbox;
error Unauthorized();
error InvalidOrigin(uint32 origin);
event MessageReceived(uint32 indexed origin, bytes32 indexed sender, bytes body);
constructor(address _mailbox, address _ism) {
mailbox = _mailbox;
interchainSecurityModule = IInterchainSecurityModule(_ism);
}
function handle(
uint32 _origin,
bytes32 _sender,
bytes calldata _body
) external payable override {
if (msg.sender != mailbox) revert Unauthorized();
emit MessageReceived(_origin, _sender, _body);
}
}
AggregationISM (Combine Multiple Security Models)
import {StaticAggregationISMFactory} from "@hyperlane-xyz/core/contracts/isms/aggregation/StaticAggregationISMFactory.sol";
// Require BOTH a MultisigISM AND a custom verification ISM to pass
address[] memory modules = new address[](2);
modules[0] = address(multisigIsm); // 3-of-5 validator signatures
modules[1] = address(customVerifier); // additional verification
uint8 threshold = 2; // all modules must pass
IStaticAggregationISMFactory factory = IStaticAggregationISMFactory(
AGGREGATION_ISM_FACTORY_ADDRESS
);
address aggregationIsm = factory.deploy(modules, threshold);
RoutingISM (Per-Origin Security)
import {DomainRoutingISMFactory} from "@hyperlane-xyz/core/contracts/isms/routing/DomainRoutingISMFactory.sol";
// Use different ISMs for messages from different origin chains
uint32[] memory domains = new uint32[](2);
domains[0] = 1; // Ethereum
domains[1] = 42161; // Arbitrum
address[] memory isms = new address[](2);
isms[0] = address(ethereumMultisigIsm); // stricter for Ethereum
isms[1] = address(arbitrumMultisigIsm); // different validators for Arbitrum
IDomainRoutingISMFactory factory = IDomainRoutingISMFactory(
DOMAIN_ROUTING_ISM_FACTORY_ADDRESS
);
address routingIsm = factory.deploy(msg.sender, domains, isms);
Hooks
Post-Dispatch Hooks
Hooks execute after dispatch() and before the function returns. The primary use is interchain gas payment, but hooks support arbitrary logic.
interface IPostDispatchHook {
/// @notice Type identifier for this hook
function hookType() external view returns (uint8);
/// @notice Returns whether the hook supports metadata
function supportsMetadata(
bytes calldata metadata
) external view returns (bool);
/// @notice Post-dispatch hook logic
function postDispatch(
bytes calldata metadata,
bytes calldata message
) external payable;
/// @notice Quote the fee for this hook
function quoteDispatch(
bytes calldata metadata,
bytes calldata message
) external view returns (uint256);
}
Dispatch with Custom Hook and Metadata
interface IMailboxV3 {
/// @notice Dispatch with explicit hook and metadata
function dispatch(
uint32 _destinationDomain,
bytes32 _recipientAddress,
bytes calldata _messageBody,
bytes calldata _hookMetadata,
IPostDispatchHook _hook
) external payable returns (bytes32 messageId);
}
Interchain Gas Payment
// The standard dispatch with value covers gas payment via the default hook
const fee = await publicClient.readContract({
address: MAILBOX,
abi: mailboxAbi,
functionName: "quoteDispatch",
args: [destinationDomain, recipientBytes32, messageBody],
});
// Send dispatch with the quoted fee as msg.value
const { request } = await publicClient.simulateContract({
address: MAILBOX,
abi: mailboxAbi,
functionName: "dispatch",
args: [destinationDomain, recipientBytes32, messageBody],
value: fee,
account: account.address,
});
Interchain Accounts
Interchain Accounts (ICA) allow you to execute transactions on remote chains. When you send an ICA call from Chain A, Hyperlane creates a deterministic account on Chain B that only Chain A can control. This account can call any contract on Chain B.
ICA Call Structure
struct CallLib.Call {
address to; // target contract on destination
uint256 value; // ETH value to send
bytes data; // calldata to execute
}
Executing a Remote Transaction
const ICA_ROUTER = "0xYourICARouterAddress" as const;
const icaRouterAbi = [
{
name: "callRemote",
type: "function",
stateMutability: "payable",
inputs: [
{ name: "_destinationDomain", type: "uint32" },
{
name: "_calls",
type: "tuple[]",
components: [
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "data", type: "bytes" },
],
},
],
outputs: [{ name: "messageId", type: "bytes32" }],
},
{
name: "quoteGasPayment",
type: "function",
stateMutability: "view",
inputs: [{ name: "_destinationDomain", type: "uint32" }],
outputs: [{ name: "", type: "uint256" }],
},
{
name: "getRemoteInterchainAccount",
type: "function",
stateMutability: "view",
inputs: [
{ name: "_destination", type: "uint32" },
{ name: "_owner", type: "address" },
{ name: "_router", type: "address" },
{ name: "_ism", type: "address" },
],
outputs: [{ name: "", type: "address" }],
},
] as const;
import { encodeFunctionData } from "viem";
// Encode the call you want to execute on the destination chain
const remoteCalldata = encodeFunctionData({
abi: [
{
name: "transfer",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
],
functionName: "transfer",
args: ["0xRecipientAddress" as Address, 1000_000000n],
});
const ARBITRUM_DOMAIN = 42161;
const gasFee = await publicClient.readContract({
address: ICA_ROUTER,
abi: icaRouterAbi,
functionName: "quoteGasPayment",
args: [ARBITRUM_DOMAIN],
});
const { request } = await publicClient.simulateContract({
address: ICA_ROUTER,
abi: icaRouterAbi,
functionName: "callRemote",
args: [
ARBITRUM_DOMAIN,
[
{
to: "0xTokenContractOnArbitrum" as Address,
value: 0n,
data: remoteCalldata,
},
],
],
value: gasFee,
account: account.address,
});
const hash = await walletClient.writeContract(request);
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") throw new Error("ICA call reverted");
Getting Your ICA Address
The ICA address on the destination chain is deterministic based on your origin address, the ICA router, and the ISM:
const icaAddress = await publicClient.readContract({
address: ICA_ROUTER,
abi: icaRouterAbi,
functionName: "getRemoteInterchainAccount",
args: [
ARBITRUM_DOMAIN,
account.address,
ICA_ROUTER,
"0x0000000000000000000000000000000000000000", // default ISM
],
});
Permissionless Deployment
Deploy Hyperlane to a New Chain
Hyperlane can be deployed to any chain without permission. The process:
- Deploy core contracts — Mailbox, ProxyAdmin, ISM factories
- Configure default ISM — Set the security model for inbound messages
- Deploy ValidatorAnnounce — Validators publish their signing locations here
- Run validators — At least one validator must attest to messages
- Run a relayer — Delivers messages from/to your chain
Using the Hyperlane CLI
# Install the CLI
npm install -g @hyperlane-xyz/cli
# Initialize a chain config for your new chain
hyperlane config create chain
# Deploy core contracts
hyperlane deploy core \
--chain your-chain-name \
--key $PRIVATE_KEY
# Deploy a Warp Route
hyperlane deploy warp \
--config warp-route-config.yaml \
--key $PRIVATE_KEY
# Run a validator
hyperlane validator \
--chain your-chain-name \
--key $VALIDATOR_PRIVATE_KEY
# Run a relayer
hyperlane relayer \
--chains your-chain-name,ethereum,arbitrum
Chain Configuration
# chain-config.yaml
yourchain:
chainId: 123456
domainId: 123456
name: yourchain
protocol: ethereum
rpcUrls:
- http: https://rpc.yourchain.com
nativeToken:
name: ETH
symbol: ETH
decimals: 18
blockExplorers:
- name: YourChainExplorer
url: https://explorer.yourchain.com
apiUrl: https://api.explorer.yourchain.com/api
Fee Estimation
quoteDispatch
Always call quoteDispatch() before dispatching to determine the required msg.value for interchain gas payment:
const fee = await publicClient.readContract({
address: MAILBOX,
abi: [
{
name: "quoteDispatch",
type: "function",
stateMutability: "view",
inputs: [
{ name: "_destinationDomain", type: "uint32" },
{ name: "_recipientAddress", type: "bytes32" },
{ name: "_messageBody", type: "bytes" },
],
outputs: [{ name: "fee", type: "uint256" }],
},
] as const,
functionName: "quoteDispatch",
args: [destinationDomain, recipientBytes32, messageBody],
});
// Always send at least the quoted fee — overpayment is refunded by some hooks
const { request } = await publicClient.simulateContract({
address: MAILBOX,
abi: mailboxAbi,
functionName: "dispatch",
args: [destinationDomain, recipientBytes32, messageBody],
value: fee,
account: account.address,
});
Gas Overhead Estimation
Interchain gas fees account for:
- Destination chain gas price (estimated by the relayer)
handle()execution gas on the destination- ISM verification gas (signature checks)
- Relayer operational overhead
For large payloads or complex handle() logic, gas costs scale linearly. Estimate your handle() gas usage on a fork and add a safety margin.
Contract Addresses
Last verified: February 2026
Core Contracts
| Contract | Ethereum | Arbitrum | Base | Optimism | Polygon |
|---|---|---|---|---|---|
| Mailbox | 0xc005dc82818d67AF737725bD4bf75435d065D239 |
0x979Ca5202784112f4738403dBec5D0F3B9daabB9 |
0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D |
0xd4C1905BB739D293F7a14F97241A65a7458291c3 |
0x5d934f4e2f797775e53561bB72aca21ba36B96BB |
| DefaultISM | 0x6b1bb4ce664Bb4164AEB4d3D2E7DE7450DD8084C |
0x8105a095368f1a184CceA86cDB98920e74Ffb992 |
0x60448b880c8Aa3fef44dCcc2CaAB4FD178DeE46f |
0xAa4Fe29e0db0D2891352e2770b400B1e0B0C2D67 |
0x9C2ae13212B89Ced2027c2a7Ef26eb3eEf143867 |
| InterchainGasPaymaster | 0x6cA0B6D22da47f091B7613C7A727eC00ac3486d2 |
0x3b6044acd6767f017e99318AA6Ef93b7B06A5a22 |
0xc3F23848Ed2e04C0c6d41bd7804fa8f89F940B94 |
0xD8A76C4D91fCbB7Cc8eA795DFDF870E48368995C |
0x0071740Bf129b05C4684abfbBeD248D80971cce2 |
| ValidatorAnnounce | 0x9bBdef63594D5FFc2f370Fe52115DdAAFBA66D76 |
0x9bBdef63594D5FFc2f370Fe52115DdAAFBA66D76 |
0x9bBdef63594D5FFc2f370Fe52115DdAAFBA66D76 |
0x9bBdef63594D5FFc2f370Fe52115DdAAFBA66D76 |
0x9bBdef63594D5FFc2f370Fe52115DdAAFBA66D76 |
ISM Factory Contracts
| Contract | Ethereum | Arbitrum |
|---|---|---|
| StaticMultisigISMFactory | 0x8b83fefd896fAa52057798f6426E9f0B080FCCcE |
0x8b83fefd896fAa52057798f6426E9f0B080FCCcE |
| StaticAggregationISMFactory | 0x8F7454AC98228f3504bB91eA3D0281e457E00385 |
0x8F7454AC98228f3504bB91eA3D0281e457E00385 |
| DomainRoutingISMFactory | 0xC2E36cd6e32e194EE11f15D9273B64461A4D694A |
0xC2E36cd6e32e194EE11f15D9273B64461A4D694A |
Error Handling
| Error | Cause | Fix |
|---|---|---|
!msg.value |
Insufficient gas payment sent with dispatch | Call quoteDispatch() and send the returned fee as msg.value |
!module |
ISM verification failed | Check validator signatures, ensure correct ISM is configured |
!recipient.ism |
Recipient's ISM address is invalid | Verify recipient implements interchainSecurityModule() returning a valid address |
delivered |
Message already processed on destination | This is idempotent — the message was already delivered. No action needed |
!paused |
Mailbox is paused | Wait for Mailbox owner to unpause, or use an alternative Mailbox |
!threshold |
MultisigISM threshold not met | Ensure enough validators have signed — check validator set and threshold |
!signer |
Validator signature is invalid | Verify the signing validator is in the ISM's validator set |
Security
Sender Verification (Non-Negotiable)
Always verify msg.sender == mailbox in handle(). Without this check, anyone can call your recipient contract directly, bypassing cross-chain verification.
function handle(
uint32 _origin,
bytes32 _sender,
bytes calldata _body
) external payable override {
if (msg.sender != address(mailbox)) revert Unauthorized();
// Safe to process
}
Origin and Sender Validation
Validate both the origin domain and the sender address to prevent unauthorized cross-chain calls:
error UnauthorizedSender(uint32 origin, bytes32 sender);
mapping(uint32 => bytes32) public authorizedSenders;
function handle(
uint32 _origin,
bytes32 _sender,
bytes calldata _body
) external payable override {
if (msg.sender != address(mailbox)) revert Unauthorized();
if (authorizedSenders[_origin] != _sender) {
revert UnauthorizedSender(_origin, _sender);
}
// Safe to process
}
ISM Selection
- For high-value operations: use
AggregationISMcombining MultisigISM + an additional verification layer - For standard messaging:
MultisigISMwith a reputable validator set and appropriate threshold - For per-chain granularity:
RoutingISMto apply different security per origin chain - Never rely solely on the default ISM for high-value operations — deploy your own