Skip to content

Add LayerZero as fallback bridge alongside Hyperlane #130

@hudsonhrh

Description

@hudsonhrh

Summary

Add LayerZero as a second cross-chain messaging option so satellite chains aren't dependent on a single bridge. Either bridge working is sufficient — no multi-bridge consensus needed. This is a fallback/redundancy pattern, not an M-of-N security model.

Motivation

PoaManagerSatellite currently hardcodes Hyperlane as the sole messaging path from the home chain. Since satellite chains have no local owner (governance flows exclusively from the home chain org), a Hyperlane outage would make satellites permanently ungovernable — no upgrades, no admin calls, no new contract types.

Design

Hub side (PoaManagerHub)

  • Add a second dispatch path using LayerZero's ILayerZeroEndpoint.send().
  • New functions mirror existing ones:
    • upgradeBeaconCrossChainLZ()
    • addContractTypeCrossChainLZ()
    • adminCallCrossChainLZ()
  • Alternatively, a single set of cross-chain functions that accept a Bridge enum parameter (Hyperlane | LayerZero) so the caller picks which bridge to use at call time.
  • Satellite registration should track both Hyperlane domain IDs and LayerZero endpoint IDs per chain.

Satellite side (PoaManagerSatellite)

  • Accept messages from either bridge — two handle-style entry points:
    • handle(uint32 origin, bytes32 sender, bytes calldata body) — Hyperlane (existing)
    • lzReceive(uint16 srcChainId, bytes calldata srcAddress, uint64 nonce, bytes calldata payload) — LayerZero
  • Both entry points validate the source (hub chain + hub address via their respective bridge's conventions) and then call the same internal _processMessage(bytes) to decode and execute.
  • The mailbox immutable becomes two immutables: hyperlaneMailbox and lzEndpoint. Similarly, hub identity needs to be stored for both bridges (hubDomain/hubAddress for Hyperlane, hubLzChainId/hubLzAddress for LayerZero).

Interface abstraction

Extract a minimal IBridge interface so future bridges (Wormhole, Axelar, etc.) can be added without restructuring:

interface IBridge {
    function dispatch(uint32 destChain, bytes32 recipient, bytes calldata payload) external payable;
}

Hub stores a mapping of bridgeId => IBridge and satellite stores a mapping of bridgeId => authorized sender config. This keeps the pattern extensible without rearchitecting each time.

Message format

No changes needed — the existing (uint8 msgType, ...) encoding is bridge-agnostic. Both Hyperlane and LayerZero just relay opaque bytes.

Key constraints

  • No consensus required: a message from either bridge is sufficient. This is a 1-of-N trust model — we trust each bridge independently.
  • Same validation per bridge: each bridge path validates origin chain + sender address using that bridge's native mechanisms.
  • Owner stays renounced on satellites: the entire point is that governance only flows from the home chain. The fallback is a second bridge, not a local key.

Files to modify

  • src/crosschain/PoaManagerHub.sol — add LZ dispatch, bridge abstraction
  • src/crosschain/PoaManagerSatellite.sol — add LZ receive, shared _processMessage internal
  • src/crosschain/interfaces/IHyperlane.sol — keep as-is, add ILayerZero.sol alongside
  • New: src/crosschain/interfaces/ILayerZero.sol — minimal LZ endpoint interface
  • Tests: test/crosschain/ — full coverage for LZ path + both-bridges-down scenarios

Non-goals

  • M-of-N bridge consensus
  • Automatic failover detection (caller picks which bridge to use)
  • Blocking mainnet launch — this is a post-launch hardening improvement

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions