> ## Documentation Index
> Fetch the complete documentation index at: https://docs.layerzero.network/llms.txt
> Use this file to discover all available pages before exploring further.

# OApp on Stellar

> Build an Omnichain Application (OApp) on Stellar using Soroban smart contracts. Learn the contract structure, deployment, peer configuration, and crosschain messaging.

The OApp Standard provides developers with a generic message passing interface to send and receive arbitrary pieces of data between contracts existing on different blockchain networks.

How the data is interpreted and what actions it triggers depend on the specific OApp implementation.

## 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

| Aspect               | EVM (Solidity)                                                    | Stellar (Soroban)                                                           |
| -------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------- |
| **Contract pattern** | Inherit from `OApp.sol`                                           | Use `#[lz_contract]` + `#[oapp]` proc macros on struct                      |
| **Authorization**    | `msg.sender` checks                                               | `require_auth()` framework                                                  |
| **Send pattern**     | `_lzSend(dstEid, message, options, fee, refund)`                  | `__lz_send(env, dst_eid, message, options, fee_payer, fee, refund_address)` |
| **Receive pattern**  | Override `_lzReceive(origin, guid, message, executor, extraData)` | Implement `LzReceiveInternal` trait                                         |

## Build a Minimal OApp

Follow these steps to set up your project, configure dependencies, and implement a minimal OApp contract.

<Tip>
  For a complete working example, see the [Counter OApp](https://github.com/LayerZero-Labs/monorepo-external/tree/main/apps/project-types/omni-counter-app/contracts/stellar) in the LayerZero monorepo.
</Tip>

### Prerequisites

* [Stellar CLI](https://developers.stellar.org/docs/tools/developer-tools/cli/stellar-cli) (v25.1.0+)
* [Rust](https://www.rust-lang.org/tools/install) (v1.90.0+) with `wasm32v1-none` target (`rustup target add wasm32v1-none`)
* Familiarity with [Soroban smart contracts](https://developers.stellar.org/docs/learn/smart-contract-internals) and [Rust on Soroban](https://developers.stellar.org/docs/build/guides)

### Step 1: Set Up Your Project

Create a new Soroban project:

```bash wrap theme={null}
stellar contract init my-oapp
cd my-oapp
```

### Step 2: Configure Dependencies

Add LayerZero dependencies to your `Cargo.toml`:

```toml wrap theme={null}
[package]
name = "my-oapp"
version = "0.1.0"
edition = "2021"

[dependencies]
soroban-sdk = "25.1.1"
# LayerZero OApp dependencies
# oapp = { ... }
# oapp-macros = { ... }
# endpoint-v2 = { ... }
# common-macros = { ... }
# utils = { ... }

[dev-dependencies]
soroban-sdk = { version = "25.1.1", features = ["testutils"] }

[lib]
crate-type = ["cdylib"]
```

<Info>
  Check the [OApp contracts](https://github.com/LayerZero-Labs/monorepo-external/tree/main/apps/oapp-app/contracts/stellar), [protocol contracts](https://github.com/LayerZero-Labs/monorepo-external/tree/main/contracts/protocol/stellar/contracts) on LayerZero GitHub for the latest Stellar contract packages.
</Info>

### Step 3: Project Structure

Organize your contract:

```
my-oapp/
├── Cargo.toml
├── src/
│   └── lib.rs          # Contract implementation
└── rust-toolchain.toml  # Pin to compatible Rust version
```

### Step 4: Implement the Contract

Here's a minimal OApp that sends and receives crosschain messages:

```rust wrap theme={null}
#![no_std]
use soroban_sdk::{Address, Bytes, BytesN, Env};
use common_macros::{contract_impl, lz_contract};
use endpoint_v2::{MessagingFee, Origin};
use oapp::oapp_core::init_ownable_oapp;
use oapp::oapp_receiver::LzReceiveInternal;
use oapp::oapp_sender::{FeePayer, OAppSenderInternal};
use oapp_macros::oapp;

// The #[lz_contract] macro generates contract, ownable, TTL traits
// The #[oapp] macro generates OApp trait implementations
#[lz_contract]
#[oapp]
pub struct MyOApp;

// Constructor
#[contract_impl]
impl MyOApp {
    pub fn __constructor(
        env: &Env,
        owner: &Address,
        endpoint: &Address,
        delegate: &Address,
    ) {
        init_ownable_oapp::<Self>(env, owner, endpoint, delegate);
    }

    /// Estimate the messaging fee for sending a crosschain message.
    pub fn quote(env: &Env, dst_eid: u32, options: &Bytes, pay_in_zro: bool) -> MessagingFee {
        // Encode the outbound message payload before estimating the messaging fee.
        // let message = msg_codec::encode();
        Self::__quote(env, dst_eid, &message, options, pay_in_zro)
    }

    /// Send a crosschain message to the destination chain.
    pub fn send(env: &Env, caller: &Address, dst_eid: u32, options: &Bytes, fee: &MessagingFee) {
        caller.require_auth();

        // Encode the outbound message payload before sending the cross-chain message.
        // let message = msg_codec::encode();

        // Send the message — caller already authorized via require_auth() above
        Self::__lz_send(env, dst_eid, &message, options, &FeePayer::Verified(caller.clone()), fee, caller);
    }
}

// You must implement LzReceiveInternal for custom receive logic
impl LzReceiveInternal for MyOApp {
    fn __lz_receive(
        env: &Env,
        origin: &Origin,
        guid: &BytesN<32>,
        message: &Bytes,
        extra_data: &Bytes,
        executor: &Address,
        value: i128,
    ) {
        // Decode and process the incoming message
        // Example: store the message or trigger an action
    }
}
```

## OApp Components

The `#[oapp]` macro generates implementations for these traits:

| Trait                | Purpose                  | Key Functions                                                                        |
| -------------------- | ------------------------ | ------------------------------------------------------------------------------------ |
| `OAppCore`           | Base OApp functionality  | `endpoint()`, `peer()`, `set_peer()`, `set_delegate()`                               |
| `OAppSenderInternal` | Internal send helpers    | `__quote()`, `__lz_send()`                                                           |
| `OAppReceiver`       | Receive message handling | `lz_receive()`, `allow_initialize_path()`, `next_nonce()`, `is_compose_msg_sender()` |
| `OAppOptionsType3`   | Execution options        | `enforced_options()`, `set_enforced_options()`, `combine_options()`                  |

**You must always implement:** `LzReceiveInternal` -- this is your custom receive logic.

To provide a custom implementation for any generated trait, use:

```rust wrap theme={null}
#[lz_contract]
#[oapp(custom = [receiver, options_type3])]
pub struct MyCustomOApp;
// Now you must implement OAppReceiver and OAppOptionsType3 yourself
```

## How OApp Messaging Works

### Peer Configuration

Before sending or receiving messages, configure the trusted peer address for each remote chain:

The `set_peer` function requires the contract `Owner`.

```rust wrap theme={null}
// Set the peer for a remote endpoint ID (owner only)
oapp.set_peer(&dst_eid, &Some(remote_oapp_bytes32), &caller);
```

Peers are stored in **persistent storage** (keyed by endpoint ID) and validated on every inbound message.

<Warning>
  You must set peers on **both sides** of the connection. If chain A's OApp sets chain B as a peer, chain B's OApp must also set chain A as a peer. Messages from unregistered peers are rejected.
</Warning>

### Message Flow

#### Send Flow

```
┌─────────────┐
│   OApp      │ 1. User calls send()
│ (Your App)  │
└──────┬──────┘
       │ 2. Transfer fee, build message
       ▼
┌─────────────┐
│  Endpoint   │ 3. Assign nonce, compute GUID
└──────┬──────┘
       │ 4. Route to send library
       ▼
┌─────────────┐
│   ULN302    │ 5. Assign jobs to workers
└──────┬──────┘
       │ 6. Calculate fees
       ▼
┌─────────────────┐
│ DVNs + Executor │ 7. Monitor and deliver on destination
└─────────────────┘
```

#### Receive Flow

The OApp is the entry point, not the Endpoint. This avoids reentrancy issues and supports ABA messaging patterns.

```
┌─────────────┐
│  Executor   │ 1. Authorizes via require_auth()
└──────┬──────┘
       │ 2. Calls OApp directly
       ▼
┌─────────────┐
│   OApp      │ 3. Validates peer, forwards value
│ (Your App)  │ 4. Calls endpoint.clear()
│             │ 5. Executes __lz_receive()
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  Endpoint   │ 6. Verifies payload hash
│             │ 7. Emits PacketDelivered
└─────────────┘
```

### 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:

```rust wrap theme={null}
// let message = msg_codec::encode();
```

#### Step 2: Build Execution Options

Options specify how the message should be executed on the destination chain (gas limit, native value, etc.):

```rust wrap theme={null}
// Type 3 options are combined: enforced options + caller-provided options
let combined = oapp.combine_options(&dst_eid, &msg_type, &extra_options);
```

#### Step 3: Quote the Fee

```rust wrap theme={null}
let fee = MyOApp::__quote(
    &env,
    dst_eid,        // Destination endpoint ID
    &message,       // Encoded message payload
    &options,       // Execution options
    false,          // pay_in_zro
);
```

#### Step 4: Send the Message

```rust wrap theme={null}
let receipt = MyOApp::__lz_send(
    &env,
    dst_eid,
    &message,
    &combined_options,
    &FeePayer::Unverified(from.clone()),
    &fee,
    &refund_address,
);
```

The `FeePayer` enum tracks whether the payer has already been authorized, preventing duplicate `require_auth()` calls.

### Receiving Messages

When a message arrives, the `lz_receive` flow (provided by the `OAppReceiver` trait's default implementation) handles validation and routing:

1. **Executor authenticates**: `executor.require_auth()`
2. **Peer validation**: Asserts `origin.sender` matches the configured peer for `origin.src_eid`
3. **Value forwarding**: Transfers native token from executor to OApp if value != 0
4. **Payload clearing**: Calls `endpoint.clear()` to mark the message as delivered
5. **Your logic**: Calls `__lz_receive()` with the decoded message

```rust wrap theme={null}
impl LzReceiveInternal for MyOApp {
    fn __lz_receive(
        env: &Env,
        origin: &Origin,       // Source chain info (src_eid, sender, nonce)
        guid: &BytesN<32>,     // Message GUID
        message: &Bytes,       // Your application payload
        extra_data: &Bytes,    // Additional data from executor
        executor: &Address,    // Executor that delivered the message
        value: i128,           // Native token value forwarded
    ) {
        // Decode your message format
        // Execute business logic
        // Emit events as needed
    }
}
```

### Message Inspection

Optionally, set an external inspector contract to validate outbound messages before they're sent:

```rust wrap theme={null}
// Set a message inspector (owner only)
oapp.set_msg_inspector(&Some(inspector_address), &operator);
```

The inspector implements the `IOAppMsgInspector` trait:

```rust wrap theme={null}
trait IOAppMsgInspector {
    fn inspect(env: &Env, oapp: &Address, message: &Bytes, options: &Bytes) -> bool;
}
```

The inspector can reject a message by returning `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.

<Note>
  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.
</Note>

## Deployment

### Step 1: Build the Contract

```bash wrap theme={null}
stellar contract build
```

This compiles your contract to WASM at `target/wasm32v1-none/release/my_oapp.wasm`.

### Step 2: Deploy to Testnet

```bash wrap theme={null}
stellar contract deploy \
  --wasm target/wasm32v1-none/release/my_oapp.wasm \
  --network testnet \
  --source my-account \
  -- \
  --owner <OWNER_ADDRESS> \
  --endpoint <ENDPOINT_ADDRESS> \
  --delegate <DELEGATE_ADDRESS>
```

The constructor arguments are passed after `--`. The contract is deployed and initialized atomically.

### Step 3: Configure Peers

After deployment, set the peer addresses for each remote chain:

```bash wrap theme={null}
stellar contract invoke \
  --id <YOUR_OAPP_ADDRESS> \
  --network testnet \
  --source my-account \
  -- \
  set_peer \
  --eid <REMOTE_EID> \
  --peer <REMOTE_PEER_BYTES32> \
  --operator <OWNER_ADDRESS>
```

<Warning>
  The Stellar CLI (v25.1.0) has a known bug where `Option<BytesN<32>>` arguments (like `--peer`) are always set to `None` regardless of the format provided. If the CLI `set_peer` command does not work, use the [Stellar JavaScript SDK](https://stellar.github.io/js-stellar-sdk/) as a workaround:

  ```javascript wrap theme={null}
  import { Contract, xdr, nativeToScVal, Address, TransactionBuilder, rpc } from '@stellar/stellar-sdk';

  const contract = new Contract(YOUR_OAPP_ADDRESS);
  // Left-pad EVM address (20 bytes) with 12 zero bytes to get 32 bytes
  const peerBytes = Buffer.from('000000000000000000000000' + evmAddress.slice(2).toLowerCase(), 'hex');
  const tx = new TransactionBuilder(account, { fee: '10000000', networkPassphrase })
    .addOperation(
      contract.call('set_peer',
        nativeToScVal(dstEid, { type: 'u32' }),
        xdr.ScVal.scvBytes(peerBytes),
        new Address(deployer.publicKey()).toScVal()
      )
    )
    .setTimeout(30).build();
  const sim = await server.simulateTransaction(tx);
  const prepared = rpc.assembleTransaction(tx, sim).build();
  prepared.sign(keypair);
  await server.sendTransaction(prepared);
  ```
</Warning>

### Step 4: Change Delegate (Optional)

The delegate is set during deployment via the constructor's `delegate` 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:

```bash wrap theme={null}
stellar contract invoke \
  --id <YOUR_OAPP_ADDRESS> \
  --network testnet \
  --source my-account \
  -- \
  set_delegate \
  --delegate <NEW_DELEGATE_ADDRESS> \
  --operator <OWNER_ADDRESS>
```

### Step 5: Set Message Libraries (Optional)

Configure custom send and receive libraries if you don't want the default. The `new_lib` parameter is `Option<Address>` — pass `None` to reset to the default library:

```bash wrap theme={null}
# Set custom send library
stellar contract invoke \
  --id <ENDPOINT_ADDRESS> \
  --network testnet \
  --source my-account \
  -- \
  set_send_library \
  --caller <DELEGATE_ADDRESS> \
  --sender <YOUR_OAPP_ADDRESS> \
  --dst_eid <DESTINATION_EID> \
  --new_lib <LIBRARY_ADDRESS>
```

```bash wrap theme={null}
# Set custom receive library
stellar contract invoke \
  --id <ENDPOINT_ADDRESS> \
  --network testnet \
  --source my-account \
  -- \
  set_receive_library \
  --caller <DELEGATE_ADDRESS> \
  --receiver <YOUR_OAPP_ADDRESS> \
  --src_eid <SOURCE_EID> \
  --new_lib <LIBRARY_ADDRESS> \
  --grace_period 0
```

### Step 6: Set Enforced Options (Optional)

Optionally, configure enforced execution options for each destination:

```bash wrap theme={null}
stellar contract invoke \
  --id <YOUR_OAPP_ADDRESS> \
  --network testnet \
  --source my-account \
  -- \
  set_enforced_options \
  --options '[{"eid": <DST_EID>, "msg_type": 1, "options": "<OPTIONS_HEX>"}]' \
  --operator <OWNER_ADDRESS>
```

See [Message Execution Options](/v2/developers/evm/configuration/options) for how to build the options bytes for worker configuration (gas limits, native value, etc.).

## 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 a `MessagingFee` 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's `send` function.

**Key parameters**:

* `dst_eid`: Destination chain endpoint ID
* `message`: Your encoded payload (raw bytes)
* `options`: Execution parameters (gas limits, native value)
* `fee_payer`: `FeePayer::Unverified(addr)` or `FeePayer::Verified(addr)`
* `fee`: The `MessagingFee` from `__quote()`
* `refund_address`: Where to send excess fees

**FeePayer enum**:

* `FeePayer::Unverified(addr)` — Safe default. `__lz_send` will call `addr.require_auth()`.
* `FeePayer::Verified(addr)` — Use when the caller has already called `require_auth()` to avoid duplicate authorization in the Soroban auth tree.

### lz\_receive()

Processes incoming messages delivered by the Executor. The default implementation (provided by `OAppReceiver`) 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:

| Function                                   | Caller           | Description                                                                                                                     |
| ------------------------------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `set_peer`                                 | Owner            | Register trusted remote OApp addresses                                                                                          |
| `set_enforced_options`                     | Owner            | Define minimum execution options per destination                                                                                |
| `set_delegate`                             | Owner            | Change the delegate address on the Endpoint                                                                                     |
| `set_config` (DVN/Executor)                | Delegate or OApp | Configure DVN and Executor via the Endpoint ([DVN & Executor Config](/v2/developers/stellar/configuration/dvn-executor-config)) |
| `set_send_library` / `set_receive_library` | Delegate or OApp | Override default message libraries on the Endpoint                                                                              |

### Events

| Event               | Topics | Data                                   |
| ------------------- | ------ | -------------------------------------- |
| `PeerSet`           | --     | `eid: u32`, `peer: Option<BytesN<32>>` |
| `EnforcedOptionSet` | --     | `Vec<EnforcedOptionParam>`             |

## 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](https://stellar.org/protocol/sep-23): 1-byte version + 32-byte payload + 2-byte CRC16 checksum. LayerZero uses the **32-byte payload** (contract ID hash) as the bytes32 peer address.

```solidity wrap theme={null}
// On EVM: Use Stellar OApp contract's 32-byte payload (not the full C-address)
myOApp.setPeer(
    <STELLAR_EID>,           // Stellar endpoint ID
    bytes32(payload)         // 32-byte payload from Stellar C-address
);
```

To extract the 32-byte payload from a C-address, decode the base32 StrKey and take bytes 1–32 (skipping the version byte and CRC16 checksum).

### 2. Set Enforced Options for Stellar Destination

Configure minimum execution options for messages sent to Stellar:

```solidity wrap theme={null}
// On EVM: Set enforced options for Stellar pathway
bytes memory options = Options.newOptions()
    .addExecutorLzReceiveOption(5000, 0);  // Gas units, no msg.value

myOApp.setEnforcedOptions(
    EnforcedOptionParam({
        eid: <STELLAR_EID>,
        msgType: SEND,
        options: options
    })
);
```

### 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's `send` 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::Unverified` when 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](/v2/developers/stellar/oft/overview)**: Deploy a token with built-in crosschain transfer.
* **[DVN & Executor Config](/v2/developers/stellar/configuration/dvn-executor-config)**: Configure your security stack.
* **[Technical Overview](/v2/developers/stellar/technical-overview)**: Understand the full message lifecycle.
* **[Troubleshooting](/v2/developers/stellar/troubleshooting/common-errors)**: Common errors and solutions.
