OApp Technical Reference
LayerZero’s Omnichain Application (OApp) standard defines a common set of patterns and interfaces for any smart contract that needs to send and receive messages across multiple blockchains. By inheriting OApp’s core functionality, higher-level primitives (such as OFT, ONFT, or any custom cross-chain logic) can rely on a unified, secure messaging layer.
All OApp implementations must handle:
- Message sending: Encode and dispatch outbound messages
- Message receiving: Decode and process inbound messages
- Fee handling: Quote, collect, and refund native & ZRO fees
- Peer management: Maintain trusted mappings between chains
- Channel management and security: Control security and execution settings between chains
Deployment
Every OApp needs to be deployed on each chain where it will operate. Initialization involves two steps:
1. Integrate with the local Endpoint
- Pass the local Endpoint V2 address into your constructor or initializer.
- The Endpoint’s delegate authority is set to your OApp and the address initializing unless overridden.
- As a delegate, your OApp can call any
endpoint.*
security method (setSendLibrary
,setConfig
, etc.) in a secure, authorized manner.
2. Configure peers (directional peering)
- On each chain, the owner calls
setPeer(eid, peerAddress)
to register the remote OApp’s address for a given Endpoint ID. - Repeat on the destination chain: register the source chain’s OApp address under its Endpoint ID.
- Because trust is directional, the receiving OApp checks
peers[srcEid] == origin.sender
before processing inbound messages.
For guidelines on channel security, see Message Channel Security. For an example implementation, see the OFT Technical Reference.
Core Message Flow
OApps follow a three-step life cycle. Developers focus on local state changes and message encoding; LayerZero handles secure routing and final delivery.
Phase | Actors | Responsibility |
---|---|---|
1. send(...) | OApp | Perform local state change and encode the message |
3. Transport | LayerZero, DVNs, & Executors | Build, verify, and route the packet to the destination chain |
4. lzReceive(...) | OApp | Validate origin, decode message, apply state change |
1. send(...)
Entrypoint
- Developer-defined logic
- Perform a local state change (e.g., burn or lock tokens, record intent).
- Encode all necessary data (addresses, amounts, or arbitrary instructions) into a byte array.
- Optionally accept execution options (gas limits, native gas transfers, or LayerZero Executor services).
- Key points
- Your public
send(...)
handles only local logic and message construction. - All packet assembly, peer lookup, and fee handling occur inside the internal call to
endpoint.send(...)
.
- Your public
2. Transport and Routing
- Fee payment and validation
- Ensure the caller has supplied exactly the required native or ZRO fee.
- When
endpoint.send(...)
executes, the Endpoint verifies that the fees match the quote from the chosen messaging library. Underpayment causes a revert.
- Packet construction and dispatch
- The Endpoint computes the next outbound nonce for
(sender, dstEid, receiver)
and builds aPacket
struct withnonce
,srcEid
,sender
,dstEid
,receiver
,GUID
, and the rawmessage
. - It looks up which send library to use, either a per-OApp override or a default, for
(sender, dstEid)
. - The send library serializes the
Packet
into anencodedPacket
and returns aMessagingFee
struct. - The Endpoint emits a
PacketSent(...)
event so DVNs and Executors know which packet to process.
- The Endpoint computes the next outbound nonce for
- DVNs & Executors
- Paid DVNs pick up the packet, verify its integrity, and relay it to the destination chain’s Endpoint V2.
- The destination library enforces DVN verification and block-confirmation requirements based on your receive config.
- Destination Endpoint validation
- Verify that the packet’s
srcEid
has a registered peer. - Confirm that
origin.sender
matchespeers[srcEid]
.
- Verify that the packet’s
- Invoke
lzReceive(...)
- If validation succeeds, the destination Endpoint calls your OApp’s public
lzReceive(origin, guid, message, executor, extraData)
.
- If validation succeeds, the destination Endpoint calls your OApp’s public
3. lzReceive(...)
Entrypoint
- Access control and peer check
- Only the Endpoint may call
lzReceive
. - Immediately validate that
_origin.sender == peers[_origin.srcEid]
.
- Only the Endpoint may call
- Internal
_lzReceive(...)
logic- Decode the byte array into original data types (addresses, amounts, or instructions).
- Execute the intended on-chain business logic (e.g., mint tokens, unlock collateral, update balances).
- If there’s a composable hook, your OApp can invoke
sendCompose(...)
to bundle further cross-chain calls.
- Outcome
- Upon completion, the destination chain’s state reflects the source chain’s intent. Any post-processing (events, composable calls) occurs here.
This clear separation between local state updates in send(...)
versus remote updates in _lzReceive(...)
lets you focus on business logic while LayerZero’s Endpoint V2 manages transport intricacies.
Security and Channel Management
Whether you’re using Solidity, Rust, or Move, these foundational patterns ensure consistent security, extensibility, and developer ergonomics.
Security and roles
- Owner
- Manages delegates, peers, and enforced gas settings
setPeer(...)
: update trust mappingssetDelegate(...)
: assign a new delegate for Endpoint configurationssetEnforcedOptions(...)
: define per-chain minimum gas for inbound execution
- Delegate
- Manages Endpoint settings and message-channel controls
setSendLibrary(oappAddress, eid, newLibrary)
: override send library for(oappAddress, eid)
.setReceiveLibrary(oappAddress, eid, newLibrary, gracePeriod)
: override receive library;gracePeriod
lets the previous library handle retries.setReceiveLibraryTimeout(oappAddress, eid, library, newTimeout)
: update how long an old receive library remains valid.setConfig(oappAddress, libraryAddress, params[])
: adjust per-library settings (DVNs, Executors, confirmations).skip(oappAddress, srcEid, srcSender, nonce)
: advance the inbound nonce without processing when verification fails.nilify(oappAddress, srcEid, srcSender, nonce, payloadHash)
: treat the payload as empty and advance the inbound nonce.burn(oappAddress, srcEid, srcSender, nonce, payloadHash)
: permanently discard a malicious or irrecoverable payload.tipUse multisigs or your preferred governance to manage Owner and Delegate roles.
Peering and Trust Management
peers
mapping- Store a mapping from
eid → bytes32 peerAddress
. Usingbytes32
lets you store addresses for various chains. setPeer(eid, peerAddress)
updates that mapping. Passingbytes32(0)
disables the pathway.
- Store a mapping from
- Directional trust
- Registering on Chain A → Chain B does not register the reverse. Each side must call
setPeer
for the other. - On receipt, enforce
peers[origin.srcEid] == origin.sender
to confirm the message is from the expected contract.
- Registering on Chain A → Chain B does not register the reverse. Each side must call
- Updating peers
- If you redeploy or upgrade an OApp, call
setPeer
on both old and new deployments to maintain continuity.
- If you redeploy or upgrade an OApp, call
Further Reading
- Message Channel Security
Deep dive into Endpoint V2's cryptographic guarantees, signature verification, and relayer/oracle incentives. - OFT Technical Reference
A concrete OApp example for fungible token transfers, illustrating how to use OApp's core patterns without platform-specific code. - Omnichain Composability
Patterns for building advanced cross-chain primitives (AMM routers, multi-chain staking, governance) on top of OApp's hooks and the Executor model.