Skip to main content
What you’ll be able to do
  • Send a message from Metal to a contract on another chain.
  • Deliver the message and execute its destination call.
To call a contract on another blockchain, an application calls Metal’s outbox contract to record a message specifying the destination chain, receiving contract, and function call. A relayer delivers the message by submitting it to the inbox contract on the destination chain, along with proof that Metal recorded and finalized it. The inbox checks the proof and calls the receiving contract using the message’s function and arguments. Record the message as complete after verifying that the receiving contract performed the requested action and the destination block is final.

Prepare a configured route

Obtain the route provider’s authenticated configuration. You need the destination chain ID and RPC endpoint, its MetalLightClient and MetalInbox addresses and public artifacts, the receiving contract’s address and ABI, the authorized source sender, and the destination’s finality policy. Fund the signing accounts that submit the source and destination transactions for each chain’s transaction fees. Verify runtime bytecode against those artifacts and read the inbox’s lightClient() and metalChainId(). Check the light client’s Metal chain ID, genesis hash, verifier version, and validator configuration against the route provider’s authenticated values. The destination must support EIP-2537 and transient storage. Read the light-client interfaces for these checks. MetalLightClient accepts certificates only for epoch 0, so check the source network’s current certificate epoch before sending. The receiving contract must require both msg.sender == inbox and IMetalInbox(inbox).metalSender() == authorizedSourceSender inside its dispatched function. Its application state or event must identify the intended action. metalSender() is zero outside execution.

Record the message on Metal

Use the Metal outbox address in protocol contracts and the outbox ABI. Encode the receiving contract’s function call with its ABI, then call send(destinationChainId, target, payload). The destination chain ID is a uint64. target is the receiving contract, and payload contains its exact function selector and arguments. For a viem application, install the public dependency with pnpm add viem. Encode the action using the receiving contract’s published ABI:
receiverAbi, receiverFunction, and receiverArguments describe the intended call. Submit data to the outbox from the authorized source sender. If the receiving contract authorizes a source contract, that contract must call the outbox. A direct call from an externally owned account authenticates that account. When an application payment and message must succeed together, perform both in the same source transaction. Save the signed transaction and its hash before broadcast. Decode MessageSent from the exact outbox address in the successful receipt and match its destinationChainId, target, sender and payload to the intended message. Save the emitted nonce, receipt block number and block hash, then check canonical Metal finality. Use the nonce from the event, since concurrent sends can change a simulation’s predicted return value.

Select an accepted checkpoint

Read latestBlockHash() and then checkpoint(hash) on the destination light client. An accepted checkpoint covers the message when its Metal block number is at least the message’s source block number. If no accepted checkpoint covers it, read Metal’s finalized head with consensus_status and fetch metal_getFinalityCertificate for a covering finalized block using the certificate procedure. Require a non-null certificate, epoch zero, and keccak256(headerRlp) == blockHash. Submit update using the response’s headerRlp, epoch, view, parentView, signersBitmap, and signatureEipUncompressed, in that order. Both block height and view must advance the light client’s latest values. Save the signed destination transaction before broadcast, check successful execution and isAccepted(blockHash), then apply destination finality. One checkpoint can cover many messages. A null certificate can reflect delayed persistence or pruning. Retry for a recent finalized block or another endpoint. If another relayer advances the light client first, check whether the checkpoint hash was accepted. Otherwise, select a later accepted checkpoint and build its proof. Preserve the chosen hash and returned interopMessageRoot with the delivery record.

Build the message proof

Retrieve every MessageSent event from the Metal outbox from nonce zero through the checkpoint block, including messages for other destinations. Paginate block ranges to respect RPC limits. Verify the canonical block hashes, sort by nonce, and reject gaps or duplicates. The leaf count is the number of all messages at that block. Obtain missing event history from an indexer or RPC endpoint before attempting delivery. Each leaf uses ABI encoding of the full message:
Build a depth-32 binary tree whose leaf at index nonce is that commitment. Missing leaves are emptyLeaf. At each level, hash the left and right child in that order. The parent of two empty subtrees is pair(empty, empty). For a sparse implementation, store occupied nodes and derive missing siblings from the hashes of empty subtrees at each level. For the requested nonce, record its sibling at each of the 32 levels, starting at the leaf level. Divide the index by two after each level. To verify a path, hash pair(node, sibling) for an even index and pair(sibling, node) for an odd index. The message root binds the tree root and leaf count and must equal the checkpoint’s interopMessageRoot:
Here treeRoot is the depth-32 root and leafCount is a bigint. Require nonce < leafCount. Changing the checkpoint requires a matching leaf count and path, even when the source message is unchanged. An old accepted checkpoint and its original proof remain usable while the nonce is unconsumed.

Execute and verify the destination effect

Check MetalInbox.consumed(nonce). If false, simulate and submit execute with checkpointBlockHash, nonce, sender, target, payload, leafCount, and the 32-element path, in that order. The relayer submits this destination transaction from a signing account funded for that chain’s transaction fees. Save its signed bytes and hash before broadcast. Require a successful receipt and the inbox’s MessageExecuted with the expected nonce, target and sender. Check consumed(nonce) == true, then read the receiving contract’s event or state to verify the intended application effect. Confirm the receipt’s canonical block and destination finality before marking the delivery complete. Key the delivery record by destination chain ID, inbox address and nonce. Retain the source chain ID and outbox address with the original message fields, checkpoint and transaction attempts. If a response is lost, look up the saved hash and consumed state before rebroadcasting the same signed transaction. Retrying delivery keeps the original message nonce, while a new send records a separate instruction.

Recover and test rejection

A receiving contract’s revert rolls back the application call and consumed flag. Track retries per message because the inbox allows other nonces to execute first. Define any ordering, expiry, cancellation and acknowledgement rules in the application protocol. Test the route with a disposable receiving contract. Alter the payload and a proof sibling and confirm rejection leaves state unchanged. Cause the receiving contract to revert, confirm consumed stays false, remove that condition, and deliver the same message. After success, replay must fail without repeating its effect. Record the final destination state and transaction hashes to test recovery after a relayer restart.