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

# LayerZero V2 OApp on Starknet

> Build crosschain applications on Starknet with LayerZero V2 OApp standard. Learn component integration, message sending and receiving.

The **Omnichain Application (OApp)** standard provides the foundational building blocks for crosschain messaging on Starknet. OApps can send arbitrary data to any supported chain and receive messages from other chains.

## What is an OApp on Starknet?

An OApp on Starknet is a Cairo contract that:

1. **Integrates with LayerZero** via the OAppCoreComponent
2. **Sends messages** through the Endpoint's `send` function
3. **Receives messages** by implementing the `OAppHooks` trait
4. **Manages peers** (trusted remote OApps on other chains)

## Differences from EVM OApps

| Aspect          | EVM (Solidity)               | Starknet (Cairo)                  |
| --------------- | ---------------------------- | --------------------------------- |
| Base Contract   | `OApp` inheritance           | `OAppCoreComponent` composition   |
| Send Message    | `_lzSend()`                  | `_lz_send()` via OAppSenderImpl   |
| Receive Message | `_lzReceive()` override      | `_lz_receive` via OAppHooks trait |
| Peer Storage    | `mapping(uint32 => bytes32)` | `Map<u32, Bytes32>`               |
| Authorization   | `onlyOwner` modifier         | `assert_only_owner()`             |
| Options Builder | `OptionsBuilder` library     | ByteArray encoding                |

## Installation

### Step 1: Install LayerZero Cairo Contracts

```bash theme={null}
# Create your project directory
mkdir my-oapp-project && cd my-oapp-project

# Initialize npm and install LayerZero Starknet packages
npm init -y
npm install @layerzerolabs/protocol-starknet-v2
```

### Step 2: Configure Scarb.toml

```toml wrap theme={null}
[package]
name = "my_oapp"
version = "0.1.0"
edition = "2024_07"

[dependencies]
starknet = "2.14.0"
openzeppelin = "2.0.0"
lz_utils = { path = "./node_modules/@layerzerolabs/protocol-starknet-v2/libs/lz_utils" }
layerzero = { path = "./node_modules/@layerzerolabs/protocol-starknet-v2/layerzero" }

[dev-dependencies]
snforge_std = "0.53.0"

[[target.starknet-contract]]
sierra = true
casm = true
```

<Note>
  **Tool versions**

  Use Scarb 2.14.0 and Starknet Foundry 0.53.0. Mismatched versions can cause class hash mismatch errors during `sncast declare`.
</Note>

### Step 3: Create Project Structure

```bash theme={null}
mkdir -p src tests
echo 'pub mod my_oapp;' > src/lib.cairo
```

```
my-oapp/
├── package.json
├── node_modules/@layerzerolabs/protocol-starknet-v2/
├── Scarb.toml
├── src/
│   ├── lib.cairo
│   └── my_oapp.cairo
└── tests/
    └── test_my_oapp.cairo
```

### lib.cairo

```rust wrap theme={null}
pub mod my_oapp;
```

***

### Step 4: Configure snfoundry.toml

Create a `snfoundry.toml` with your account name and RPC URL. See [Starknet Guidance](/v2/developers/starknet/technical-reference/starknet-guidance) for the full configuration reference and RPC version compatibility notes.

## Deployment

### Step 1: Build

```bash theme={null}
scarb build
```

Build artifacts are generated in `target/dev/` by default.

### Step 2: Declare

```bash theme={null}
sncast --account <ACCOUNT_NAME> declare \
  --contract-name MyOApp \
  --url <RPC_URL>

# Returns: class_hash = <CLASS_HASH>
```

### Step 3: Deploy

```bash theme={null}
sncast --account <ACCOUNT_NAME> deploy \
  --class-hash <CLASS_HASH> \
  --url <RPC_URL> \
  --arguments '<ENDPOINT_ADDRESS>, <OWNER_ADDRESS>, <NATIVE_TOKEN_ADDRESS>'

# Constructor parameters:
# - endpoint: LayerZero Endpoint address
# - owner: Contract owner address
# - native_token: Fee payment token (STRK address)
```

<Note>
  **Network flag**

  If you set `url` in `snfoundry.toml`, omit `--network` (sncast will reject it).
</Note>

<Tip>
  **Using --arguments**

  The `--arguments` flag allows passing constructor arguments in a human-readable format. sncast automatically serializes them based on the contract's ABI. For more details, see [Calldata Transformation](https://foundry-rs.github.io/starknet-foundry/starknet/calldata-transformation.html).
</Tip>

### Step 4: Verify

```bash theme={null}
sncast verify \
  --class-hash <CLASS_HASH> \
  --contract-name MyOApp \
  --verifier voyager \
  --network sepolia \
  --confirm-verification
```

For more verification options, see the [Starknet Foundry verification guide](https://foundry-rs.github.io/starknet-foundry/starknet/verify.html).

### Step 5: Configure

```bash theme={null}
# Set peer for destination chain (e.g., Ethereum Mainnet eid=30101)
# For EVM address 0x1234567890abcdef1234567890abcdef12345678:
# - Pad to 32 bytes: 0x0000000000000000000000001234567890abcdef1234567890abcdef12345678
# - Split into u256 (low, high): low=0x90abcdef1234567890abcdef12345678, high=0x12345678
sncast --account <ACCOUNT_NAME> invoke \
  --contract-address <YOUR_OAPP> \
  --function set_peer \
  --url <RPC_URL> \
  --calldata 0x7595 0x90abcdef1234567890abcdef12345678 0x12345678
```

<Info>
  **Bytes32 Encoding**

  Peer addresses are stored as `Bytes32` (a struct containing a `u256`). For EVM addresses (20 bytes), left-pad with zeros to 32 bytes.

  **Calldata format for `set_peer(eid: u32, peer: Bytes32)`:**

  1. `eid` - endpoint ID as hex (e.g., `0x7595` = 30101 for Ethereum Mainnet)
  2. `peer.value.low` - lower 128 bits of the padded address
  3. `peer.value.high` - upper 128 bits of the padded address

  Use `--calldata` with space-separated hex values (not `--arguments`) for complex types like `Bytes32`.
</Info>

***

## Working Example: Minimal OApp

A minimal OApp on Starknet:

```rust wrap theme={null}
#[starknet::contract]
pub mod MyOApp {
    use layerzero::oapps::oapp::oapp_core::OAppCoreComponent;
    use layerzero::common::structs::packet::Origin;
    use lz_utils::bytes::Bytes32;
    use openzeppelin::access::ownable::OwnableComponent;
    use starknet::ContractAddress;

    // Declare components
    component!(path: OAppCoreComponent, storage: oapp_core, event: OAppCoreEvent);
    component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);

    // Embed OAppCore implementation (exposes external functions)
    #[abi(embed_v0)]
    impl OAppCoreImpl = OAppCoreComponent::OAppCoreImpl<ContractState>;
    #[abi(embed_v0)]
    impl ILayerZeroReceiverImpl = OAppCoreComponent::LayerZeroReceiverImpl<ContractState>;
    #[abi(embed_v0)]
    impl IOAppReceiverImpl = OAppCoreComponent::OAppReceiverImpl<ContractState>;
    impl OAppCoreInternalImpl = OAppCoreComponent::InternalImpl<ContractState>;

    // Embed Ownable implementation
    #[abi(embed_v0)]
    impl OwnableImpl = OwnableComponent::OwnableImpl<ContractState>;
    impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        oapp_core: OAppCoreComponent::Storage,
        #[substorage(v0)]
        ownable: OwnableComponent::Storage,
        // Your custom storage here
        data: felt252,
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    pub enum Event {
        #[flat]
        OAppCoreEvent: OAppCoreComponent::Event,
        #[flat]
        OwnableEvent: OwnableComponent::Event,
    }

    #[constructor]
    fn constructor(
        ref self: ContractState,
        endpoint: ContractAddress,
        owner: ContractAddress,
        native_token: ContractAddress,
    ) {
        // Initialize OAppCore with endpoint, owner (delegate), and native token
        self.oapp_core.initializer(endpoint, owner, native_token);
        self.ownable.initializer(owner);
    }

    // Implement OAppHooks to handle incoming messages
    impl OAppHooks of OAppCoreComponent::OAppHooks<ContractState> {
        fn _lz_receive(
            ref self: OAppCoreComponent::ComponentState<ContractState>,
            origin: Origin,
            guid: Bytes32,
            message: ByteArray,
            executor: ContractAddress,
            extra_data: ByteArray,
            value: u256,
        ) {
            // Your receive logic here
            // Access contract state via get_contract_mut!
        }
    }
}
```

## Required Components

### OAppCoreComponent

The core LayerZero integration:

```rust wrap theme={null}
// Storage fields
pub struct Storage {
    pub OAppCore_endpoint: ContractAddress,      // LayerZero Endpoint
    pub OAppCore_native_token: ContractAddress,  // Fee payment token
    pub OAppCore_peers: Map<u32, Bytes32>,       // Trusted peers per chain
}
```

**Provided Functions**:

| Function                 | Description                  |
| ------------------------ | ---------------------------- |
| `set_peer(eid, peer)`    | Set trusted peer for a chain |
| `get_peer(eid)`          | Get peer address for a chain |
| `set_delegate(delegate)` | Set configuration delegate   |
| `endpoint()`             | Get Endpoint address         |

### OwnableComponent

OpenZeppelin's ownership management:

```rust wrap theme={null}
// Provided Functions
owner() -> ContractAddress
transfer_ownership(new_owner)
renounce_ownership()
```

***

## How OApp Messaging Works

### Peer Configuration: Establishing Trust

Peers must be set bidirectionally for two OApps to communicate:

#### Setting a Peer

The `OAppCoreComponent` provides `set_peer` automatically when you embed `OAppCoreImpl`. You call it directly on your deployed contract:

```bash theme={null}
sncast invoke --contract-address <YOUR_OAPP> --function set_peer --calldata <eid> <peer_low> <peer_high>
```

Internally, the component implements it as:

```rust wrap theme={null}
// Inside OAppCoreComponent::OAppCoreImpl (already embedded)
fn set_peer(ref self: ComponentState<TContractState>, eid: u32, peer: Bytes32) {
    self._assert_only_owner();
    self.OAppCore_peers.entry(eid).write(peer);
    self.emit(PeerSet { eid, peer });
}
```

#### Peer Address Format

Peers are stored as `Bytes32` for cross-VM compatibility:

```rust wrap theme={null}
// Starknet address → Bytes32
let starknet_peer: Bytes32 = starknet_address.into();

// EVM address (20 bytes) → Bytes32 (left-padded with zeros)
let evm_peer: Bytes32 = Bytes32 {
    value: 0x000000000000000000000000_ABCDEF1234567890ABCDEF1234567890ABCDEF12
};
```

#### Bidirectional Setup

```
Chain A (Starknet)              Chain B (EVM)
┌─────────────────┐            ┌─────────────────┐
│ OApp A          │            │ OApp B          │
│                 │            │                 │
│ peers[B] = 0xB  │◄──────────►│ peers[A] = 0xA  │
└─────────────────┘            └─────────────────┘
```

Both sides must set peers before messages can flow.

***

### Sending Messages

#### Step 1: Quote the Fee

```rust wrap theme={null}
use layerzero::oapps::oapp::oapp_core::OAppCoreComponent;
use layerzero::common::structs::messaging::{MessagingParams, MessagingFee};

fn quote(
    self: @ContractState,
    dst_eid: u32,
    message: ByteArray,
    options: ByteArray,
    pay_in_lz_token: bool,
) -> MessagingFee {
    let oapp_core = get_dep_component!(self, OAppCore);
    OAppCoreComponent::OAppSenderImpl::_quote(
        oapp_core,
        dst_eid,
        message,
        options,
        pay_in_lz_token,
    )
}
```

#### Step 2: Build Options

Options specify execution parameters on the destination chain:

```rust wrap theme={null}
use lz_utils::byte_array_ext::byte_array_ext::ByteArrayTraitExt;

const EXECUTOR_WORKER_ID: u8 = 1;
const OPTION_TYPE_LZRECEIVE: u8 = 1;

/// Build executor options for lz_receive with specified gas limit
fn build_options(gas_limit: u128) -> ByteArray {
    // Build params (gas only, no native value)
    let mut params: ByteArray = Default::default();
    params.append_u128(gas_limit);

    // Build options with Type 3 format
    let mut options: ByteArray = Default::default();
    options.append_u16(3);                              // Option type 3 header
    options.append_u8(EXECUTOR_WORKER_ID);              // Worker ID (Executor = 1)
    options.append_u16(params.len().try_into().unwrap() + 1);  // Length (params + option type)
    options.append_u8(OPTION_TYPE_LZRECEIVE);           // LzReceive option type
    options.append(@params);                            // Gas limit (16 bytes)

    options
}
```

#### Step 3: Send the Message

The `_lz_send` function handles fee payment internally. It expects the caller to have approved the OApp contract (not the endpoint) to spend their tokens. The function will:

1. Transfer tokens from caller to the contract
2. Approve the endpoint to spend the tokens
3. Send the message via the endpoint

```rust wrap theme={null}
fn send(
    ref self: ContractState,
    caller: ContractAddress,
    dst_eid: u32,
    message: ByteArray,
    options: ByteArray,
    fee: MessagingFee,
    refund_address: ContractAddress,
) -> MessageReceipt {
    // _lz_send handles token transfer and endpoint approval internally
    let mut oapp_core = get_dep_component_mut!(ref self, OAppCore);
    OAppCoreComponent::OAppSenderImpl::_lz_send(
        ref oapp_core,
        caller,        // Caller who approved this contract for fee payment
        dst_eid,
        message,
        options,
        fee,
        refund_address,
    )
}
```

#### Complete Send Example

```rust wrap theme={null}
#[external(v0)]
fn send_message(
    ref self: ContractState,
    dst_eid: u32,
    message: ByteArray,
) {
    let caller = get_caller_address();

    // Build options (200,000 gas for lz_receive)
    let options = build_options(200000);

    // Quote fee
    let fee = self.quote(dst_eid, message.clone(), options.clone(), false);

    // IMPORTANT: Caller must have approved THIS CONTRACT (not the endpoint)
    // to spend native_fee amount of the native token BEFORE calling this function.
    // The _lz_send function will:
    // 1. transfer_from(caller, this_contract, fee)
    // 2. approve(endpoint, fee)
    // 3. endpoint.send(...)

    // Send message - _lz_send handles all token transfers internally
    let mut oapp_core = get_dep_component_mut!(ref self, OAppCore);
    let receipt = OAppCoreComponent::OAppSenderImpl::_lz_send(
        ref oapp_core,
        caller,
        dst_eid,
        message,
        options,
        fee,
        caller,  // refund_address
    );

    // Emit event with guid for tracking
    self.emit(MessageSent { guid: receipt.guid, dst_eid });
}
```

***

### Receiving Messages

#### Implementing OAppHooks

The `OAppHooks` trait defines how your OApp handles incoming messages:

```rust wrap theme={null}
impl OAppHooks of OAppCoreComponent::OAppHooks<ContractState> {
    fn _lz_receive(
        ref self: OAppCoreComponent::ComponentState<ContractState>,
        origin: Origin,           // Source chain info
        guid: Bytes32,            // Message unique ID
        message: ByteArray,       // Your payload
        executor: ContractAddress, // Who executed the message
        extra_data: ByteArray,    // Additional data from executor
        value: u256,              // Native tokens forwarded
    ) {
        // 1. Decode your message payload
        let (action, data) = decode_message(@message);

        // 2. Access contract state if needed
        let mut contract = self.get_contract_mut();

        // 3. Execute your logic
        match action {
            Action::Store => {
                contract.data.write(data);
            },
            Action::Execute => {
                // Call other contracts, update state, etc.
            },
        }

        // 4. Emit events for tracking
        contract.emit(MessageReceived {
            guid,
            src_eid: origin.src_eid,
            sender: origin.sender,
        });
    }
}
```

#### Origin Verification

The OAppCore ensures only the Endpoint can call `lz_receive` and that the sender matches the trusted peer:

```rust wrap theme={null}
// Inside OAppCoreComponent::LayerZeroReceiverImpl
fn lz_receive(
    ref self: ComponentState<TContractState>,
    origin: Origin,
    guid: Bytes32,
    message: ByteArray,
    executor: ContractAddress,
    extra_data: ByteArray,
    value: u256,
) {
    // Only the Endpoint can call lz_receive
    self._assert_only_endpoint();

    // Verify peer is set and matches sender
    let expected_peer = self._get_peer_or_revert(origin.src_eid);
    assert_with_byte_array(
        expected_peer == origin.sender,
        err_only_peer(origin.src_eid, origin.sender),
    );

    // Call your _lz_receive implementation (via OAppHooks trait)
    self._lz_receive(origin, guid, message, executor, extra_data, value);
}
```

***

## Events

### Standard OApp Events

```rust wrap theme={null}
// Peer configuration changed (emitted by OAppCoreComponent)
#[derive(Drop, starknet::Event)]
pub struct PeerSet {
    #[key]
    pub eid: u32,
    #[key]
    pub peer: Bytes32,
}
```

<Note>
  **DelegateSet Event**

  The `DelegateSet` event is emitted by the **Endpoint contract** (not the OApp) when `set_delegate` is called. Listen for it on the Endpoint address, not your OApp.
</Note>

### Custom Events

Add your own events for tracking:

```rust wrap theme={null}
#[derive(Drop, starknet::Event)]
pub struct MessageSent {
    #[key]
    pub guid: Bytes32,
    pub dst_eid: u32,
}

#[derive(Drop, starknet::Event)]
pub struct MessageReceived {
    #[key]
    pub guid: Bytes32,
    pub src_eid: u32,
    pub sender: Bytes32,
}
```

***

## Network Addresses

| Network          | Resource           | Address                                                              |
| ---------------- | ------------------ | -------------------------------------------------------------------- |
| Starknet Sepolia | LayerZero Endpoint | `0x0316d70a6e0445a58c486215fac8ead48d3db985acde27efca9130da4c675878` |
| Starknet Sepolia | STRK Token         | `0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d` |
| Starknet Mainnet | LayerZero Endpoint | `0x524e065abff21d225fb7b28f26ec2f48314ace6094bc085f0a7cf1dc2660f68`  |
| Starknet Mainnet | STRK Token         | `0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d` |

***

## Next Steps

* [OFT Overview](/v2/developers/starknet/oft/overview) - Token transfers
* [Protocol Overview](/v2/developers/starknet/protocol-overview) - Message lifecycle
* [Configuration Guide](/v2/developers/starknet/configuration/dvn-executor-config) - DVN setup
* [Troubleshooting](/v2/developers/starknet/troubleshooting/common-errors) - Common errors
