Building an omnichain app means writing contracts that send and receive arbitrary cross-chain messages: function calls, asset transfers, state updates. The transport is a generalized messaging rail like LayerZero v2, Wormhole, Hyperlane, Chainlink CCIP, or Axelar GMP. This guide walks through the integration patterns that show up in every production deployment: rail selection, message format design, source finality handling, fee accounting, replay safety, and failure recovery. Examples reference real protocols and real production code rather than abstract patterns.
The reader is assumed to have written EVM smart contracts. Concepts apply to non-EVM chains too. Wormhole and Axelar both support Solana, Sui, Aptos, and Cosmos, but the example code in this guide targets Solidity. As of April 2026, more than 200 production omnichain applications run across these 5 rails, including Stargate (30+ chains), PancakeSwap, Aave GHO, Tapioca, and Squid Router (over 60 chains).
What Is an Omnichain App?
An omnichain app is one whose state spans multiple blockchains and whose user-facing actions can affect that state regardless of which chain the user is on. A user holding a position on Arbitrum can borrow against it on Optimism. A governance vote on Ethereum can update parameters on Polygon. A token deployed across ten chains has a single canonical supply tracked through cross-chain mints and burns.
Three patterns recur in production:
Mirrored state. The same state exists on every chain and is kept in sync by cross-chain messages. Examples: cross-chain governance vote tallies, oracle price feeds, omnichain token supply.
Sharded state with cross-chain coordination. Different state lives on different chains, with cross-chain messages coordinating actions across them. Examples: Tapioca's lending markets, Aave's GHO supply caps (which target a 100M unit cap per chain).
Single source of truth with cross-chain triggers. State lives on one chain, and other chains can trigger reads or invoke functions remotely. Examples: ENS resolution from L2s (added in 2023), cross-chain liquidation triggers.
The right pattern depends on the application. Choosing wrong is expensive: switching from sharded to mirrored after launch typically requires rewriting 60-80% of the app's contracts and migrating user state across chains.
Picking a Messaging Rail
The five major messaging protocols make different trade-offs. The choice usually reduces to chain coverage, security model, and latency requirements.
Rail | Best fit when | Reference doc |
LayerZero v2 | App spans many EVM chains; per-route security configurability matters | |
Wormhole | Solana, Aptos, Sui, or Cosmos chains are primary; fixed multisig assumption is acceptable | |
Hyperlane | Deploying on a new appchain or rollup; permissionless deployment is needed | |
Chainlink CCIP | Institutional or regulated flows; rate limits and risk-network pause matter | |
Axelar GMP | Slashing-backed validator economics matter; gas abstraction across many chains is needed |
Most production omnichain apps integrate two or three rails. A token issuer might use LayerZero OFT for EVM chains, Wormhole NTT for Solana, and Hyperlane Warp Routes for new appchains. Each route picks the rail that fits.
How to Design a Message Format
Every cross-chain message has a payload — a bytes blob the receiving contract parses. Three rules keep the payload safe and forward-compatible.
Use ABI encoding for typed payloads. Solidity's abi.encode and abi.decode handle versioning poorly out of the box, so include a 1-byte version field at the start of every payload. LayerZero's OApp template uses a leading uint8 message type that the receiver switches on; this lets the application add new message types without breaking 0-version receivers.
Include the source chain ID in the payload, even if the rail provides it separately. Verifying that the chain ID matches the application's expected source on the destination side prevents cross-route replay if the application ever supports multiple source chains.
Include a unique 32-byte nonce or message ID in the payload for application-level replay protection. The rail provides its own replay protection at the protocol level (LayerZero's nonce system, Wormhole's 64-bit sequence number, Hyperlane's leaf index), but applications often need replay protection at the function-call level too. A vault that processes "claim 1,000 USDC" twice still has a bug, even if the rail correctly delivered the message twice for a legitimate reason like a manual re-execution.
Handling Source Finality
The source chain's transaction must be final before any cross-chain action depends on it. "Final" means different things on different chains.
Ethereum L1. Full finality after 64-96 epochs, roughly 13 minutes. Most rails wait for this on Ethereum sources.
Ethereum rollups (Arbitrum, Optimism, Base). Fast pre-confirmation in seconds, but final finality requires the rollup's state root to be posted to L1 (Arbitrum: 1-4 hours; Optimism: 1-2 hours with the optional 7-day fraud window).
Solana. Finality after 32 confirmations, roughly 12 seconds.
Cosmos chains. Finality is single-block (Tendermint, ~6 seconds).
Polygon. Single-block finality (~2 seconds) on PoS chain, but checkpoint to Ethereum takes 30-60 minutes for some bridges.
The rail's documented "finality wait" is usually the shortest configuration. For high-value flows, applications should configure a longer wait. CCIP exposes this as a per-lane parameter; LayerZero exposes it through DVN block-confirmation settings; Hyperlane exposes it through ISM configuration.
Two failure modes to defend against. First, the rail signs before finality and the source chain reorgs — the message is delivered for a transaction that no longer exists. Second, the application acts on the message before destination finality — a separate concern, but parallel reasoning. Alchemy's finality overview covers the chain-by-chain specifics.
Fee Accounting
Every cross-chain message has at least three fee components: source-chain gas (paid in source native token), destination-chain gas (paid by an executor or relayer), and a protocol fee (paid in LINK for CCIP, in destination native gas for Wormhole, in destination native gas plus per-DVN fees for LayerZero, in source native gas for Hyperlane).
Two patterns dominate.
Estimate-and-charge: the application calls the rail's fee-quote function (quoteSend in LayerZero, getFee in CCIP, quoteDispatch in Hyperlane) before executing the user transaction, charges the user the quoted fee plus a buffer, and sends the message with the fee attached. This is simple but requires the user to pay a gas-token-denominated cost they may not understand.
Sponsored execution: a paymaster contract pays the message fee on behalf of the user, and the application charges the user in a stablecoin or in the application's native token. This is the pattern Squid Router uses for cross-chain swaps — the user sees a single all-in price quoted in the source token. Implementing it requires the application to maintain a balance of every native gas token across every supported chain, which is operationally heavy unless the application uses an orchestration layer that handles gas abstraction.
Axelar's Gas Service simplifies the second pattern at the rail level — the source-chain prepayment can be in any token Axelar accepts, and Axelar Relayers convert it to the destination's native gas. This is one of Axelar's strongest UX advantages.
Replay and Reentrancy Safety
Two attacks recur in cross-chain integration audits.
Replay attacks happen when the same message is delivered twice. Every rail has built-in replay protection — LayerZero uses a per-(source, destination, sender) nonce; Wormhole uses a sequence number per emitter; Hyperlane uses a Merkle leaf index — but the application must check it correctly. The standard pattern is to track processed message IDs in storage and revert on duplicates.
Cross-chain reentrancy happens when a destination-chain handler calls back into the messaging rail with a new outbound message before finalizing its own state changes. If the second message somehow triggers a callback that re-enters the original handler, state can be corrupted. The fix is the same as same-chain reentrancy: use the checks-effects-interactions pattern and finalize state before any external call.
The most-cited cross-chain reentrancy incident is the Nomad exploit (August 2022, $190M loss). The root cause was a missed initialization check that let any message hash be replayed on the destination, but the proximate symptom was a copy-paste exploit pattern that anyone watching the mempool could replicate. Nomad's post-mortem walks through the chain of mistakes.
Testing and Local Dev
Cross-chain code is hard to test because two real chains are involved. Three tools cover the gap.
Hardhat / Foundry forking — fork the source and destination chains in separate forked nodes and bridge messages between them with a test-only relayer.
Rail-specific local stacks — LayerZero ships a local test stack at LayerZero-Labs/devtools that simulates DVN attestations. Hyperlane provides
hyperlane core deploy --localfor local Mailbox testing. CCIP ships a Foundry starter kit.Testnet deployments — every major rail supports Sepolia, Optimism Sepolia, Arbitrum Sepolia, Base Sepolia, and several others. Most production teams test on testnet for end-to-end flows before mainnet deployment.
Production Failure Recovery
Cross-chain messages can fail at the destination — gas estimation was wrong, a downstream contract reverted, the rail had a temporary outage. Every production omnichain app needs a recovery path.
LayerZero v2 supports lzReceive retry — a failed message can be re-executed by anyone calling the destination Endpoint with the original payload. The Endpoint stores failed messages and exposes them through Scan APIs.
Wormhole's Specialized Relayer pattern lets applications register a custom relayer that handles failures with application-specific recovery logic. The Standard Relayer is best-effort; specialized relayers can implement retry queues, escalation policies, and human review.
CCIP exposes a manual execution path through Smart Execution. If the auto-executing DON fails to deliver, the user (or any watcher) can call the destination Router with the proof and force delivery. The pattern is documented in CCIP's architecture docs.
Hyperlane delivers messages through Relayers and falls back to manual process calls if the auto-relayer fails. Any address can call process with valid validator signatures and trigger delivery.
How Eco Routes Helps
Eco Routes is an orchestration layer that sits above the messaging rails. Applications integrating Routes write to a single intent interface — "send 1,000 USDC from Arbitrum to Solana, settle in 30 seconds, accept up to 8 basis points slippage" — and Routes selects between Hyperlane, LayerZero v2, Wormhole, and Circle CCTP based on cost, finality, and the security profile required. Routes handles rail-specific configuration, fee estimation, replay protection, and failure recovery.
For omnichain apps that span the 15 chains Eco supports — Ethereum, Optimism, Base, Arbitrum, HyperEVM, Plasma, Polygon, Ronin, Unichain, Ink, Celo, Solana, Sonic, BSC, Worldchain — Routes removes the per-rail integration work. Builders can start with the Routes CLI quickstart on GitHub or read the cross-chain messaging pillar for the broader architectural picture.
FAQ
What is the difference between an OApp and a contract that uses CCIP?
An OApp is a contract that inherits from LayerZero's OApp base class and uses the LayerZero v2 endpoints for cross-chain messaging. A CCIP contract uses Chainlink's CCIP Router and TokenPool contracts. Both achieve the same outcome — cross-chain function calls — but the integration interfaces are protocol-specific. Applications can implement both for redundancy.
Do I need to handle gas on every chain my app supports?
Yes, unless you use a rail or orchestration layer that abstracts gas. Without abstraction, the application's relayer or paymaster must hold a balance of every chain's native gas token. Axelar's Gas Service, CCIP's LINK-fee model, and orchestration layers like Eco Routes reduce this to a single source-token balance.
How do I prevent cross-chain replay attacks?
Track processed message IDs in destination-chain storage and revert on duplicates. The rail provides protocol-level replay protection (nonces, sequence numbers, leaf indices), but the application should add its own at the function-call level. Use a mapping from bytes32 messageId to bool processed.
Can I deploy an omnichain app on a chain my chosen rail does not support?
Not directly. The rail must be deployed on every chain the application uses. If the rail does not support a chain, options are: pick a different rail (Hyperlane is the most likely fit due to its permissionless deployment), use a multi-rail integration that falls through, or use an orchestration layer that handles rail selection.
How long does a cross-chain message take to deliver?
Source-chain finality plus rail signing time plus destination-chain inclusion. For Ethereum L1 source to L2 destination via LayerZero with two DVNs, expect 15–20 minutes (most of it Ethereum finality). For L2-to-L2 via Hyperlane multisig, expect 1–2 minutes. For Solana to EVM via Wormhole, expect 30–90 seconds. The rail's documentation gives chain-by-chain numbers.

