What is an OApp on Stellar?
An Omnichain Application (OApp) on Stellar is a Soroban smart contract that can send and receive crosschain messages via the LayerZero protocol. OApps serve as the base for all LayerZero integrations on Stellar, including OFTs.Differences from EVM OApps
Build a Minimal OApp
Follow these steps to set up your project, configure dependencies, and implement a minimal OApp contract.Prerequisites
- Stellar CLI (v25.1.0+)
- Rust (v1.90.0+) with
wasm32v1-nonetarget (rustup target add wasm32v1-none) - Familiarity with Soroban smart contracts and Rust on Soroban
Step 1: Set Up Your Project
Create a new Soroban project:Step 2: Configure Dependencies
Add LayerZero dependencies to yourCargo.toml:
Check the OApp contracts, protocol contracts on LayerZero GitHub for the latest Stellar contract packages.
Step 3: Project Structure
Organize your contract:Step 4: Implement the Contract
Here’s a minimal OApp that sends and receives crosschain messages:OApp Components
The#[oapp] macro generates implementations for these traits:
You must always implement:
LzReceiveInternal — this is your custom receive logic.
To provide a custom implementation for any generated trait, use:
How OApp Messaging Works
Peer Configuration
Before sending or receiving messages, configure the trusted peer address for each remote chain: Theset_peer function requires the contract Owner.
Message Flow
Send Flow
Receive Flow
The OApp is the entry point, not the Endpoint. This avoids reentrancy issues and supports ABA messaging patterns.Sending Messages
Sending a crosschain message involves four steps:Step 1: Encode Your Message
Structure your message payload. LayerZero transports raw bytes — how you encode them depends on your application:Step 2: Build Execution Options
Options specify how the message should be executed on the destination chain (gas limit, native value, etc.):Step 3: Quote the Fee
Step 4: Send the Message
FeePayer enum tracks whether the payer has already been authorized, preventing duplicate require_auth() calls.
Receiving Messages
When a message arrives, thelz_receive flow (provided by the OAppReceiver trait’s default implementation) handles validation and routing:
- Executor authenticates:
executor.require_auth() - Peer validation: Asserts
origin.sendermatches the configured peer fororigin.src_eid - Value forwarding: Transfers native token from executor to OApp if value != 0
- Payload clearing: Calls
endpoint.clear()to mark the message as delivered - Your logic: Calls
__lz_receive()with the decoded message
Message Inspection
Optionally, set an external inspector contract to validate outbound messages before they’re sent:IOAppMsgInspector trait:
false or by panicking. Note: the OFT standard ignores the return value and relies on panics for rejection — if building a custom OApp, you can check the boolean return in your send logic.
The
set_msg_inspector function is available on OFT contracts. If building a custom OApp, you can implement message inspection in your send logic directly.Deployment
Step 1: Build the Contract
target/wasm32v1-none/release/my_oapp.wasm.
Step 2: Deploy to Testnet
--. The contract is deployed and initialized atomically.
Step 3: Configure Peers
After deployment, set the peer addresses for each remote chain:Step 4: Change Delegate (Optional)
The delegate is set during deployment via the constructor’sdelegate parameter. The delegate is the address authorized to call endpoint configuration functions (set_config, set_send_library, set_receive_library) on behalf of your OApp. To change the delegate after deployment:
Step 5: Set Message Libraries (Optional)
Configure custom send and receive libraries if you don’t want the default. Thenew_lib parameter is Option<Address> — pass None to reset to the default library:
Step 6: Set Enforced Options (Optional)
Optionally, configure enforced execution options for each destination:Core Methods
These are the primary functions your OApp will use to send and receive crosschain messages.__quote()
Estimates the fee required to send a crosschain message without actually sending it. Returns aMessagingFee containing the native and ZRO fee breakdown.
When to use: Before sending to determine the required fee, or to display estimated costs to users.
__lz_send()
Sends a crosschain message to a destination chain. Internally transfers the fee to the Endpoint, looks up the peer for the destination, and calls the Endpoint’ssend function.
Key parameters:
dst_eid: Destination chain endpoint IDmessage: Your encoded payload (raw bytes)options: Execution parameters (gas limits, native value)fee_payer:FeePayer::Unverified(addr)orFeePayer::Verified(addr)fee: TheMessagingFeefrom__quote()refund_address: Where to send excess fees
FeePayer::Unverified(addr)— Safe default.__lz_sendwill calladdr.require_auth().FeePayer::Verified(addr)— Use when the caller has already calledrequire_auth()to avoid duplicate authorization in the Soroban auth tree.
lz_receive()
Processes incoming messages delivered by the Executor. The default implementation (provided byOAppReceiver) performs validation, payload clearing, and delegates to your __lz_receive() implementation.
Your implementation: Implement LzReceiveInternal with your custom business logic. The base validation (executor auth, peer check, payload clearing) is handled before your code runs.
Message Encoding
Your OApp is responsible for encoding and decoding message payloads. LayerZero transports raw bytes — how you structure them depends on your application. Key principles:- Use consistent byte order (big-endian recommended for cross-VM compatibility with EVM)
- Use fixed-width fields where possible for deterministic parsing
- Test encoding/decoding on both source and destination chains
Configuration
Configuration functions have different authorization requirements:Events
Configuring Remote Chains to Send to Stellar
When configuring OApps on other chains (e.g., EVM, Solana) to send messages to Stellar, follow standard LayerZero configuration with these Stellar-specific details:1. Use Payload as Peer
Stellar addresses use StrKey encoding: 1-byte version + 32-byte payload + 2-byte CRC16 checksum. LayerZero uses the 32-byte payload (contract ID hash) as the bytes32 peer address.2. Set Enforced Options for Stellar Destination
Configure minimum execution options for messages sent to Stellar:3. Standard DVN Configuration
DVN configuration on remote chains follows standard LayerZero patterns — no Stellar-specific changes needed. See the platform-specific implementation guides for your source chain’s configuration.Best Practices
Configure Security Before Setting Peers
Configure DVNs, Executor, and enforced options before setting peers. Setting a peer opens the messaging pathway for that remote chain.Use FeePayer::Verified When Appropriate
If your OApp’ssend function already calls require_auth() on the caller, pass FeePayer::Verified(caller) to __lz_send() to avoid a duplicate authorization node in Soroban’s auth tree.
Monitor TTL for Persistent Storage
Peer mappings are stored in persistent storage with TTL. LayerZero contracts automatically extend TTL when entries are accessed, but low-activity OApps should monitor and extend TTL for critical state to prevent archival.Test Bidirectional Messaging
Always test both sending and receiving on testnet before deploying to mainnet. Verify that messages are correctly encoded on the source chain and decoded on the destination chain.Security Considerations
Critical Validations
- Peer verification: Every inbound message is checked against the configured peer. Only messages from the registered peer address for a given source chain are accepted.
- Payload hash verification: The
endpoint.clear()operation verifies the message against the stored payload hash, ensuring the delivered message matches what was verified by DVNs. If your contract uses the#[oapp]macro, this is already handled for you.
Common Pitfalls
- Forgetting to set peers on both sides of the connection
- Not setting enforced options, resulting in failed execution on the destination chain
- Using
FeePayer::Unverifiedwhen the caller is already authorized, causing auth tree issues - Not monitoring TTL for low-activity contracts, leading to archived storage entries
Next Steps
- Build an OFT: Deploy a token with built-in crosschain transfer.
- DVN & Executor Config: Configure your security stack.
- Technical Overview: Understand the full message lifecycle.
- Troubleshooting: Common errors and solutions.