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.
info
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.
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
send(...)
EntrypointDeveloper-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(...)
.
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.
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]
.
Invoke
lzReceive(...)
If validation succeeds, the destination Endpoint calls your OApp’s public
lzReceive(origin, guid, message, executor, extraData)
.
3. lzReceive(...)
Entrypoint
lzReceive(...)
EntrypointAccess control and peer check
Only the Endpoint may call
lzReceive
.Immediately validate that
_origin.sender == peers[_origin.srcEid]
.
Internal
_lzReceive(...)
logicDecode 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
mappingStore 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.
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.
Updating peers
If you redeploy or upgrade an OApp, call
setPeer
on both old and new deployments to maintain continuity.
Further Reading
Last updated