OApp.sol implements the core interface for calling LayerZero’s Endpoint V2 on EVM chains. It also provides hookable _lzSend and _lzReceive methods so you can inject your own business logic:
Installation
To start using LayerZero contracts in a new project, use the LayerZero CLI tool, create-lz-oapp. The CLI tool is an npx package that allows developers to create any omnichain application in <4 minutes! Get started by running the following from your command line:- npm
- yarn
- pnpm
- forge
LayerZero contracts work with both OpenZeppelin V5 and V4 contracts. Specify your desired version in your project’s package.json:
Custom OApp Contract
To build your own crosschain application, inherit fromOApp.sol and implement two key pieces:
- Send business logic: how you encode and dispatch a custom
_messageon the source - Receive business logic: how you decode and apply an incoming
_messageon the destination
- A constructor wiring in the local Endpoint and owner
- A
sendString(...)function that updates state, encodes a string, and calls_lzSend(...) - An override of
_lzReceive(...)that decodes the string and applies business logic - (Optional) a
quoteSendString(...)function to query the fee details needed to callsendString(...)
Constructor
- Pass the Endpoint V2 address and owner address into the base contracts.
OApp(_endpoint, _owner)binds your contract to the local LayerZero Endpoint V2 and registers the owner as the delegate, making it the only address that can change configurations (such as libraries, DVNs, and Executors.Ownable(_owner)makes_ownerthe only address that can change configurations (such as peers, enforced options, and delegate).
- After deployment, the owner can call:
setConfig(...)to adjust library or DVN parameterssetSendLibrary(...)andsetReceiveLibrary(...)to override default librariessetPeer(...)to whitelist remote OApp addressessetDelegate(...)to assign a different delegate address
A full overview of how to use these adminstrative functions can be found below under Deployment & Wiring.
sendString(…)
-
Update local state (optional)
- Before sending, you might update a counter, lock tokens, or perform any onchain action specific to your app.
-
Encode the message
- Use
abi.encode(_message),abi.encodePacked(_message), or manual byte shifting/offsets to turn the string into abytesarray. LayerZero packets carry rawbytes, so you must encode any data type into bytes first.
- Use
-
Call
_lzSend(...)_dstEidis the destination chain’s Endpoint ID. LayerZero uses numeric IDs (e.g.,30101for Ethereum,30168for Solana)._messageis the ABI-encoded string (bytes memory)._optionsis abytesarray specifying gas or executor instructions for the destination. For example, anExecutorLzReceiveOptiontells the destination how much gas to allocate to your receive call.MessagingFee(msg.value, 0)pays fees in native gas. If you wanted to pay in ZRO tokens, set the second field instead.payable(msg.sender)specifies the refund address for any unused gas. This can be any address (EOA or contract), but if it’s a contract, the contract must have a fallback function to receive the refund.
_lzReceive(…)
-
Endpoint verification
- Only the LayerZero Endpoint V2 contract can invoke this function. The base
OAppReceiverenforces that. - The call succeeds only if
_origin.sender == peers[_origin.srcEid]. In other words, the sender’s address must match the registered peer for that source chain.
- Only the LayerZero Endpoint V2 contract can invoke this function. The base
-
Decode the incoming bytes
- Use
abi.decode(_message, (string))to extract the original string. If you sent a different data type (e.g., a struct), decode with the matching types. - Alternatively, you can use
abi.decodePacked()for packed encoding, or manually splice bytes from specific offsets if you know the exact format of your data structures.
- Use
-
Apply your business logic
- In this example, we store the decoded string in
lastMessage. - You could instead:
- Emit an event (e.g.,
emit MessageReceived(_origin.srcEid, decoded)) - Mint or unlock tokens based on the message
- Call another contract to trigger a downstream workflow
- Emit an event (e.g.,
- In this example, we store the decoded string in
(Optional) quoteSendString(…)
You can optionally call the internalOAppSender._quote(...) method in a public function to provide accurate estimation for the gas cost of calling MyOApp.sendString(...).
The internal _quote method queries the send library selected by the OApp and asks the workers (DVNs and Executor) for fee details for the given encoded message:
-
Fee estimation before sending
- Before calling
sendString(...), you need to know how much native gas (or ZRO tokens) to send with your transaction. ThequoteSendString(...)function provides this cost estimate.
- Before calling
-
Mirrors send logic
- The quote function uses the same message encoding (
abi.encode(_string)) and option handling (combineOptions(_dstEid, SEND, _options)) as the actual send function, ensuring accurate fee estimates.
- The quote function uses the same message encoding (
-
Enforced options integration
- By inheriting
OAppOptionsType3and usingcombineOptions(...), the quote function automatically includes any enforced options that the contract owner has configured for theSENDmessage type, plus any additional options provided by the caller.
- By inheriting
-
Flexible payment options
-
The
_payInLzTokenparameter lets you choose whether to pay fees in the native gas token of the source chain or in ZRO tokens. Example usage:
-
The
This section shows you exactly:
- Where to update or check local state before sending
- How to encode and send your application data over LayerZero
- Where to decode incoming data and execute your custom logic
string examples with whatever data structures and state changes your application requires.
Deployment and Wiring
After you finish writing and testing yourMyOApp contract, follow these steps to deploy it on each network and wire up the messaging stack.
1. Deploy Your OApp Contract
DeployMyOApp on each chain using either the LayerZero CLI (recommended) or manual deployment scripts.
- LayerZero CLI
- Manual Foundry
After running The LayerZero CLI provides automated deployment with built-in endpoint detection based on your The CLI will prompt you to:The CLI automatically:
pnpm compile at the root level of your example repo, you can deploy your contracts.Network Configuration
Before using the CLI, you’ll need to configure your networks inhardhat.config.ts with LayerZero Endpoint IDs and declare an RPC URL in your .env or directly in the config file:The key addition to a standard
hardhat.config.ts is the inclusion of LayerZero Endpoint IDs (eid) for each network. Check the Deployments section for all available endpoint IDs.hardhat.config.ts networks object:- Select chains to deploy to:
- Choose deploy script tags:
- Confirm deployment:
- Detects the correct LayerZero Endpoint V2 address for each chain
- Deploys your OApp contract with proper constructor arguments
- Generates deployment artifacts in
./deployments/folder - Creates network-specific deployment files (e.g.,
deployments/sepolia/MyOApp.json)
2. Wire Messaging Libraries and Configurations
Once your contracts are onchain, you must set up send/receive libraries and DVN/Executor settings so crosschain messages flow correctly.- LayerZero CLI
- Manual Foundry
The LayerZero CLI automatically handles all wiring via a single configuration file and command:Make sure your contract object’s This automatically handles:
Configuration File
In your project root, you can find alayerzero.config.ts file:contractName matches the named deployment file for the network under ./deployments/.Wire Everything
Run a single command to configure all pathways:- Fetching the necessary contract addresses for each network from metadata
- Setting send and receive libraries
- Configuring DVNs and Executors
- Setting up peers between contracts
- Applying enforced options
- All bidirectional pathways in your config
Usage
Once deployed and wired, you can begin sending crosschain messages.Calling send
- LayerZero CLI
- Manual Foundry
The LayerZero CLI provides a convenient task for sending messages that automatically handles fee estimation and transaction execution.Parameters:The task automatically:
Using the Send Task
The CLI includes a built-inlz:oapp:send task that:- Quotes the gas cost using your OApp’s
quoteSendString()function - Sends the message with the correct fee
- Waits for confirmation and provides tracking links
--dst-eid: Destination endpoint ID (required)--string: Message to send (required)--network: Source network name from your hardhat config (required)--options: Execution options in hex format (optional, defaults to0x)
- Finds your deployed
MyOAppcontract - Quotes the exact gas fee needed
- Sends the transaction with proper gas estimation
- Provides block explorer and LayerZero Scan links for tracking
Extensions
The OApp Standard can be extended with various messaging patterns to support complex crosschain applications. Each pattern functions as a distinct omnichain building block, capable of being used independently or in combination.ABA (Ping-Pong) Pattern
The ABA pattern enables nested messaging where a message sent from Chain A to Chain B triggers another message back to Chain A (A → B → A). This is useful for crosschain authentication, data feeds, or conditional contract execution.
Implementation
The key is to nest an_lzSend call within your _lzReceive function:
Batch Send
Batch Send allows a single transaction to initiate multiple_lzSend calls to various destination chains, reducing operational overhead for multi-chain operations.
Key Implementation Points
The batch send pattern includes several important design decisions:- Fee Validation: Override
_payNativeto change fee check from equivalency to<since batch fees are cumulative - Consistent Loop Pattern: Both
quoteandsendfunctions use identical for loops to iterate through destinations for predictable behavior
Implementation
Call Composer
Composed messaging enables horizontal composability where a message triggers external contract calls on the destination chain throughlzCompose. Unlike vertical composability (multiple calls in a single transaction), horizontal composability processes operations as separate, containerized message packets.
Benefits of Horizontal Composability
- Fault Isolation: If a composed call fails, it doesn’t revert the main token transfer or message
- Gas Efficiency: Each step can have independent gas limits and execution options
- Flexible Workflows: Complex multi-step operations can be broken into manageable pieces
Sending Side
Receiving Side
Composer Contract
Message Ordering
LayerZero supports both unordered (default) and ordered delivery patterns.Ordered Delivery Implementation
Important Nonce Management Considerations
When implementing ordered delivery, be aware of these critical nonce synchronization issues:-
Nonce Validation: The
_acceptNoncefunction must be called in_lzReceiveto verify the incoming nonce matches the expected sequence before processing any message. -
Protocol vs Local Nonce Mismatch: Functions like
skip(),burn(), andclear()advance the protocol’s nonce but do not automatically update your OApp’s local nonce mapping. This creates a dangerous mismatch where:- Protocol nonce: 15 (after skipping message 15)
- OApp mapping: 14 (still expecting message 15)
- Result: All future messages will be rejected
-
Solution: If your OApp needs to use
skip(),burn(), orclear(), you must manually increment your local nonce to stay synchronized:
Rate Limiting
Control message frequency to prevent spam and ensure controlled crosschain interactions:Further Reading
For detailed implementations and advanced patterns, see:- Message Execution Options - Options configuration
- OApp Technical Reference - Deep dive into OApp mechanics
- Integration Checklist - Security considerations and best practices