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

# Stellar Technical Overview

> Comprehensive technical reference for LayerZero on Stellar. Covers Soroban architecture, the full message lifecycle (send, verify, receive), proc macros, storage model, and protocol internals.

This article covers Stellar's Soroban smart contract platform and the full LayerZero V2 protocol lifecycle from the perspective of a LayerZero developer.

**What you'll learn:**

* Soroban's WASM-based VM architecture, address types, and gas model
* The complete message lifecycle: send, verify, receive, and recovery
* Contract development model: proc macros, storage, TTL, and upgrades
* Operations and security: ownership, delegation, and RBAC
* Technical constraints specific to Stellar

<Tip>
  If you're new to Stellar, start with the [Getting Started](/v2/developers/stellar/getting-started) guide to understand the key differences from EVM before diving into the technical details.
</Tip>

## VM Architecture

Soroban is Stellar's smart contract runtime. Contracts are written in Rust, compiled to **WebAssembly (WASM)**, and executed in a sandboxed VM.

### Key Characteristics

| Property               | Detail                         |
| ---------------------- | ------------------------------ |
| **Language**           | Rust                           |
| **Compilation target** | `wasm32v1-none`                |
| **SDK**                | `soroban-sdk` v25.1.1          |
| **Execution model**    | Single-threaded, deterministic |

### Address Types

Stellar uses StrKey-encoded addresses with the structure: **1-byte version + 32-byte payload + 2-byte checksum** (35 bytes total, base32-encoded to 56 characters):

| Address Type  | First Char | Payload                    | Usage                   |
| ------------- | ---------- | -------------------------- | ----------------------- |
| **G-address** | `G`        | 32-byte Ed25519 public key | Accounts (EOAs)         |
| **C-address** | `C`        | 32-byte contract ID        | Soroban smart contracts |

#### Crosschain Address Format

LayerZero V2 standardizes all crosschain addresses as `bytes32`. Since G-addresses and C-addresses share the same 32-byte payload structure, LayerZero uses this payload directly as the `bytes32` representation. The payload is guaranteed to be unique across both address types.

When receiving crosschain messages, the protocol resolves the `bytes32` back to a Stellar address using **deterministic address resolution**:

* **OApp and Composer addresses**: The `bytes32` value is interpreted directly as a contract ID (C-address).
* **Native drop and OFT receiver addresses**: Contract-first detection is applied — if a contract exists at the address, it is treated as a C-address; otherwise, it is treated as a G-address (account).

### XDR Encoding

Where EVM uses ABI encoding for function calls and configuration, Soroban uses **XDR (External Data Representation)**. This applies to message library configuration (ULN configs, executor configs), cross-contract calls, and event data. The TypeScript SDK handles XDR encoding/decoding automatically.

### Gas Model

Soroban uses a **resource-based fee model** rather than EVM-style gas:

| Resource               | Description                       |
| ---------------------- | --------------------------------- |
| **CPU instructions**   | Computation cost                  |
| **Memory bytes**       | Runtime memory allocation         |
| **Read/write bytes**   | Ledger storage I/O                |
| **Read/write entries** | Number of ledger entries accessed |
| **Transaction size**   | Byte size of the transaction      |
| **Events**             | Size of emitted events            |

Fees are calculated based on resource consumption and a network-wide base fee multiplier. Unlike EVM, there is no concept of "gas price" that fluctuates with demand -- fees are deterministic based on the resources declared upfront.

<Tip>
  Soroban transactions must **declare their resource limits upfront** (CPU, memory, storage). If the transaction exceeds the declared limits, it fails. The Stellar CLI and SDK estimate these limits automatically when simulating transactions.
</Tip>

## Message Lifecycle

### Overview

```mermaid theme={null}
sequenceDiagram
    participant User
    participant OApp as OApp (Source)
    participant EP as Endpoint V2 (Source)
    participant ULN as ULN-302 (Source)
    participant DVN as DVN (Off-chain)
    participant Exec as Executor (Off-chain)
    participant DstULN as ULN-302 (Dest)
    participant DstEP as Endpoint V2 (Dest)
    participant DstOApp as OApp (Dest)

    User->>OApp: send(from, send_param, fee, refund_address)
    OApp->>EP: send(sender, messaging_params, refund_address)
    EP->>ULN: send(packet, options)
    Note over EP: Emit PacketSent event
    DVN-->>DVN: Observe event, verify on destination
    DVN->>DstULN: verify(dvn, packet_header, payload_hash, confirmations)
    Note over DstULN: Store DVN attestation
    DstULN->>DstULN: commit_verification(packet_header, payload_hash)
    DstULN->>DstEP: verify(receive_lib, origin, receiver, payload_hash)
    Note over DstEP: Emit PacketVerified event
    Exec->>DstOApp: lz_receive(executor, origin, guid, message, extra_data, value)
    DstOApp->>DstEP: clear(caller, origin, receiver, guid, message)
    Note over DstEP: Emit PacketDelivered event
```

### Core Data Structures

#### MessagingParams

The parameters passed to the Endpoint's `send` function:

```rust wrap theme={null}
struct MessagingParams {
    dst_eid: u32,          // Destination endpoint ID
    receiver: BytesN<32>,  // Recipient address (32-byte canonical form)
    message: Bytes,        // Application-level message payload
    options: Bytes,        // Execution options (gas, value, etc.)
    pay_in_zro: bool,      // Pay fees in ZRO token instead of native
}
```

#### Origin

Identifies the source of an inbound message:

```rust wrap theme={null}
struct Origin {
    src_eid: u32,          // Source endpoint ID
    sender: BytesN<32>,    // Sender address (32-byte canonical form)
    nonce: u64,            // Message sequence number
}
```

#### MessagingFee

Fee breakdown for sending a message:

```rust wrap theme={null}
struct MessagingFee {
    native_fee: i128,      // Fee in native token (XLM)
    zro_fee: i128,         // Fee in ZRO token (if pay_in_zro = true)
}
```

#### MessagingReceipt

Returned after a successful send:

```rust wrap theme={null}
struct MessagingReceipt {
    guid: BytesN<32>,      // Globally unique message identifier
    nonce: u64,            // Outbound nonce for this path
    fee: MessagingFee,     // Actual fees charged
}
```

### Send Workflow

#### Step 1: Application Prepares Send

The OApp (or OFT) prepares the message and quotes the fee:

```rust wrap theme={null}
// Quote the messaging fee
let fee = oft.quote_send(&from, &send_param, &false);

// Execute the send
let (receipt, oft_receipt) = oft.send(&from, &send_param, &fee, &refund_address);
```

#### Step 2: Endpoint Send

The OApp calls the Endpoint's `send` function:

1. The OApp transfers the native fee to the Endpoint: `native_token.transfer(fee_payer, endpoint, native_fee)`
2. If paying in ZRO, transfers ZRO fee similarly
3. The Endpoint assigns the next outbound nonce and computes the GUID
4. The Endpoint calls the configured send library (ULN-302)

#### Step 3: Message Library Processing

ULN-302 processes the outbound packet:

1. Encodes the packet (header + payload)
2. Computes the payload hash
3. Calculates DVN, executor, and treasury fees

After the send library returns, the Endpoint emits the `PacketSent` event with the encoded packet and options.

**Send Events:**

| Event                | Topics                    | Data                                        |
| -------------------- | ------------------------- | ------------------------------------------- |
| `PacketSent`         | --                        | `encoded_packet`, `options`, `send_library` |
| `OFTSent` (OFT only) | `guid`, `dst_eid`, `from` | `amount_sent_ld`, `amount_received_ld`      |

### Verification Workflow

After the `PacketSent` event is emitted, off-chain DVNs observe the event and verify the message on the destination chain.

#### Step 1: DVN Submits Verification

Each configured DVN calls `verify` on the destination's ULN-302:

```rust wrap theme={null}
// DVN submits verification (on destination chain)
uln302.verify(&dvn, &packet_header, &payload_hash, &confirmations);
```

The DVN authenticates via Soroban's custom account contract (multisig with secp256k1 signatures).

#### Step 2: Threshold Check

ULN-302 checks whether enough DVNs have verified:

* **All required DVNs** must verify
* **At least `optional_dvn_threshold`** optional DVNs must verify

The `verifiable` function checks the current verification status without committing:

```rust wrap theme={null}
let is_ready = uln302.verifiable(&packet_header, &payload_hash);
```

#### Step 3: Commit Verification

Once the threshold is met, anyone can call `commit_verification` (permissionless):

```rust wrap theme={null}
uln302.commit_verification(&packet_header, &payload_hash);
```

This calls `endpoint.verify(...)`, which stores the payload hash and emits `PacketVerified`.

**Verification Events:**

| Event            | Topics               | Data           |
| ---------------- | -------------------- | -------------- |
| `PacketVerified` | `origin`, `receiver` | `payload_hash` |

### Receive Workflow

After verification, the Executor delivers the message to the destination OApp.

#### Step 1: Executor Calls lz\_receive

The Executor calls `lz_receive` on the destination OApp:

```rust wrap theme={null}
oapp.lz_receive(
    &executor,       // Executor address (must authorize)
    &origin,         // Source chain info
    &guid,           // Message GUID
    &message,        // Application payload
    &extra_data,     // Additional data
    &value,          // Native token value to forward
);
```

#### Step 2: OApp Validates and Clears

The OApp's `lz_receive` implementation (provided by the `OAppReceiver` trait's default method):

1. **Validates executor**: `executor.require_auth()`
2. **Validates sender**: Checks `origin.sender` matches the configured peer for `origin.src_eid`
3. **Forwards value**: If `value != 0`, transfers native token from executor to OApp
4. **Clears payload**: Calls `endpoint.clear(oapp, origin, oapp, guid, message)` to mark the message as delivered
5. **Executes business logic**: Calls `__lz_receive(origin, guid, message, extra_data, executor, value)` (your custom implementation)

#### Step 3: Application Logic

Your `__lz_receive` implementation processes the decoded message. For OFTs, this includes token crediting (unlock or mint), address resolution, and optional compose handling -- see [OFT Overview](/v2/developers/stellar/oft/overview#receiving-tokens) for details.

**Receive Events:**

| Event                    | Topics                  | Data                 |
| ------------------------ | ----------------------- | -------------------- |
| `PacketDelivered`        | `origin`, `receiver`    | --                   |
| `OFTReceived` (OFT only) | `guid`, `src_eid`, `to` | `amount_received_ld` |

#### Step 4: Compose (Optional)

If the message includes a compose payload, the Endpoint stores it and the Executor triggers the compose call in a separate transaction:

1. OApp calls `endpoint.send_compose(from, to, guid, index, compose_msg)`
2. Executor calls `composer.lz_compose(executor, from, guid, index, message, extra_data, value)` on the target composer contract
3. Composer processes the compose message (e.g., swap, stake, etc.)

**Compose Events:**

| Event         | Topics                        | Data      |
| ------------- | ----------------------------- | --------- |
| `ComposeSent` | `from`, `to`, `guid`, `index` | `message` |

### Recovery Operations

If a message fails to deliver or gets stuck, several recovery operations are available. The `caller` must be the receiving OApp itself or its registered delegate.

#### Skip

Skip the **next expected** inbound nonce without processing the message. The provided nonce must equal `inbound_nonce + 1` -- you cannot skip arbitrary nonces. Useful for ordered messaging when a message should be bypassed:

```rust wrap theme={null}
endpoint.skip(&caller, &receiver, &src_eid, &sender, &nonce);
```

#### Clear

Remove a verified payload that hasn't been delivered. The nonce in `origin` must be at or below `inbound_nonce`, and the hash derived from `guid` and `message` must match the stored payload hash:

```rust wrap theme={null}
endpoint.clear(&caller, &origin, &receiver, &guid, &message);
```

#### Nilify

Mark a message as nil (void), preventing execution until re-verified. The supplied `payload_hash` must exactly match the currently stored value. For a verified, unexecuted nonce, pass its stored hash. To pre-emptively nilify a future nonce, the nonce must be greater than `inbound_nonce` but no greater than `inbound_nonce + 256`; pass `None` when no hash is currently stored. A nonce at or below `inbound_nonce` can be nilified only while it still has a stored payload hash:

```rust wrap theme={null}
// Use Some(payload_hash) for an existing verified hash.
// For a future nonce with no stored hash, set expected_payload_hash to None instead.
let expected_payload_hash = Some(payload_hash);
endpoint.nilify(&caller, &receiver, &src_eid, &sender, &nonce, &expected_payload_hash);
```

#### Burn

Permanently burn a nonce. The nonce must be at or below `inbound_nonce`, and `payload_hash` must match a hash currently stored for that nonce. Unlike nilify, the removed hash cannot be restored by re-verification:

```rust wrap theme={null}
endpoint.burn(&caller, &receiver, &src_eid, &sender, &nonce, &payload_hash);
```

**Recovery Operations Summary:**

| Operation | When to Use                                                   | Key Preconditions                                                                                                                                             | Reversible?     |
| --------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
| `skip`    | Skip the next nonce in ordered delivery                       | `nonce == inbound_nonce + 1`                                                                                                                                  | No              |
| `clear`   | Remove an undelivered verified payload                        | `nonce <= inbound_nonce`; the supplied payload must match the stored hash                                                                                     | No              |
| `nilify`  | Void a verified message or pre-emptively block a future nonce | The supplied optional hash must match the stored value; either `nonce > inbound_nonce` (and within the 256-nonce pending window) or a hash must already exist | Yes (re-verify) |
| `burn`    | Permanently destroy a nonce                                   | `nonce <= inbound_nonce`; a matching stored hash must exist                                                                                                   | No              |

***

## Contract Development Model

### Trait Composition with Proc Macros

Soroban contracts use Rust's proc macros instead of inheritance:

| Macro                         | Purpose                                                                     |
| ----------------------------- | --------------------------------------------------------------------------- |
| `#[oapp]`                     | Generates OApp trait implementations (Core, Sender, Receiver, OptionsType3) |
| `#[oapp(custom = [...])]`     | Skip generating specific traits for custom implementations                  |
| `#[storage]`                  | Generates typed storage accessors from an enum                              |
| `#[contract_impl]`            | Extends contract functions with automatic TTL management                    |
| `#[only_auth]`                | Auth-gated function attribute                                               |
| `#[only_role(account, ROLE)]` | Role-based access control check                                             |
| `#[ownable]`                  | Generates ownership management (single-step and 2-step transfer)            |
| `#[upgradeable]`              | Contract upgrade + migration support                                        |
| `#[lz_contract]`              | Combines `#[contract]` + `#[ownable]` + TTL management                      |
| `#[ttl_configurable]`         | Allows adjusting TTL threshold and extension parameters                     |
| `#[ttl_extendable]`           | Automatically extends TTL on storage access                                 |

**Example:**

```rust wrap theme={null}
#[oapp]
pub struct MyOFT;

// The #[oapp] macro generates:
// - OAppCore (endpoint, peer, set_peer, set_delegate)
// - OAppSenderInternal (__quote, __lz_send)
// - OAppReceiver (lz_receive, allow_initialize_path, next_nonce)
// - OAppOptionsType3 (enforced_options, set_enforced_options, combine_options)
```

### Storage Model

Soroban contracts use a typed enum with the `#[storage]` macro to define storage layout:

```rust wrap theme={null}
#[storage]
enum OFTStorage {
    #[instance(u32)]
    DecimalsDiff,        // Immutable after construction
    #[instance(Address)]
    Token,               // Immutable after construction
    #[instance(Address)]
    MsgInspector,        // Optional, mutable
}
```

The macro generates typed `get`, `set`, `has`, and `remove` methods for each variant.

#### Storage Tiers

| Tier           | Annotation         | TTL              | Use in LayerZero                          |
| -------------- | ------------------ | ---------------- | ----------------------------------------- |
| **Instance**   | `#[instance(T)]`   | Tied to contract | Endpoint address, token address, decimals |
| **Persistent** | `#[persistent(T)]` | Must be extended | Peer mappings, rate limit state           |
| **Temporary**  | `#[temporary(T)]`  | Short-lived      | Caches, intermediate computation          |

#### TTL Management

All Soroban storage entries have a time-to-live. LayerZero contracts use the `#[ttl_configurable]` and `#[ttl_extendable]` macros to manage TTL automatically:

* **Instance storage**: TTL extended automatically on every contract call via `#[contract_impl]`
* **Persistent storage**: TTL extended automatically when entries are read or written
* **Temporary storage**: Short-lived, no automatic extension

<Warning>
  If a persistent entry's TTL expires, it becomes **archived**. It still exists on the ledger but must be restored (with a fee) before it can be read. Ensure your operational processes monitor and extend TTLs for critical state.
</Warning>

#### Manual TTL Configuration

Contracts with `#[ttl_configurable]` allow adjusting TTL thresholds:

```bash wrap theme={null}
# Set TTL extension parameters (owner/admin only)
# Values are in ledgers (~5 seconds each)
stellar contract invoke \
  --id <CONTRACT_ADDRESS> \
  --network testnet \
  --source my-account \
  -- \
  set_ttl_configs \
  --instance '{"threshold": 1000, "extend_to": 5000}' \
  --persistent '{"threshold": 1000, "extend_to": 5000}'
```

#### Monitoring TTL

For operational monitoring, check the TTL of critical entries:

```bash wrap theme={null}
stellar contract read \
  --id <CONTRACT_ADDRESS> \
  --network testnet \
  --key <STORAGE_KEY> \
  --durability persistent
```

<Warning>
  Monitor persistent storage TTLs in production. If a peer mapping entry expires and gets archived, the OApp won't be able to validate inbound messages from that chain until the entry is restored.
</Warning>

### Contract Upgrades

LayerZero Soroban contracts support native upgrades via the `#[upgradeable]` macro:

```rust wrap theme={null}
// Upgrade to a new WASM implementation
contract.upgrade(&new_wasm_hash);

// Must call migrate after upgrade to clear the migration flag
contract.migrate(&migration_data);
```

**Upgrade flow:**

1. Upload the new WASM to the network (but don't create a contract instance)
2. Call `upgrade` on the existing contract with the new WASM hash
3. The contract code is replaced atomically
4. Call `migrate` to clear the migration flag and optionally adjust state

<Tip>
  Always test upgrades on testnet first. Verify that storage layout is compatible -- adding new storage variants is safe, but removing or reordering existing ones is not.
</Tip>

***

## Operations & Security

### Ownership

LayerZero Soroban contracts support both single-step and **2-step ownership transfer**:

* **Single-step**: `transfer_ownership(new_owner)` -- immediate, irreversible (will fail if a 2-step transfer is already pending)
* **2-step (recommended)**:
  1. Current owner calls `begin_ownership_transfer(new_owner, ttl)` -- proposes transfer with a TTL window (in ledgers, \~5s each)
  2. New owner calls `accept_ownership()` -- confirms transfer within the TTL
  3. If not accepted within the TTL, the proposal expires and the original owner retains ownership

This prevents accidental ownership loss from typos or incorrect addresses.

```bash wrap theme={null}
# Step 1: Begin transfer (2-step)
# TTL is in ledgers (~5s each). 120960 ledgers ≈ 1 week.
stellar contract invoke \
  --id <CONTRACT_ADDRESS> \
  --network testnet \
  --source current-owner \
  -- \
  begin_ownership_transfer \
  --new_owner <NEW_OWNER_ADDRESS> \
  --ttl 120960
```

```bash wrap theme={null}
# Step 2: Accept transfer (from new owner)
stellar contract invoke \
  --id <CONTRACT_ADDRESS> \
  --network testnet \
  --source new-owner \
  -- \
  accept_ownership
```

### Delegate

The delegate can manage endpoint configuration (message libraries, DVN config) without owner-level access:

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

Set to `None` to remove the delegate.

### Role-Based Access Control

`RoleBasedAccessControl` adds OpenZeppelin-style role membership so administrative actions can be delegated without surrendering ownership. A vanilla `#[lz_contract] #[oapp]` contract exposes both layers automatically.

Some contracts (e.g., SAC Manager) use RBAC with role-gated functions:

```rust wrap theme={null}
#[only_role(operator, "MINTER_ROLE")]
fn mint(env: &Env, to: &Address, amount: i128, operator: &Address) {
    // Only accounts with MINTER_ROLE can call this
}
```

***

## Technical Constraints

### Pending Inbound Nonce Cap

LayerZero's Stellar Endpoint uses an eager inbound nonce model with a bounded pending nonce list:

* Maximum **256 pending nonces** (`PENDING_INBOUND_NONCE_MAX_LEN`)
* Nonces beyond `inbound_nonce + 256` cannot be verified
* Pending nonces drain automatically as they become consecutive

### Reentrancy Prohibition

Soroban prohibits reentrancy -- contracts cannot call themselves (directly or indirectly) in the same transaction. This is why LayerZero uses **pull mode** for message delivery: the executor calls `OApp.lz_receive()` directly, and the OApp calls `endpoint.clear()` internally to verify and clear the payload. This avoids the Endpoint -> OApp -> Endpoint reentrancy that would occur if the Endpoint delivered messages to the OApp directly.

## Next Steps

* **[Build an OApp](/v2/developers/stellar/oapp/overview)**: Create your first Omnichain Application.
* **[Build an OFT](/v2/developers/stellar/oft/overview)**: Deploy an Omnichain Fungible Token.
* **[DVN & Executor Config](/v2/developers/stellar/configuration/dvn-executor-config)**: Configure your security stack.
* **[Troubleshooting](/v2/developers/stellar/troubleshooting/common-errors)**: Common errors and how to resolve them.
