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
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
PoaManagerSatellitecurrently 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)ILayerZeroEndpoint.send().upgradeBeaconCrossChainLZ()addContractTypeCrossChainLZ()adminCallCrossChainLZ()Bridgeenum parameter (Hyperlane | LayerZero) so the caller picks which bridge to use at call time.Satellite side (
PoaManagerSatellite)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_processMessage(bytes)to decode and execute.mailboximmutable becomes two immutables:hyperlaneMailboxandlzEndpoint. Similarly, hub identity needs to be stored for both bridges (hubDomain/hubAddressfor Hyperlane,hubLzChainId/hubLzAddressfor LayerZero).Interface abstraction
Extract a minimal
IBridgeinterface so future bridges (Wormhole, Axelar, etc.) can be added without restructuring:Hub stores a mapping of
bridgeId => IBridgeand satellite stores a mapping ofbridgeId => 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
Files to modify
src/crosschain/PoaManagerHub.sol— add LZ dispatch, bridge abstractionsrc/crosschain/PoaManagerSatellite.sol— add LZ receive, shared_processMessageinternalsrc/crosschain/interfaces/IHyperlane.sol— keep as-is, addILayerZero.solalongsidesrc/crosschain/interfaces/ILayerZero.sol— minimal LZ endpoint interfacetest/crosschain/— full coverage for LZ path + both-bridges-down scenariosNon-goals
🤖 Generated with Claude Code