> ## 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 Sui Protocol Implementation

> Overview of Sui Protocol Implementation on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure...

This page provides a deep technical dive into the LayerZero V2 protocol implementation on Sui, documenting the complete message lifecycle with actual contract code, function signatures, and transaction analysis.

**What you'll find**:

* Complete send workflow (7 steps from OApp to MessagingReceipt)
* DVN verification and commit process with storage management
* Executor delivery and OApp receive handling
* Real transaction analysis from mainnet
* Event emissions and monitoring
* Recovery operations (skip, clear, nilify, burn)

**Target audience**: Developers who understand Sui basics and want to deeply understand the protocol implementation.

<Tip>
  ### Prerequisites

  Before reading this page, familiarize yourself with Sui fundamentals in [Technical Overview](/v2/developers/sui/technical-overview). For SDK usage and practical implementation, see [OFT SDK](/v2/developers/sui/oft/sdk) or implementation guides for [OApp](/v2/developers/sui/oapp/overview) and [OFT](/v2/developers/sui/oft/overview).
</Tip>

***

This page documents the complete message lifecycle with contract-level implementation details:

* **Send Workflow:** Message initiation, fee calculation, nonce management, and packet dispatch
* **Verification Workflow:** DVN submission, threshold checking, and verification commitment
* **Receive Workflow:** Executor delivery, payload clearing, and OApp processing

## Send Overview

When a user sends a crosschain message, the following high-level steps occur within a single Programmable Transaction Block (PTB):

1. **OApp Initiates Send:** User calls the OApp module's `send()` function, which creates a `Call<SendParam, MessagingReceipt>` object
2. **Endpoint Processes:** The Endpoint increments the outbound nonce, constructs a packet with GUID, and routes to the send library
3. **ULN302 Assigns Jobs:** The message library creates child `Call` objects for the executor and each DVN
4. **Workers Calculate Fees:** Executor and DVNs estimate their fees and return `FeeRecipient` results
5. **Confirmation Chain:** Results flow back through confirm functions, aggregating fees and emitting events
6. **OApp Finalizes:** The OApp confirms the send call to extract the `MessagingReceipt`

### Endpoint Send

The `EndpointV2` module orchestrates message sending through its shared object.

#### EndpointV2 Shared Object

```rust wrap theme={null}
/// The main endpoint object that coordinates all crosschain messaging operations
public struct EndpointV2 has key {
    id: UID,
    eid: u32,                               // This chain's LayerZero endpoint ID
    call_cap: CallCap,                      // Capability for creating calls
    oapp_registry: OAppRegistry,            // Registry of all registered OApps
    composer_registry: ComposerRegistry,    // Registry for compose handlers
    message_lib_manager: MessageLibManager, // Manages send/receive libraries
}
```

#### MessagingChannel per OApp

Each OApp gets a dedicated `MessagingChannel` [shared object](https://docs.sui.io/concepts/object-ownership/shared) for parallel execution:

```rust wrap theme={null}
/// Shared object managing message channels for a specific OApp
public struct MessagingChannel has key {
    id: UID,
    oapp: address,                        // OApp owner of this channel
    channels: Table<ChannelKey, Channel>, // Maps (eid, remote_oapp) → channel state
    is_sending: bool,                     // Prevents reentrancy
}

/// Composite key identifying a specific channel path
public struct ChannelKey has copy, drop, store {
    remote_eid: u32,      // Destination endpoint ID
    remote_oapp: Bytes32, // Remote OApp address (32 bytes)
}

/// State for a specific channel path
public struct Channel has store {
    outbound_nonce: u64,                          // Next nonce for sends
    lazy_inbound_nonce: u64,                      // Last cleared (executed) nonce
    inbound_payload_hashes: Table<u64, Bytes32>, // Verified messages awaiting execution
}
```

#### Step 1: OApp Creates Send Call

The OApp module creates a `Call` object targeting the Endpoint:

```rust wrap theme={null}
/// From oapp::send()
public fun send(
    self: &mut OApp,
    oapp_cap: &CallCap,         // Proves OApp ownership
    dst_eid: u32,                // Destination endpoint ID
    message: vector<u8>,         // Message payload
    options: vector<u8>,         // Execution options
    native_fee: Coin<SUI>,       // Fee payment in SUI
    lz_token_fee: Option<Coin<ZRO>>, // Optional ZRO payment
    refund_address: address,     // Address for refunds
    ctx: &mut TxContext,
): Call<SendParam, MessagingReceipt> {
    self.assert_oapp_cap(oapp_cap);

    // Lookup peer address for destination
    let receiver = self.peer.get_peer(dst_eid);

    // Combine enforced options with provided options
    let final_options = self.enforced_options.combine_options(dst_eid, SEND_MSG_TYPE, options);

    // Create send parameters
    let send_param = endpoint_send::create_param(
        dst_eid,
        receiver,
        message,
        final_options,
        native_fee,
        lz_token_fee,
        refund_address,
    );

    // Create Call object targeting the Endpoint
    call::create(oapp_cap, endpoint!(), false, send_param, ctx)
}
```

**Key Points**:

* Returns a `Call` object (hot potato—must be consumed)
* The `Call` has no `drop` or `store` abilities
* PTB must route this `Call` to the Endpoint module
* `oapp_cap` validates ownership via ID comparison

#### Step 2: Endpoint Processes Send

The Endpoint receives the `Call`, manages state, and delegates to the send library:

```rust wrap theme={null}
/// From endpoint_v2::send()
public fun send(
    self: &EndpointV2,
    messaging_channel: &mut MessagingChannel,
    call: &mut Call<EndpointSendParam, MessagingReceipt>,
    ctx: &mut TxContext,
): Call<MessageLibSendParam, MessageLibSendResult> {
    // Validate Call came from the OApp that owns this channel
    call.assert_caller(messaging_channel.oapp());

    // Get the configured send library for this destination
    let (send_lib, _) = self.message_lib_manager.get_send_library(
        call.caller(),
        call.param().dst_eid()
    );

    // Create outbound packet with incremented nonce
    // This is where the nonce++ happens:
    let send_param = messaging_channel.send(self.eid(), call.param());

    // Create child Call targeting the send library
    call.create_single_child(&self.call_cap, send_lib, send_param, ctx)
}
```

**Inside `messaging_channel.send()`**:

```rust wrap theme={null}
/// From messaging_channel::send()
public(package) fun send(
    self: &mut MessagingChannel,
    src_eid: u32,
    param: &EndpointSendParam,
): MessageLibSendParam {
    assert!(!self.is_sending, ESendReentrancy);
    self.is_sending = true;

    // Get or create the channel for this destination
    let channel_key = ChannelKey {
        remote_eid: param.dst_eid(),
        remote_oapp: param.receiver()
    };

    if (!self.channels.contains(channel_key)) {
        // Initialize channel if first send
        let channel = Channel {
            outbound_nonce: 0,
            lazy_inbound_nonce: 0,
            inbound_payload_hashes: table::new(ctx),
        };
        self.channels.add(channel_key, channel);
    };

    // Increment nonce
    let channel = &mut self.channels[channel_key];
    channel.outbound_nonce = channel.outbound_nonce + 1;

    // Build packet with GUID
    let packet = outbound_packet::create(
        channel.outbound_nonce,
        src_eid,
        self.oapp,              // Sender address
        param.dst_eid(),
        param.receiver(),
        param.message(),
    );

    // Create send parameters for message library
    message_lib_send::create_param(
        packet,
        param.options(),
        param.pay_in_zro(),
        param.native_fee_ref(),
        param.lz_token_fee_ref(),
    )
}
```

**GUID Generation**:

Uses [keccak256 hashing](https://docs.sui.io/references/framework/sui-framework/hash) with [BCS-encoded](https://github.com/MystenLabs/sui/blob/main/docs/content/concepts/cryptography/system) parameters:

```rust wrap theme={null}
/// From outbound_packet module
public fun create(...): OutboundPacket {
    let guid = hash::keccak256!(
        &vector[
            nonce.to_be_bytes(),      // Convert to bytes (big-endian)
            src_eid.to_be_bytes(),
            sender.to_bytes(),
            dst_eid.to_be_bytes(),
            receiver.data(),
            message,
        ]
    );

    OutboundPacket { nonce, src_eid, sender, dst_eid, receiver, guid, message }
}
```

#### Step 3: ULN302 Assigns Jobs to Workers

The ULN302 message library creates child `Call` objects for the executor and DVNs:

```rust wrap theme={null}
/// From uln_302::send()
public fun send(
    self: &Uln302,
    call: &mut Call<MessageLibSendParam, MessageLibSendResult>,
    ctx: &mut TxContext,
): (Call<ExecutorAssignJobParam, FeeRecipient>, MultiCall<DvnAssignJobParam, FeeRecipient>) {
    call.assert_caller(endpoint!());
    assert!(self.is_supported_eid(call.param().base().packet().dst_eid()), EUnsupportedEid);

    // Get executor and DVN parameters from SendUln
    let (executor, executor_param, dvns, dvn_params) = self.send_uln.send(call.param());

    // Create a new child batch (capacity: 1 executor + N DVNs)
    call.new_child_batch(&self.call_cap, 1);

    // Create child Call for each DVN
    let dvn_calls = dvns.zip_map!(dvn_params, |dvn, param|
        call.create_child(&self.call_cap, dvn, param, false, ctx)
    );

    // Create child Call for executor (marked as last child)
    let executor_call = call.create_child(&self.call_cap, executor, executor_param, true, ctx);

    // Return executor call and MultiCall wrapper for DVN calls
    (executor_call, multi_call::create(&self.call_cap, dvn_calls))
}
```

**Inside `send_uln::send()`**:

```rust wrap theme={null}
/// From send_uln::send() - prepares worker parameters
public(package) fun send(
    self: &SendUln,
    param: &MessageLibSendParam,
): (address, ExecutorAssignJobParam, vector<address>, vector<DvnAssignJobParam>) {
    let packet = param.base().packet();
    let sender = packet.sender();
    let dst_eid = packet.dst_eid();

    // Get effective executor configuration (OApp-specific or default)
    let executor_config = self.get_executor_config(sender, dst_eid);

    // Validate message size
    assert!(packet.message().length() <= executor_config.max_message_size(), EInvalidMessageSize);

    // Split options into executor and DVN options
    let (executor_options, dvn_options) = worker_options::split_worker_options(param.base().options());

    // Create executor job parameters
    let executor_param = executor_assign_job::create_param(
        packet.guid(),
        packet.dst_eid(),
        sender,
        packet.message().length(),
        executor_options,
    );

    // Get effective ULN configuration (OApp-specific or default)
    let uln_config = self.get_uln_config(sender, dst_eid);

    // Encode packet header for DVN verification
    let packet_header = packet_v1_codec::encode_packet_header(packet);
    let payload_hash = packet_v1_codec::payload_hash(packet);

    // Create DVN job parameters for each configured DVN
    let (dvns, dvn_params) = self.create_dvn_params(
        packet.guid(),
        packet.dst_eid(),
        sender,
        packet_header,
        payload_hash,
        uln_config,
        dvn_options,
    );

    (executor_config.executor(), executor_param, dvns, dvn_params)
}
```

#### Step 4: Workers Process Job Assignments

**Executor Assignment**:

```rust wrap theme={null}
/// From executor::assign_job()
public fun assign_job(
    self: &Executor,
    call: &mut Call<AssignJobParam, FeeRecipient>,
    ctx: &mut TxContext,
): Call<FeelibGetFeeParam, u64> {
    // Extract parameters
    let param = *call.param().base();

    // Create child call to fee library for fee calculation
    self.create_feelib_get_fee_call(call, param, ctx)
}

/// Executor confirms fee calculation
public fun confirm_assign_job(
    self: &Executor,
    executor_call: &mut Call<AssignJobParam, FeeRecipient>,
    feelib_call: Call<FeelibGetFeeParam, u64>,
) {
    // Destroy fee library call and extract fee
    let (_, _, fee) = executor_call.destroy_child(self.worker.worker_cap(), feelib_call);

    // Complete executor call with FeeRecipient
    executor_call.complete(
        self.worker.worker_cap(),
        fee_recipient::create(fee, self.worker.deposit_address())
    );
}
```

**DVN Assignment** (similar pattern):

```rust wrap theme={null}
/// From dvn::assign_job()
public fun assign_job(
    self: &DVN,
    call: &mut Call<DvnAssignJobParam, FeeRecipient>,
    ctx: &mut TxContext,
): Call<FeelibGetFeeParam, u64> {
    let param = *call.param().base();
    self.create_feelib_get_fee_call(call, param, ctx)
}
```

#### Step 5: ULN302 Confirms Send

The ULN302 destroys worker `Call` objects and aggregates fees:

```rust wrap theme={null}
/// From uln_302::confirm_send()
public fun confirm_send(
    self: &Uln302,
    endpoint: &EndpointV2,
    treasury: &Treasury,
    messaging_channel: &mut MessagingChannel,
    endpoint_call: &mut Call<EndpointSendParam, MessagingReceipt>,
    mut send_library_call: Call<MessageLibSendParam, MessageLibSendResult>,
    executor_call: Call<ExecutorAssignJobParam, FeeRecipient>,
    dvn_multi_call: MultiCall<DvnAssignJobParam, FeeRecipient>,
    ctx: &mut TxContext,
) {
    send_library_call.assert_caller(endpoint!());

    // Destroy DVN calls and collect fee recipients
    let (mut dvns, mut dvn_recipients) = (vector[], vector[]);
    dvn_multi_call.destroy(&self.call_cap).do!(|dvn_call| {
        let (dvn, _, dvn_recipient) = send_library_call.destroy_child(&self.call_cap, dvn_call);
        dvns.push_back(dvn);
        dvn_recipients.push_back(dvn_recipient);
    });

    // Destroy executor call and collect fee recipient
    let (executor, _, executor_recipient) = send_library_call.destroy_child(&self.call_cap, executor_call);

    // Calculate total fees and encoded packet
    let send_result = send_uln::confirm_send(
        send_library_call.param(),
        executor,
        executor_recipient,
        dvns,
        dvn_recipients,
        treasury,
    );
    send_library_call.complete(&self.call_cap, send_result);

    // Call endpoint for final confirmation
    let (native_token, zro_token) = endpoint.confirm_send(
        &self.call_cap,
        messaging_channel,
        endpoint_call,
        send_library_call,
        ctx,
    );

    // Distribute fees to workers
    send_uln::handle_fees(treasury, executor_recipient, dvn_recipients, native_token, zro_token, ctx);
}
```

**Inside `send_uln::confirm_send()`**:

```rust wrap theme={null}
public(package) fun confirm_send(
    param: &MessageLibSendParam,
    executor: address,
    executor_recipient: FeeRecipient,
    dvns: vector<address>,
    dvn_recipients: vector<FeeRecipient>,
    treasury: &Treasury,
): MessageLibSendResult {
    let packet = param.base().packet();

    // Aggregate worker fees
    let mut native_recipients = vector[executor_recipient];
    native_recipients.append(dvn_recipients);

    let total_native_fee = native_recipients.fold!(0, |acc, r| acc + r.fee());

    // Calculate treasury fee
    let (treasury_recipient, zro_recipient) = treasury.quote_treasury_fee(
        packet.sender(),
        packet.dst_eid(),
        total_native_fee,
        param.base().pay_in_zro()
    );

    if (treasury_recipient.fee() > 0) {
        native_recipients.push_back(treasury_recipient);
    };

    let zro_recipients = if (zro_recipient.fee() > 0) {
        vector[zro_recipient]
    } else {
        vector[]
    };

    // Encode packet for event emission
    let encoded_packet = packet_v1_codec::encode_packet(packet);

    // Emit events
    event::emit(ExecutorFeePaidEvent { guid: packet.guid(), executor, fee: executor_recipient });
    event::emit(DVNFeePaidEvent { guid: packet.guid(), dvns, fees: dvn_recipients });

    // Return result with fee recipients
    message_lib_send::create_result(encoded_packet, native_recipients, zro_recipients)
}
```

#### Step 6: Endpoint Finalizes Send

```rust wrap theme={null}
/// From endpoint_v2::confirm_send()
public fun confirm_send(
    self: &EndpointV2,
    send_library: &CallCap,              // Library's capability (static call)
    messaging_channel: &mut MessagingChannel,
    endpoint_call: &mut Call<EndpointSendParam, MessagingReceipt>,
    send_library_call: Call<MessageLibSendParam, MessageLibSendResult>,
    ctx: &mut TxContext,
): (Coin<SUI>, Coin<ZRO>) {
    messaging_channel.assert_ownership(endpoint_call.caller());

    // Destroy library call and extract results
    let (send_lib, param, result) = endpoint_call.destroy_child(&self.call_cap, send_library_call);
    assert!(send_lib == send_library.id(), EUnauthorizedSendLibrary);

    // Process fee payment and emit events
    let (receipt, paid_native_token, paid_zro_token) = messaging_channel.confirm_send(
        send_lib,
        endpoint_call.param_mut(&self.call_cap),
        param,
        result,
        ctx,
    );

    // Complete the Call with MessagingReceipt
    endpoint_call.complete(&self.call_cap, receipt);

    // Return collected fees for distribution
    (paid_native_token, paid_zro_token)
}
```

**Inside `messaging_channel.confirm_send()`**:

```rust wrap theme={null}
public(package) fun confirm_send(
    self: &mut MessagingChannel,
    send_library: address,
    endpoint_param: &mut EndpointSendParam,
    message_lib_param: MessageLibSendParam,
    result: MessageLibSendResult,
    ctx: &mut TxContext,
): (MessagingReceipt, Coin<SUI>, Coin<ZRO>) {
    // Extract fee recipients
    let (encoded_packet, native_recipients, zro_recipients) = result.destroy();

    // Calculate total fees required
    let total_native_fee = native_recipients.fold!(0, |acc, r| acc + r.fee());
    let total_zro_fee = zro_recipients.fold!(0, |acc, r| acc + r.fee());

    // Split coins from endpoint_param
    let paid_native = coin::split(endpoint_param.native_fee_mut(), total_native_fee, ctx);
    let paid_zro = if (total_zro_fee > 0) {
        coin::split(endpoint_param.lz_token_fee_mut().borrow_mut(), total_zro_fee, ctx)
    } else {
        coin::zero(ctx)
    };

    // Emit PacketSentEvent
    event::emit(PacketSentEvent {
        encoded_packet,
        options: *message_lib_param.base().options(),
        send_library,
    });

    // Create receipt
    let packet = message_lib_param.base().packet();
    let receipt = messaging_receipt::create(packet.guid(), packet.nonce(), total_native_fee, total_zro_fee);

    // Reset sending flag
    self.is_sending = false;

    (receipt, paid_native, paid_zro)
}
```

#### Step 7: OApp Extracts Receipt

The OApp confirms the send call to extract the receipt:

```rust wrap theme={null}
/// From oapp::confirm_lz_send()
public fun confirm_lz_send(
    self: &OApp,
    oapp_cap: &CallCap,
    call: Call<SendParam, MessagingReceipt>,
): (SendParam, MessagingReceipt) {
    self.assert_oapp_cap(oapp_cap);

    // Destroy the Call and extract results
    let (endpoint, param, receipt) = call.destroy(oapp_cap);
    assert!(endpoint == endpoint!(), EOnlyEndpoint);

    (param, receipt)
}
```

### Example PTB for Send (from actual transaction)

Based on transaction `HXZqH1RdANEkstz3MTFGMuLQ74CfgAkwQCq1YW8TMHHH`:

```javascript wrap theme={null}
// PTB commands in order:
1. SplitCoins - Split fee from sender's SUI
2. MoveCall - bytes32::from_bytes (convert recipient to Bytes32)
3. MoveCall - send_param::create (create SendParam struct)
4. MoveCall - oft_sender::tx_sender (create OFTSender context)
5. SplitCoins - Split token amount from sender's coin
6. MoveCall - oft::send (initiate OFT send, returns Call)
7. MoveCall - endpoint::send (route Call to endpoint)
8. MoveCall - uln_302::send (create worker calls)
9. MoveCall - executor::assign_job (executor processes)
10. MoveCall - dvn::assign_job (each DVN processes)
11. MoveCall - executor::confirm_assign_job
12. MoveCall - dvn::confirm_assign_job (for each DVN)
13. MoveCall - uln_302::confirm_send
14. MoveCall - oft::confirm_send (extract receipt)

// Events emitted:
- ExecutorFeePaidEvent
- DVNFeePaidEvent
- PacketSentEvent
- OFTSentEvent
```

### Send Limitations

#### Max Message Size

The `maxMessageSize` is configured per executor and OApp:

```rust wrap theme={null}
/// From ExecutorConfig struct
public struct ExecutorConfig has copy, drop, store {
    executor: address,        // Executor address
    max_message_size: u64,   // Maximum message bytes (default varies by network)
}
```

Default is typically 10,000 bytes, but OApps can configure custom limits.

#### Fee Payment Model

Unlike EVM's direct fee transfer, Sui uses `Coin` object splitting:

```rust wrap theme={null}
// Split exact fee amount from provided coins
let paid_native = coin::split(native_fee_coin, required_fee, ctx);

// Transfer to fee recipient
transfer::public_transfer(paid_native, recipient_address);

// Refund excess
transfer::public_transfer(remaining_coin, refund_address);
```

***

## Verification Workflow

After the `PacketSentEvent` is emitted on the source chain, DVNs independently verify the message on the destination chain.

### DVN Verification Process

#### Step 1: DVN Monitors Source Chain

DVNs watch for `PacketSentEvent` and wait for the configured number of block confirmations (finality).

#### Step 2: DVN Submits Verification

Each DVN calls the `verify()` function on the ULN302:

```rust wrap theme={null}
/// From uln_302::verify()
public fun verify(
    self: &Uln302,
    verification: &mut Verification,  // Shared verification object
    call: Call<DvnVerifyParam, Void>,
) {
    let dvn = call.caller();  // DVN's CallCap proves identity
    let param = call.complete_and_destroy(&self.call_cap);

    // Store verification in the Verification shared object
    receive_uln::verify(
        verification,
        dvn,
        *param.packet_header(),
        param.payload_hash(),
        param.confirmations()
    )
}
```

**Inside `receive_uln::verify()`**:

```rust wrap theme={null}
/// From receive_uln::verify()
public(package) fun verify(
    self: &mut Verification,
    dvn: address,
    packet_header: vector<u8>,
    payload_hash: Bytes32,
    confirmations: u64,
) {
    // Create confirmation key
    let key = ConfirmationKey {
        header_hash: hash::keccak256!(&packet_header),
        payload_hash,
        dvn,
    };

    // Store confirmations in Table
    table_ext::upsert!(&mut self.confirmations, key, confirmations);

    // Emit event for monitoring
    event::emit(PayloadVerifiedEvent {
        dvn,
        header: packet_header,
        confirmations,
        proof_hash: payload_hash,
    });
}
```

**Verification Storage**:

```rust wrap theme={null}
/// Shared object storing all DVN confirmations
public struct Verification has key {
    id: UID,
    // Maps (header_hash, payload_hash, dvn) → confirmation_count
    confirmations: Table<ConfirmationKey, u64>,
}
```

### Commit Verification

After sufficient DVNs have verified (meeting the X of Y of N threshold), anyone can call `commit_verification()`:

```rust wrap theme={null}
/// From uln_302::commit_verification()
public fun commit_verification(
    self: &Uln302,
    endpoint: &EndpointV2,
    verification: &mut Verification,
    messaging_channel: &mut MessagingChannel,
    packet_header: vector<u8>,
    payload_hash: Bytes32,
    clock: &Clock,
) {
    // Verify and reclaim storage from Verification object
    let header = self.receive_uln.verify_and_reclaim_storage(
        verification,
        endpoint.eid(),
        packet_header,
        payload_hash,
    );

    // Call endpoint to insert verified payload hash
    endpoint.verify(
        &self.call_cap,              // Library capability
        messaging_channel,            // Destination OApp's channel
        header.src_eid(),             // Source endpoint ID
        header.sender(),              // Source OApp address
        header.nonce(),               // Message nonce
        payload_hash,                 // Payload hash
        clock,                        // For timeout validation
    );
}
```

**Inside `receive_uln::verify_and_reclaim_storage()`**:

```rust wrap theme={null}
public(package) fun verify_and_reclaim_storage(
    self: &ReceiveUln,
    verification: &mut Verification,
    local_eid: u32,
    encoded_packet_header: vector<u8>,
    payload_hash: Bytes32,
): PacketHeader {
    // Decode and validate packet header
    let header = packet_v1_codec::decode_header(encoded_packet_header);
    assert!(header.dst_eid() == local_eid, EInvalidEid);

    let header_hash = hash::keccak256!(&encoded_packet_header);
    let receiver = header.receiver();
    let src_eid = header.src_eid();

    // Get effective ULN configuration
    let uln_config = self.get_uln_config(receiver, src_eid);

    // Check all required DVNs have verified
    let mut verified_count = 0;
    uln_config.required_dvns().do!(|dvn| {
        let key = ConfirmationKey { header_hash, payload_hash, dvn: *dvn };
        assert!(verification.confirmations.contains(key), EVerifying);

        // Remove confirmation (reclaim storage)
        verification.confirmations.remove(key);
        verified_count = verified_count + 1;
    });

    // Check optional DVN threshold is met
    if (uln_config.optional_dvn_count() > 0) {
        let mut optional_verified = 0;
        uln_config.optional_dvns().do!(|dvn| {
            let key = ConfirmationKey { header_hash, payload_hash, dvn: *dvn };
            if (verification.confirmations.contains(key)) {
                verification.confirmations.remove(key);
                optional_verified = optional_verified + 1;
            };
        });
        assert!(optional_verified >= uln_config.optional_dvn_threshold(), EVerifying);
    };

    header
}
```

### Endpoint Verify

The Endpoint inserts the verified payload hash into the messaging channel:

```rust wrap theme={null}
/// From endpoint_v2::verify()
public fun verify(
    self: &EndpointV2,
    receive_library: &CallCap,          // Library's capability
    messaging_channel: &mut MessagingChannel,
    src_eid: u32,
    sender: Bytes32,
    nonce: u64,
    payload_hash: Bytes32,
    clock: &Clock,
) {
    // Validate receive library is authorized
    self.message_lib_manager.assert_receive_library(
        messaging_channel.oapp(),
        src_eid,
        receive_library.id(),
        clock
    );

    // Insert payload hash into messaging channel
    messaging_channel.verify(src_eid, sender, nonce, payload_hash);
}
```

**Inside `messaging_channel::verify()`**:

```rust wrap theme={null}
public(package) fun verify(
    self: &mut MessagingChannel,
    src_eid: u32,
    sender: Bytes32,
    nonce: u64,
    payload_hash: Bytes32,
) {
    assert!(payload_hash != EMPTY_PAYLOAD_HASH, EInvalidPayloadHash);

    // Get or create channel for this pathway
    let channel_key = ChannelKey { remote_eid: src_eid, remote_oapp: sender };
    if (!self.channels.contains(channel_key)) {
        self.init_channel(src_eid, sender);
    };

    // Insert payload hash into the channel
    let channel = &mut self.channels[channel_key];
    channel.inbound_payload_hashes.add(nonce, payload_hash);

    // Emit verification event
    event::emit(PacketVerifiedEvent {
        src_eid,
        sender,
        nonce,
        receiver: self.oapp,
        payload_hash,
    });
}
```

**Message State Transition**:

```
Send → PacketSentEvent emitted on source chain
    ↓
DVNs monitor and verify (off-chain)
    ↓
DVNs call verify() (onchain submission)
    ↓
Verification confirmations stored in Verification object
    ↓
commit_verification() checks threshold
    ↓
Payload hash inserted into MessagingChannel
    ↓
Message ready for execution
```

***

## Receive Workflow

After verification is committed, the Executor can deliver the message to the destination OApp.

### Executor Delivery

The Executor initiates message delivery by constructing a PTB with all required objects.

#### Step 1: Executor Queries OApp Metadata

The Executor queries the OApp's execution metadata to determine which objects are needed:

```rust wrap theme={null}
// OApp implements this to provide execution metadata
public fun get_oapp_info(oapp: &OApp): vector<u8> {
    // Returns encoded OAppInfoV1 containing required Move calls
}
```

#### Step 2: Executor Creates PTB

Based on the transaction `9fqmkJYFQyQs6u1vVmMSuqhZyobpSW7P4i7MaNVzbSFg`, the PTB contains:

```javascript wrap theme={null}
1. MoveCall - bytes32::from_bytes (decode sender)
2. MoveCall - bytes32::from_bytes (decode receiver)
3. MoveCall - option::none<Coin<SUI>> (no value transfer)
4. MoveCall - executor_worker::execute_lz_receive (entry point)
5. MoveCall - counter::lz_receive (OApp business logic)

Objects passed:
- Executor shared object (immutable reference)
- Executor capability (owned object)
- EndpointV2 shared object (immutable reference)
- MessagingChannel shared object (mutable reference)
- Clock object (for validation)
- Counter OApp shared object (mutable reference)
- Counter Peer object (immutable reference)
```

#### Step 3: Executor Calls execute\_lz\_receive

```rust wrap theme={null}
/// From executor::execute_lz_receive()
public fun execute_lz_receive(
    self: &Executor,
    endpoint: &EndpointV2,
    messaging_channel: &mut MessagingChannel,
    src_eid: u32,
    sender: Bytes32,
    nonce: u64,
    guid: Bytes32,
    message: vector<u8>,
    extra_data: vector<u8>,
    value: Option<Coin<SUI>>,
    ctx: &mut TxContext,
): Call<LzReceiveParam, Void> {
    // Create lz_receive call via endpoint
    endpoint.lz_receive(
        &self.worker.worker_cap(),  // Executor's capability
        messaging_channel,
        src_eid,
        sender,
        nonce,
        guid,
        message,
        extra_data,
        value,
        ctx,
    )
}
```

#### Step 4: Endpoint Creates lz\_receive Call

```rust wrap theme={null}
/// From endpoint_v2::lz_receive()
public fun lz_receive(
    self: &EndpointV2,
    executor: &CallCap,              // Executor's capability
    messaging_channel: &mut MessagingChannel,
    src_eid: u32,
    sender: Bytes32,
    nonce: u64,
    guid: Bytes32,
    message: vector<u8>,
    extra_data: vector<u8>,
    value: Option<Coin<SUI>>,
    ctx: &mut TxContext,
): Call<LzReceiveParam, Void> {
    // Clear the payload first (prevents reentrancy)
    messaging_channel.clear(src_eid, sender, nonce, guid, &message);

    // Create lz_receive parameters
    let lz_receive_param = lz_receive::create_param(
        src_eid,
        sender,
        nonce,
        guid,
        message,
        extra_data,
        value,
    );

    // Create Call object targeting the OApp
    call::create(
        executor,                      // Executor creates the Call
        messaging_channel.oapp(),      // Target: OApp address
        true,                          // One-way call (no result expected)
        lz_receive_param,
        ctx,
    )
}
```

**Inside `messaging_channel::clear()`**:

```rust wrap theme={null}
public(package) fun clear(
    self: &mut MessagingChannel,
    src_eid: u32,
    sender: Bytes32,
    nonce: u64,
    guid: Bytes32,
    message: &vector<u8>,
) {
    let channel_key = ChannelKey { remote_eid: src_eid, remote_oapp: sender };
    let channel = &mut self.channels[channel_key];

    // Lazy nonce update: clear all messages up to this nonce
    if (nonce > channel.lazy_inbound_nonce) {
        let mut i = channel.lazy_inbound_nonce + 1;
        while (i <= nonce) {
            assert!(channel.inbound_payload_hashes.contains(i), EInvalidNonce);
            i = i + 1;
        };
        channel.lazy_inbound_nonce = nonce;
    };

    // Verify payload hash matches verified hash
    let expected_hash = channel.inbound_payload_hashes[nonce];
    let actual_hash = hash::keccak256!(&vector[guid.data(), *message]);
    assert!(expected_hash == actual_hash, EPayloadHashNotFound);

    // Remove from storage (prevents double execution)
    channel.inbound_payload_hashes.remove(nonce);

    // Emit delivery event
    event::emit(PacketDeliveredEvent {
        src_eid,
        sender,
        receiver: self.oapp,
        nonce,
    });
}
```

**Key Security Features**:

1. **Lazy nonce validation**: Ensures all prior messages have been verified
2. **Payload hash verification**: Confirms executor provided the correct message
3. **Storage cleanup**: Removes hash to prevent double execution
4. **Event emission**: Signals successful delivery

#### Step 5: OApp Processes Message

The OApp's `lz_receive()` function is invoked via the `Call` object:

```rust wrap theme={null}
/// Example from counter OApp
public fun lz_receive(
    self: &mut Counter,
    peer: &Peer,
    call: Call<LzReceiveParam, Void>,
) {
    // Validate Call came from Endpoint
    let (callee, param, _) = call.complete_and_destroy(&self.call_cap);
    assert!(callee == endpoint_address(), EOnlyEndpoint);

    // Validate sender is configured peer
    assert!(param.sender() == peer.address, EOnlyPeer);

    // Process message (application-specific logic)
    self.count = self.count + 1;

    // Note: No need to manually call clear() - already done by Endpoint
}
```

**OApp Responsibilities**:

* * Validate `Call` came from authorized Endpoint
* * Validate sender matches configured peer
* * Process message and update state
* * No need to call `clear()` (done by Endpoint before Call creation)

### Example PTB for Receive (from actual transaction)

Based on transaction `9fqmkJYFQyQs6u1vVmMSuqhZyobpSW7P4i7MaNVzbSFg`:

```javascript wrap theme={null}
// PTB commands in order:
1. MoveCall - bytes32::from_bytes (decode sender parameter)
2. MoveCall - bytes32::from_bytes (decode guid parameter)
3. MoveCall - option::none<Coin<SUI>> (no native token transfer)
4. MoveCall - executor_worker::execute_lz_receive
   - Passes: Executor, Endpoint, MessagingChannel, src_eid, sender, nonce, guid, message
   - Returns: Call<LzReceiveParam, Void>
5. MoveCall - counter::lz_receive
   - Receives the Call object
   - Validates and processes
   - Destroys the Call

Objects used:
- 0x5f24...0c8e: Executor shared object (immutable)
- 0x00a7...9fc2: Executor CallCap (owned)
- 0xd45b...bf91: EndpointV2 shared object (immutable)
- 0x9b01...1843: MessagingChannel shared object (mutable)
- 0x6903...8c4d: Counter OApp shared object (mutable)
- 0x224b...fbb3: Counter Peer shared object (immutable)
- 0x608a...6f27: Counter's internal state (mutable)

// Events emitted:
- PacketDeliveredEvent
```

***

## Key Sui-Specific Patterns

### Object Ownership in Message Flow

| Object Type        | Ownership | Access Pattern             | Example                            |
| ------------------ | --------- | -------------------------- | ---------------------------------- |
| `EndpointV2`       | Shared    | Anyone reads, admin writes | `&EndpointV2` or `&mut EndpointV2` |
| `MessagingChannel` | Shared    | Anyone reads, owner writes | `&mut MessagingChannel`            |
| `OApp`             | Shared    | Anyone reads, admin writes | `&mut OApp`                        |
| `CallCap`          | Owned     | Must own to use            | Owned by OApp module or user       |
| `AdminCap`         | Owned     | Must own to use            | Owned by admin address             |
| `Call<P, R>`       | Neither   | Must be consumed in PTB    | Created and destroyed in same TX   |

### Call Pattern vs EVM/Solana

| Aspect                   | EVM                   | Solana                         | Sui                               |
| ------------------------ | --------------------- | ------------------------------ | --------------------------------- |
| **Cross-Contract Calls** | `delegatecall`        | CPI (Cross-Program Invocation) | `Call<Param, Result>` objects     |
| **Authorization**        | `msg.sender`          | Signer checks + PDAs           | `CallCap` validation              |
| **Return Values**        | Function returns      | CPI returns                    | `Call.complete()` sets result     |
| **Call Hierarchy**       | Call stack (implicit) | CPI depth limit (4)            | `Call` parent/child relationships |
| **Atomicity**            | Transaction revert    | Transaction revert             | PTB revert                        |

### Nonce Management

**EVM**:

```solidity wrap theme={null}
// Mapping-based nonce storage
mapping(address => mapping(uint32 => mapping(bytes32 => uint64))) outboundNonce;
```

**Solana**:

```rust wrap theme={null}
// PDA account per pathway
#[account(seeds = [NONCE_SEED, sender, dst_eid, receiver], bump)]
pub nonce: Account<'info, Nonce>,
```

**Sui**:

```rust wrap theme={null}
// Table within MessagingChannel, nested in Channel struct
public struct MessagingChannel has key {
    channels: Table<ChannelKey, Channel>,  // Maps (eid, remote_oapp) → Channel
}

public struct Channel has store {
    outbound_nonce: u64,           // Incremented on each send
    lazy_inbound_nonce: u64,       // Last executed inbound nonce
    inbound_payload_hashes: Table<u64, Bytes32>,  // Maps nonce → hash
}
```

### Fee Payment Model

**EVM**:

```solidity wrap theme={null}
// Direct transfer in msg.value
Transfer.native(executor, executorFee);
```

**Solana**:

```rust wrap theme={null}
// Token account transfer
transfer(from_account, to_account, amount);
```

**Sui**:

```rust wrap theme={null}
// Coin object splitting and transfer
let fee_coin = coin::split(&mut provided_coin, fee_amount, ctx);
transfer::public_transfer(fee_coin, recipient_address);
```

***

## Configuration Management

### Send Library Configuration

OApps can set custom send libraries per destination:

```rust wrap theme={null}
/// From endpoint_v2 (called by OApp with AdminCap)
public fun set_send_library(
    self: &mut EndpointV2,
    caller: &CallCap,
    oapp: address,
    dst_eid: u32,
    new_lib: address,
) {
    self.assert_authorized(caller.id(), oapp);
    self.message_lib_manager.set_send_library(oapp, dst_eid, new_lib);
}
```

**Default Fallback**:

```rust wrap theme={null}
/// From message_lib_manager
public(package) fun get_send_library(
    self: &MessageLibManager,
    sender: address,
    dst_eid: u32,
): (address, bool) {
    // Try OApp-specific config first
    let key = SendLibraryKey { sender, dst_eid };
    if (self.send_libraries.contains(key)) {
        return (self.send_libraries[key], false)  // Custom library
    };

    // Fall back to default
    let default_lib = self.default_send_libraries[dst_eid];
    (default_lib, true)  // Default library
}
```

### DVN Configuration

OApps configure DVN sets through the ULN:

```rust wrap theme={null}
/// ULN configuration structure
public struct UlnConfig has copy, drop, store {
    confirmations: u64,                 // Block confirmations required
    required_dvn_count: u8,             // Number of required DVNs
    optional_dvn_count: u8,             // Number of optional DVNs
    optional_dvn_threshold: u8,         // How many optional DVNs must verify
    required_dvns: vector<address>,     // Required DVN addresses
    optional_dvns: vector<address>,     // Optional DVN addresses
}
```

**Setting Configuration** (via Endpoint):

```rust wrap theme={null}
/// From endpoint_v2::set_config()
public fun set_config(
    self: &mut EndpointV2,
    caller: &CallCap,                  // OApp or delegate
    oapp: address,
    config_type: u32,
    eid: u32,
    config: vector<u8>,
    ctx: &mut TxContext,
) {
    self.assert_authorized(caller.id(), oapp);

    // Get message library
    let (lib, _) = if (is_send_config(config_type)) {
        self.message_lib_manager.get_send_library(oapp, eid)
    } else {
        self.message_lib_manager.get_receive_library(oapp, eid, clock)
    };

    // Create Call to library's set_config
    let set_config_param = message_lib_set_config::create_param(oapp, config_type, eid, config);
    let call = call::create(caller, lib, true, set_config_param, ctx);

    // Library processes configuration immediately (one-way call)
    // No confirmation needed
}
```

***

## Recovery Operations

The Endpoint provides several recovery mechanisms for stuck or problematic messages.

### Skip

Increments the lazy nonce without executing the message:

```rust wrap theme={null}
/// From endpoint_v2::skip()
public fun skip(
    self: &EndpointV2,
    caller: &CallCap,
    messaging_channel: &mut MessagingChannel,
    src_eid: u32,
    sender: Bytes32,
    nonce: u64,
) {
    self.assert_authorized(caller.id(), messaging_channel.oapp());
    messaging_channel.skip(src_eid, sender, nonce);
}
```

**Inside `messaging_channel::skip()`**:

```rust wrap theme={null}
public(package) fun skip(
    self: &mut MessagingChannel,
    src_eid: u32,
    sender: Bytes32,
    nonce: u64,
) {
    let channel_key = ChannelKey { remote_eid: src_eid, remote_oapp: sender };
    let channel = &mut self.channels[channel_key];

    // Validate nonce is next expected
    assert!(nonce == channel.lazy_inbound_nonce + 1, EInvalidNonce);

    // Increment lazy nonce (skipping this message)
    channel.lazy_inbound_nonce = nonce;

    event::emit(InboundNonceSkippedEvent {
        src_eid,
        sender,
        receiver: self.oapp,
        nonce,
    });
}
```

### Nilify

Removes verification but keeps nonce ordering:

```rust wrap theme={null}
public fun nilify(
    self: &EndpointV2,
    caller: &CallCap,
    messaging_channel: &mut MessagingChannel,
    src_eid: u32,
    sender: Bytes32,
    nonce: u64,
    payload_hash: Bytes32,
) {
    self.assert_authorized(caller.id(), messaging_channel.oapp());
    messaging_channel.nilify(src_eid, sender, nonce, payload_hash);
}
```

### Burn

Permanently blocks a nonce (irreversible):

```rust wrap theme={null}
public fun burn(
    self: &EndpointV2,
    caller: &CallCap,
    messaging_channel: &mut MessagingChannel,
    src_eid: u32,
    sender: Bytes32,
    nonce: u64,
    payload_hash: Bytes32,
) {
    self.assert_authorized(caller.id(), messaging_channel.oapp());
    messaging_channel.burn(src_eid, sender, nonce, payload_hash);
}
```

***

## Comparison with EVM and Solana

### Architecture Comparison

| Component             | EVM                    | Solana                   | Sui                            |
| --------------------- | ---------------------- | ------------------------ | ------------------------------ |
| **Code Organization** | Solidity contracts     | Rust programs            | Move packages/modules          |
| **State Storage**     | Contract storage slots | PDA accounts             | Shared/owned objects           |
| **Nonce Management**  | Nested mappings        | PDA per pathway          | Table in MessagingChannel      |
| **Message Channel**   | Contract storage       | PDA accounts             | MessagingChannel shared object |
| **Call Pattern**      | delegatecall           | CPI                      | `Call<Param, Result>` objects  |
| **Authorization**     | msg.sender             | Signers + PDA derivation | CallCap validation             |
| **Fee Payment**       | msg.value transfer     | Token account ops        | Coin object splitting          |
| **Atomicity**         | Transaction revert     | Transaction revert       | PTB revert                     |

### Send Flow Comparison

| Step               | EVM                   | Solana                  | Sui                       |
| ------------------ | --------------------- | ----------------------- | ------------------------- |
| **Initiate**       | OApp.send() internal  | OApp CPI to Endpoint    | OApp creates Call object  |
| **Nonce**          | Mapping increment     | PDA account write       | Table field increment     |
| **Library Call**   | Direct function call  | CPI to SendUln302       | Child Call creation       |
| **Worker Calls**   | Direct function calls | CPI to each worker      | Child Call per worker     |
| **Fee Collection** | Transfer to library   | Record in library       | Coin splitting            |
| **Event**          | `emit PacketSent`     | `emit_cpi!(PacketSent)` | `event::emit(PacketSent)` |

### Receive Flow Comparison

| Step                | EVM                                 | Solana                             | Sui                                  |
| ------------------- | ----------------------------------- | ---------------------------------- | ------------------------------------ |
| **Entry Point**     | Executor calls Endpoint.lzReceive   | Executor invokes with all accounts | Executor calls execute\_lz\_receive  |
| **Clear Payload**   | Endpoint clears before calling OApp | OApp CPIs back to Endpoint.clear   | Endpoint clears before creating Call |
| **OApp Invocation** | delegatecall to OApp.lzReceive      | Instruction with account list      | Call object to OApp module           |
| **Validation**      | Modifier checks                     | Account constraints + CPI auth     | Call validation + peer check         |
| **Processing**      | Override \_lzReceive                | Implement lz\_receive instruction  | Implement lz\_receive function       |

***

## Event Monitoring

### Events Emitted During Send

1. **ExecutorFeePaidEvent** (from ULN):

```rust wrap theme={null}
public struct ExecutorFeePaidEvent has copy, drop {
    guid: Bytes32,
    executor: address,
    fee: FeeRecipient,
}
```

2. **DVNFeePaidEvent** (from ULN):

```rust wrap theme={null}
public struct DVNFeePaidEvent has copy, drop {
    guid: Bytes32,
    dvns: vector<address>,
    fees: vector<FeeRecipient>,
}
```

3. **PacketSentEvent** (from MessagingChannel):

```rust wrap theme={null}
public struct PacketSentEvent has copy, drop {
    encoded_packet: vector<u8>,  // Full packet with header + payload
    options: vector<u8>,          // Execution options
    send_library: address,        // Library that processed send
}
```

4. **OFTSentEvent** (from OFT, if applicable):

```rust wrap theme={null}
public struct OFTSentEvent has copy, drop {
    guid: Bytes32,
    dst_eid: u32,
    from_address: address,
    amount_sent_ld: u64,        // Amount in local decimals
    amount_received_ld: u64,    // Amount after dust removal
}
```

### Events Emitted During Verification

1. **PayloadVerifiedEvent** (per DVN):

```rust wrap theme={null}
public struct PayloadVerifiedEvent has copy, drop {
    dvn: address,
    header: vector<u8>,
    confirmations: u64,
    proof_hash: Bytes32,
}
```

2. **PacketVerifiedEvent** (after commit):

```rust wrap theme={null}
public struct PacketVerifiedEvent has copy, drop {
    src_eid: u32,
    sender: Bytes32,
    nonce: u64,
    receiver: address,
    payload_hash: Bytes32,
}
```

### Events Emitted During Receive

1. **PacketDeliveredEvent**:

```rust wrap theme={null}
public struct PacketDeliveredEvent has copy, drop {
    src_eid: u32,
    sender: Bytes32,
    receiver: address,
    nonce: u64,
}
```

2. **OFTReceivedEvent** (from OFT, if applicable):

```rust wrap theme={null}
public struct OFTReceivedEvent has copy, drop {
    guid: Bytes32,
    src_eid: u32,
    to_address: address,
    amount_received_ld: u64,
}
```

***

## Capabilities and Authorization

### CallCap Pattern

`CallCap` is Sui's capability-based authorization for creating `Call` objects:

```rust wrap theme={null}
/// From call_cap module
public struct CallCap has key, store {
    id: UID,
    package_id: address,  // Package that owns this capability
}

/// Create a package-level CallCap using one-time witness
public fun new_package_cap<T: drop>(otw: &T, ctx: &mut TxContext): CallCap {
    CallCap {
        id: object::new(ctx),
        package_id: package::from_witness(otw),
    }
}
```

**Usage in Validation**:

```rust wrap theme={null}
/// OApp validates its CallCap
fun assert_oapp_cap(self: &OApp, cap: &CallCap) {
    assert!(self.oapp_cap.id() == cap.id(), EInvalidOAppCap);
}

/// Endpoint validates library CallCap
fun assert_send_library(lib_cap: &CallCap, expected_lib: address) {
    assert!(lib_cap.id() == expected_lib, EUnauthorizedSendLibrary);
}
```

### AdminCap Pattern

`AdminCap` authorizes administrative operations:

```rust wrap theme={null}
public struct AdminCap has key, store {
    id: UID,
}

/// Setting a peer requires AdminCap
public fun set_peer(
    self: &mut OApp,
    admin_cap: &AdminCap,  // Must own this object
    eid: u32,
    peer: Bytes32,
) {
    // AdminCap ownership proves authorization
    self.peer.set_peer(self.oapp_object_address(), eid, peer);
}
```

**Ownership Transfer**:

```rust wrap theme={null}
// Transfer AdminCap to new admin
transfer::public_transfer(admin_cap, new_admin_address);
```

***

## PTB Construction Patterns

### Simple Send PTB

```typescript wrap theme={null}
const tx = new Transaction();

// 1. Split fee from gas coin
const [feeCoin] = tx.splitCoins(tx.gas, [tx.pure.u64(feeAmount)]);

// 2. Create send parameters
const sendParam = tx.moveCall({
  target: `${oappPackage}::oapp::create_send_param`,
  arguments: [
    tx.pure.u32(dstEid),
    tx.pure.vector('u8', receiverBytes),
    tx.pure.vector('u8', messageBytes),
    tx.pure.vector('u8', optionsBytes),
    feeCoin,
    tx.pure.option('object', null), // No ZRO payment
    tx.pure.address(refundAddress),
  ],
});

// 3. Call OApp send (returns Call object)
const sendCall = tx.moveCall({
  target: `${oappPackage}::oapp::send`,
  arguments: [tx.object(oappObjectId), tx.object(callCapObjectId), sendParam],
});

// 4. Route through Endpoint (processes Call)
const libCall = tx.moveCall({
  target: `${endpointPackage}::endpoint_v2::send`,
  arguments: [tx.object(endpointObjectId), tx.object(messagingChannelId), sendCall],
});

// 5-N. Worker assignments (handled by PTB builder)

// N+1. Confirm send (destroys Call, extracts receipt)
tx.moveCall({
  target: `${oappPackage}::oapp::confirm_lz_send`,
  arguments: [
    tx.object(oappObjectId),
    tx.object(callCapObjectId),
    sendCall, // Original Call object (now completed)
  ],
});

await client.signAndExecuteTransaction({transaction: tx});
```

### Receive PTB

```typescript wrap theme={null}
const tx = new Transaction();

// 1. Decode parameters
const senderBytes32 = tx.moveCall({
  target: `${utilsPackage}::bytes32::from_bytes`,
  arguments: [tx.pure.vector('u8', senderBytes)],
});

const guidBytes32 = tx.moveCall({
  target: `${utilsPackage}::bytes32::from_bytes`,
  arguments: [tx.pure.vector('u8', guidBytes)],
});

// 2. Create empty value option (no native transfer)
const noValue = tx.moveCall({
  target: '0x1::option::none',
  typeArguments: ['0x2::coin::Coin<0x2::sui::SUI>'],
  arguments: [],
});

// 3. Execute lz_receive via Executor
const lzReceiveCall = tx.moveCall({
  target: `${executorPackage}::executor_worker::execute_lz_receive`,
  arguments: [
    tx.object(executorObjectId), // Executor shared object
    tx.object(executorCapId), // Executor CallCap
    tx.object(endpointObjectId), // Endpoint shared object
    tx.object(messagingChannelId), // Messaging channel
    tx.pure.u32(srcEid),
    senderBytes32,
    tx.pure.u64(nonce),
    guidBytes32,
    tx.pure.vector('u8', messageBytes),
    tx.pure.vector('u8', extraDataBytes),
    noValue,
  ],
});

// 4. OApp processes (receives Call object from step 3)
tx.moveCall({
  target: `${oappPackage}::counter::lz_receive`,
  arguments: [
    tx.object(counterObjectId), // OApp shared object
    tx.object(peerObjectId), // Peer validation
    lzReceiveCall, // Call from executor
  ],
});

await client.signAndExecuteTransaction({transaction: tx});
```

***

## Sui-Specific Considerations

### Object Abilities

Sui's [ability system](https://move-book.com/advanced-topics/abilities.html) controls what can be done with types:

| Ability | Meaning                        | LayerZero Usage                    |
| ------- | ------------------------------ | ---------------------------------- |
| `key`   | Can be stored at top-level     | `OApp`, `EndpointV2`, `AdminCap`   |
| `store` | Can be stored in other structs | `Peer`, `Channel`, `UlnConfig`     |
| `copy`  | Can be copied                  | `ChannelKey`, `MessagingFee`       |
| `drop`  | Can be ignored/discarded       | One-time witnesses, config structs |

**Call Object Abilities**:

```rust wrap theme={null}
public struct Call<Param, Result> {
    // Has NO abilities - cannot be dropped or stored
    // Must be explicitly destroyed via destroy() or complete_and_destroy()
}
```

This enforces the hot potato pattern—`Call` objects must be handled.

### Phantom Type Parameters

Sui uses [phantom type parameters](https://move-book.com/move-basics/generics/#phantom-type-parameters) for type safety without storage:

```rust wrap theme={null}
/// OFT uses phantom T for the coin type
public struct OFT<phantom T> has key {
    id: UID,
    treasury: OFTTreasury<T>,  // T only appears in nested types
    // ...
}

/// TreasuryCap also uses phantom T
public struct TreasuryCap<phantom T> has key, store {
    id: UID,
    total_supply: Supply<T>,
}
```

The `phantom` keyword means `T` is for type safety only—not stored directly.

### Table vs Vector

Sui uses [`Table<K, V>`](https://docs.sui.io/references/framework/sui-framework/table) for dynamic key-value storage:

```rust wrap theme={null}
/// Peer mappings by EID
public struct Peer has store {
    peers: Table<u32, Bytes32>,  // EID → peer address
}

/// vs fixed-size vector
required_dvns: vector<address>,  // Known size, stored directly
```

**Trade-offs**:

* `Table`: Dynamic size, gas per access, better for sparse data
* `vector`: Fixed size, cheaper access, better for dense data

***

## Summary

LayerZero on Sui achieves crosschain messaging through:

1. **Object-Based State**: Shared objects (`EndpointV2`, `MessagingChannel`, `OApp`) enable parallel execution
2. **Capability Authorization**: `CallCap` and `AdminCap` replace `msg.sender` checks
3. **Call Pattern**: `Call<Param, Result>` objects enable dynamic routing without `delegatecall`
4. **PTB Composition**: Atomic multi-step workflows ensure message integrity
5. **Type Safety**: Move's ability system and phantom types provide compile-time guarantees

**Key Differences from Other VMs**:

* No inheritance (explicit capability validation)
* No dynamic dispatch (Call pattern workaround)
* Object ownership model (shared vs owned vs immutable)
* Coin object model (split/merge instead of balance transfer)
* Table-based storage (not mappings or PDAs)

For implementation guides and code examples, see:

* [OApp Implementation](/v2/developers/sui/oapp/overview) - Build custom crosschain applications
* [OFT Implementation](/v2/developers/sui/oft/overview) - Deploy crosschain tokens
* [OFT SDK](/v2/developers/sui/oft/sdk) - Complete SDK methods and patterns
* [Configuration Guide](/v2/developers/sui/configuration/dvn-executor-config) - DVN, executor, and gas configuration
* [Technical Overview](/v2/developers/sui/technical-overview) - Sui fundamentals and architecture
