Skip to main content
Version: Endpoint V2

Omnichain Applications (OApps) & Design Patterns

Omnichain Applications (OApps) are LayerZero-specific contracts with custom business logic for sending and receiving information between chains. OApps use LayerZero's universal interface to implement cross-chain coordination through asynchronous messaging patterns.

Loading diagram...

Design Pattern Categories

OApp design patterns help teams decide and implement their contract business logic for cross-chain coordination:

  1. Architecture Patterns: How contracts coordinate and interact with one another across multiple chains (hub-spoke or symmetric business logic)
  2. Message Flow Patterns: How messages travel between chains (one-way push, round-trip ping-pong, batch distribution, compose workflows)
  3. Message Processing Patterns: How to control message processing (ordered delivery, rate limiting, conditional handling)
  4. Data Access Patterns: How to retrieve information from other chains (pull messaging, cross-chain queries)

LayerZero's Unopinionated Approach: LayerZero is unopinionated about your business logic. You can design messaging to be identical on every network, or implement asymmetric coordination patterns. Ultimately, LayerZero is an open framework for you to design any cross-chain interaction for any given purpose.

Key Insight: These patterns are implementation strategies for your contract business logic, not protocol features. You choose and combine patterns based on your application's specific coordination requirements.

1. Architecture Patterns

How contracts coordinate and interact with one another across multiple chains.

Hub-Spoke Architecture (Asymmetric Coordination)

One chain acts as the central "hub" with coordination logic, while other chains act as "spokes" with execution logic. The hub makes decisions, aggregates data, and distributes commands. Spokes report to the hub and execute received instructions.

Loading diagram...

Use Cases: Cross-chain governance, vault deposits (deep liquidity), oracle aggregation

Point-to-Point Architecture (Symmetric Business Logic)

All contracts have identical business logic and operate as equal peers. Each contract can initiate communication with any other contract, and all contracts handle messages the same way. No central coordinator - each contract maintains its own state while staying synchronized with peers.

Loading diagram...

Use Cases: Token transfers (OFT), peer-to-peer protocols

2. Message Flow Patterns

How messages travel between chains. Push messaging sends data from source to destination chains, with the source initiating and destination processing.

Batch Send

Send one message to multiple destination chains simultaneously:

Loading diagram...

Fee Distribution Logic: Batch send requires overriding the base OApp fee logic since you're summing multiple quotes into one payment and distributing to each send call:

// PSEUDOCODE: Batch send with fee distribution

// Override fee check from equivalency to minimum threshold
function _payNative(uint256 _nativeFee) internal override returns (uint256 nativeFee) {
if (msg.value < _nativeFee) revert NotEnoughNative(msg.value);
return _nativeFee;
}

function quoteBatch(uint32[] memory dstEids, bytes memory message) public view returns (MessagingFee memory totalFee) {
// Sum fees for all destinations
for (uint i = 0; i < dstEids.length; i++) {
MessagingFee memory fee = _quote(dstEids[i], message, options, false);
totalFee.nativeFee += fee.nativeFee;
totalFee.lzTokenFee += fee.lzTokenFee;
}
}

function sendBatch(uint32[] memory dstEids, bytes memory message) external payable {
// Validate total fee upfront
MessagingFee memory totalFee = quoteBatch(dstEids, message);
require(msg.value >= totalFee.nativeFee, "Insufficient fee");

// Distribute to each destination
for (uint i = 0; i < dstEids.length; i++) {
MessagingFee memory fee = _quote(dstEids[i], message, options, false);
_lzSend(dstEids[i], message, options, fee, payable(msg.sender));
}
}

Use Cases: Configuration updates, price feeds, state synchronization

Ping-Pong (ABA Pattern)

Chain A sends to Chain B, which calls LayerZero Endpoint within its _lzReceive business logic to send back to Chain A:

Loading diagram...

Conditional Message Handling: The _lzReceive business logic can support conditional handling based on message contents. If your application requires it, you can encode conditional identifiers in your message to determine what type of processing should occur:

// PSEUDOCODE: This pattern organizes your call logic into separate internal functions
// instead of putting all logic directly inside _lzReceive

// Define message type constants
bytes32 constant PUSH = keccak256("PUSH"); // A->B standard message
bytes32 constant PING_PONG = keccak256("PING_PONG"); // A->B->A ping-pong message

function _lzReceive(Origin calldata origin, bytes32 guid, bytes calldata message, address, bytes calldata)
internal override {
(bytes32 msgType, bytes memory businessLogic) = abi.decode(message, (bytes32, bytes));

if (msgType == PING_PONG) {
_lzReceiveAndReturn(origin, businessLogic); // Move ping pong logic to separate function
} else {
_lzReceiveOnly(businessLogic); // Move standard logic to separate function
}
}

function _lzReceiveAndReturn(Origin calldata origin, bytes memory _message) internal {
// Process incoming message (your business logic here)
processMessage(_message);

// Send response back to source chain (nested _lzSend call)
bytes memory response = abi.encode(PUSH, generateResponse(_message));
_lzSend(origin.srcEid, response, options, MessagingFee(0, 0), payable(address(this)));
}

function _lzReceiveOnly(bytes memory payload) internal {
// Standard message processing without return message (your business logic here)
processMessage(payload);
}

Critical Gas Consideration: This pattern requires off-chain gas planning. You must quote the B→A return cost off-chain and include it in your A→B execution options:

Loading diagram...

Key Insight: The Executor uses the msg.value from your lzReceiveOption to fund the B→A return message. You must calculate this cost off-chain before sending the initial A→B message.

Use Cases: Cross-chain authentication, conditional execution, data verification

Call Composer

Two-step, non-atomic process where the primary message stores a compose message for later execution:

Loading diagram...

Key Insight: The OApp calls endpoint.sendCompose() which stores a compose message tied to the LayerZero message GUID. The composer contract is called in a separate transaction, making this a non-atomic, fault-isolated process.

Use Cases: Token transfers with automated actions, multi-step DeFi operations

3. Message Processing Patterns

How to control message processing.

Ordered Delivery

OApp enforces strict sequence order by comparing protocol nonce with local nonce tracking:

Loading diagram...

Key Insight: The LayerZero Endpoint has its own nonce tracking but delivers messages unordered by default. To implement ordered delivery, the OApp must compare the protocol nonce (from message origin) with its own local nonce tracking and enforce sequence requirements.

Use Cases: Financial transactions, workflow dependencies, state machines

Rate Limiting

Control in-flight capacity per channel over time windows to prevent spam and ensure controlled interactions. Rate limiters track consumed capacity that decays over time.

LayerZero's default rate limiter implementation tracks "in-flight" capacity that decays linearly over time. Unlike simple counters that reset at fixed intervals, this approach provides smooth capacity recovery.

Loading diagram...

How It Works: When a message/token transfer occurs, the rate limiter adds the amount to "in-flight" capacity. This capacity decays linearly over the configured time window. If adding a new request would exceed the limit, the request is rejected. This provides smooth capacity recovery rather than sudden resets.

Linear Decay Visualization:

Loading diagram...

Example: With a 100-unit limit over 60 seconds, if the limit is reached at T=0, capacity decays at ~1.67 units/second. After 30 seconds, 50 units of capacity are available for new requests. Units can either be the number of individual OApp messages, or a specific value such as amount transferred per channel.

Outbound Rate Limiting (Source Chain)

Rate check occurs before sending cross-chain message. Clean failure mode with no partial states.

Loading diagram...

info

Transaction fails immediately on source chain - user retains funds, no cross-chain state changes.

Inbound Rate Limiting (Destination Chain)

Rate check occurs after cross-chain message arrives. Can create partial states requiring retry handling.

Loading diagram...

caution

Transaction succeeds on source but fails on destination - creates partial state where funds may be stuck in-flight. Applications must implement retry UX patterns.

Rate Limiter Configuration

Definition: Per channel configuration with a limit (number of messages or token value) over a time window. Capacity decays linearly over the window duration, allowing gradual recovery.

Examples:

  • Message limiting: 10 messages per 60 seconds per channel
  • Token limiting: 1000 USDC per 24 hours per channel
  • Combined limiting: Both message count and token amount restrictions per channel

Use Cases: Spam prevention, regulatory compliance, treasury protection, system stability

4. Data Access Patterns

How to retrieve information from other chains. Pull messaging requests data from other chains and returns responses to the requesting chain.

Data Queries (lzRead)

Request and retrieve state data from contracts on other chains:

Loading diagram...

Use Cases: Price feeds, state verification, cross-chain calculations

For comprehensive pull messaging patterns and implementation details, see Omnichain Queries (lzRead).

Exit Criteria

Before proceeding to Module 5, you should be able to:

  1. Explain what makes an app "omnichain" vs "multi-chain"
  2. Identify which pattern fits your use case
  3. Design a message schema for your application
  4. Implement idempotent message handling

Further Reading

Official Documentation

Code References