Skip to main content
What you’ll be able to do
  • Deliver a message from another chain to Metal.
  • Handle authenticated incoming messages in a receiving contract.
An incoming message starts in an Outbox on the source chain. A relayer proves its stored commitment under a checkpoint accepted by Metal’s light client for that chain. ForeignInbox verifies the evidence and calls the receiving contract with the original payload.

Check the route and receiving contract

Obtain the route provider’s authenticated source chain ID, source RPC endpoint, source Outbox address, artifact and approved runtime bytecode hash, Metal ForeignInbox address and artifact, and light-client configuration. You also need the receiving contract’s ABI, address and authorized source sender. Fund the relayer’s signing account on Metal and the source transaction’s signing account on its chain for their respective fees. Verify runtime bytecode against the supplied artifacts. Read the inbox’s lightClient(), foreignOutbox(), foreignOutboxCodeHash() and foreignChainId() and compare them to the route configuration. The code hash pins the runtime bytecode of a direct, immutable Outbox. A new Outbox version requires a new inbox. Preserve application records across route replacement to prevent completed instructions from executing again. The source RPC must support eth_getProof at a checkpoint that the configured light client accepts. Inside the receiving function, require both msg.sender == inbox and IForeignInbox(inbox).foreignSender() == authorizedSourceSender before changing application state. These checks authenticate the route and source application. The inbox interface exposes foreignSender() only during execution and returns zero otherwise. Anyone may relay a valid message, while the receiving contract authorizes its source sender.

Queue the source call

Encode the receiving contract’s function selector and arguments with its ABI. From the authorized source application, call Outbox.send with Metal’s chain ID as uint64, the receiving contract address, and that payload. A source contract must make this call when the Metal contract authorizes that contract as source sender. Calling the outbox directly from an externally owned account authenticates that account as the source sender. Save the signed transaction and its hash before broadcast. Decode MessageSent from the configured source outbox in its successful receipt. Match destination chain ID, target, sender and payload, then save the emitted nonce, source block number and block hash. Wait for source-chain finality before proceeding. If the submission result is uncertain, look up its saved transaction hash to resume the same message.

Select an accepted source checkpoint

Choose a finalized source block at or after the message’s block and confirm isAccepted(checkpointBlockHash) on the light client. Read its checkpoint return value and retain the accepted block number and state root. The light-client reference gives the exact return fields and update signatures. Metal block execution maintains Ethereum’s light-client checkpoints. latestUpdateTimestamp() can advance without a newer execution checkpoint, so use the checkpoint’s block number to decide whether it covers the message. Tempo and Arc require a public certificate source and documented conversion to their exact update arguments. Authenticate the provider’s pinned group key or validator set independently of that certificate feed. Each light client uses its source chain’s certificate format, as specified in the reference.

Request the account and storage proofs

Install the public viem dependency with pnpm add viem. Using a source-chain public client configured with the supplied endpoint, derive the storage key and request its proof at the selected block number:
nonce is the saved message nonce, and sourceOutbox is the authenticated outbox address. outboxCodeHash is the inbox’s foreignOutboxCodeHash() value, already matched to the provider’s approved runtime bytecode artifact. The outbox’s messages mapping is at storage slot zero. Select the storageProof entry whose key equals storageKey, comparing keys as 256-bit integers if their hex padding differs. Require a nonzero value and compare it to the message commitment below:
The account proof establishes both the outbox’s pinned code hash and its storage root under the accepted state root. The selected storage proof establishes the message commitment under that storage root. Pass the response’s accountProof and the selected entry’s proof separately so the inbox can verify both. If the endpoint provides proofs only at the latest block, collect the block and proof, then read the block again. Retry if it changed, and obtain checkpoint acceptance for the exact block hash and state root used by the proof. Keep the original proofs until acceptance, since a later checkpoint requires new proofs. For Ethereum, Metal must record that exact execution checkpoint. Use a historical proof provider if the endpoint cannot supply proofs for an accepted checkpoint.

Execute and record the result

Read ForeignInbox.consumed(nonce). If false, simulate and submit execute with checkpointBlockHash, nonce, sender, target, payload, accountProof, and storageProof, in that order. The relayer submits this Metal transaction from its signing account. The signing account’s balance in the gas token pays the transaction fee. Save the signed bytes and transaction hash before submission. Require a successful receipt and MessageExecuted from the configured inbox with the expected nonce, target and sender. Verify consumed(nonce) is true and check the receiving contract’s application event or state change. Check Metal finality and the receipt’s canonical block before applying an irreversible downstream effect. Key each delivery by Metal chain ID, inbox address and nonce, retaining the source transaction and message fields. On an uncertain response, recover the saved transaction hash and consumed state to find the existing execution. If still pending, rebroadcast the same signed transaction. Retrying delivery keeps the original message nonce, while a new send queues a separate instruction.

Recover and test failed delivery

A receiving contract’s revert rolls back the consumed flag and every change in the dispatched call. Track retries per message because the inbox allows other nonces to execute first. Define any ordering, expiry or cancellation rules in the application contract. Test the configured route with a disposable receiving contract. An altered payload or proof must fail without consuming the nonce. Cause the receiving contract to revert and verify that application state stays unchanged, then resolve that condition and deliver the same message. After success, replay must fail without repeating the effect. Save failed and successful attempts so a restarted relayer can distinguish retryable and completed delivery. Use a controlled proof fixture with a different account code hash to verify rejection before nonce consumption even when the storage value matches.