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

# Sui OFT

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

The **Omnichain Fungible Token (OFT) Standard** allows fungible tokens to be transferred across multiple blockchains without asset wrapping or middlechains. Read more on OFTs in our glossary page: [OFT](/v2/concepts/applications/oft-standard).

## What is an OFT on Sui?

An OFT on Sui is a [Move package](https://docs.sui.io/concepts/sui-move-concepts/packages) that extends the OApp functionality to enable crosschain token transfers. It integrates with Sui's native [coin type system](https://docs.sui.io/standards/coin) ([`Coin<T>`](https://docs.sui.io/references/framework/sui-framework/coin), [`Balance<T>`](https://docs.sui.io/references/framework/sui-framework/balance), `TreasuryCap<T>`) while providing LayerZero's omnichain capabilities.

This guide will walk you through deploying an OFT on Sui. To understand how OFTs integrate with Sui's coin system and the differences between mint/burn and lock/unlock token management strategies, see [Integration with Sui Coin System](#integration-with-sui-coin-system).

## Deployment

OFT deployment on Sui uses a **two-package pattern**: your token + pure [LayerZero OFT source](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/sui/contracts/oapps/oft/oft).

<Info>
  ### Mint/Burn Example

  This deployment guide demonstrates the **mint/burn** approach, where you provide the `TreasuryCap` and the OFT mints/burns tokens during crosschain transfers. This works for both new tokens and existing tokens where you control the `TreasuryCap`.

  If you DON'T have the `TreasuryCap` (frozen, held by DAO, etc.), use the **lock/unlock** (adapter) approach instead. See [Choosing Mint/Burn vs Lock/Unlock](#choosing-mintburn-vs-lockunlock) for details.
</Info>

**Prerequisites**:

* Sui CLI installed (via [suiup](https://github.com/MystenLabs/suiup))
* Node.js and npm for TypeScript SDK
* 1-2 SUI for gas fees

<Tip>
  ### New to Sui?

  If you haven't used Sui before, start with [Getting Started with Sui](/v2/developers/sui/getting-started) to understand the object model, package structure, and development basics.
</Tip>

### Step 1: Create and Deploy Your Token

**Create token package**:

```bash wrap theme={null}
mkdir my-token
cd my-token
sui move new myoft
```

**Implement token** (`sources/myoft.move`):

```rust wrap theme={null}
module myoft::myoft;

use sui::coin;

/// One-time witness for coin creation
/// Must be named same as module (MYOFT) and have only `drop` ability
public struct MYOFT has drop {}

/// Initialize the coin on package publish
fun init(otw: MYOFT, ctx: &mut TxContext) {
    // Create the coin with metadata
    let (treasury_cap, coin_metadata) = coin::create_currency(
        otw,                           // One-time witness
        6,                             // decimals (6 for crosschain compatibility)
        b"MYOFT",                      // symbol
        b"My Omnichain Fungible Token", // name
        b"A LayerZero OFT on Sui with mint/burn capabilities", // description
        option::none(),                // icon_url (optional)
        ctx
    );

    // Freeze the metadata object (makes it immutable and shared)
    transfer::public_freeze_object(coin_metadata);

    // Transfer treasury cap to deployer
    transfer::public_transfer(treasury_cap, ctx.sender());
}

// That's it! No OFT logic in token package
```

**Deploy your token**:

```bash wrap theme={null}
# From your token directory
sui client publish --gas-budget 500000000 --json > token_deploy.json

# Extract IDs
TOKEN_PACKAGE=$(jq -r '.objectChanges[] | select(.type=="published") | .packageId' token_deploy.json)
TREASURY_CAP=$(jq -r '.objectChanges[] | select(.objectType | contains("TreasuryCap")) | .objectId' token_deploy.json)
COIN_METADATA=$(jq -r '.objectChanges[] | select(.objectType | contains("CoinMetadata")) | .objectId' token_deploy.json)

echo "Token Package: $TOKEN_PACKAGE"
echo "Treasury Cap: $TREASURY_CAP"
echo "Coin Metadata: $COIN_METADATA"
```

### Step 2: Deploy LayerZero OFT Package

Deploy the pure [LayerZero OFT source](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/sui/contracts/oapps/oft/oft) without modifications.

**Recommended approach** (git dependencies):

```bash wrap theme={null}
# Copy OFT source to your project
mkdir oft
cd oft
```

**Create `Move.toml`** with git dependencies:

```toml wrap theme={null}
[package]
name = "OFT"
version = "0.0.1"
edition = "2024.beta"
license = "MIT"

[dependencies]
OApp = { git = "https://github.com/LayerZero-Labs/LayerZero-v2.git", subdir = "packages/layerzero-v2/sui/contracts/oapps/oapp", rev = "main" }
OFTCommon = { git = "https://github.com/LayerZero-Labs/LayerZero-v2.git", subdir = "packages/layerzero-v2/sui/contracts/oapps/oft/oft-common", rev = "main" }
PtbMoveCall = { git = "https://github.com/LayerZero-Labs/LayerZero-v2.git", subdir = "packages/layerzero-v2/sui/contracts/ptb-builders/ptb-move-call", rev = "main" }

[addresses]
oft = "0x0"

[dev-dependencies]
SimpleMessageLib = { git = "https://github.com/LayerZero-Labs/LayerZero-v2.git", subdir = "packages/layerzero-v2/sui/contracts/message-libs/simple-message-lib", rev = "main" }
```

**Copy OFT source files**:

```bash wrap theme={null}
# Clone LayerZero repository (temporary, just to copy sources)
git clone https://github.com/LayerZero-Labs/LayerZero-v2.git --depth 1
cp -r LayerZero-v2/packages/layerzero-v2/sui/contracts/oapps/oft/oft/sources ./
rm -rf LayerZero-v2
```

**Deploy** (dependencies auto-fetched from GitHub):

```bash wrap theme={null}
# From your OFT directory
sui client publish --gas-budget 1000000000 --json > oft_deploy.json

# Extract the package ID (you'll need this for peer configuration!)
OFT_PACKAGE=$(jq -r '.objectChanges[] | select(.type=="published") | .packageId' oft_deploy.json)
OAPP_OBJECT=$(jq -r '.objectChanges[] | select(.objectType | contains("::oapp::OApp")) | select(.owner.Shared) | .objectId' oft_deploy.json)
INIT_TICKET=$(jq -r '.objectChanges[] | select(.objectType | contains("OFTInitTicket")) | .objectId' oft_deploy.json)

echo "OFT Package: $OFT_PACKAGE"  # ← SAVE THIS! Use as peer on remote chains
echo "OApp Object: $OAPP_OBJECT"
echo "Init Ticket: $INIT_TICKET"
```

This automatically creates an `OFTInitTicket` (via `oft_impl::init()`).

<Warning>
  ### Save Your Package ID!

  The **OFT Package ID** (not object ID) is what you'll use as the peer address on remote chains. Remote chains must use this package ID to send messages to your Sui OFT.

  **Finding package ID from object** (if you didn't save it):

  ```bash wrap theme={null}
  sui client object <OFT_OBJECT_ID> --json | jq -r '.data.type' | cut -d':' -f1
  ```
</Warning>

**Alternative**: Deploy directly from the cloned repository using `--with-unpublished-dependencies` flag (requires all dependencies in correct relative paths).

<Tip>
  ### Multiple OFTs

  If deploying multiple OFTs (e.g., different tokens), **repeat this OFT package deployment for each token**. Each token gets its own OFT package instance. For adapter OFTs, see [choosing between mint/burn and lock/unlock models](#choosing-mintburn-vs-lockunlock) to avoid deploying multiple adapters for the same token.
</Tip>

### Step 3: Initialize OFT via SDK

Consume the ticket using the OFT SDK. This example uses **mint/burn initialization** by passing the `TreasuryCap`:

```typescript wrap theme={null}
import {SDK} from '@layerzerolabs/lz-sui-sdk-v2';
import {OFT} from '@layerzerolabs/lz-sui-oft-sdk-v2';
import {Stage} from '@layerzerolabs/lz-definitions';

const sdk = new SDK({client, stage: Stage.MAINNET});
const oft = new OFT(sdk, OFT_PKG, undefined, TOKEN_TYPE, OAPP);

const initTx = new Transaction();
// Use initOftMoveCall for mint/burn (includes TreasuryCap)
const [adminCap, migrationCap] = oft.initOftMoveCall(
  initTx,
  TOKEN_TYPE, // "0xTOKEN_PKG::myoft::MYOFT"
  TICKET, // OFTInitTicket object ID
  OAPP, // OApp object ID
  TREASURY, // TreasuryCap object ID (enables mint/burn)
  METADATA, // CoinMetadata object ID
  6, // shared_decimals
);
initTx.transferObjects([adminCap, migrationCap], sender);

const result = await client.signAndExecuteTransaction({
  transaction: initTx,
  signer: keypair,
  options: {showObjectChanges: true},
});

// Extract OFT object ID
const OFT_OBJECT = result.objectChanges.find(
  (c) => c.type === 'created' && c.objectType.includes('oft::OFT<'),
).objectId;

await client.waitForTransaction({digest: result.digest});
```

<Tip>
  ### Lock/Unlock Alternative

  To initialize an OFT Adapter for an **existing token** (lock/unlock model), use `oft.initOftAdapterMoveCall()` instead, which does not require the `TREASURY` parameter. See [Integration with Sui Coin System](#integration-with-sui-coin-system) for details.
</Tip>

## Integration with Sui Coin System

OFTs integrate seamlessly with Sui's native coin framework, using standard types for token management.

### Sui Coin Type System

The Sui framework provides these core types for token functionality:

**`Coin<T>`**: Owned coin object with a value

```rust wrap theme={null}
public struct Coin<phantom T> has key, store {
    id: UID,
    balance: Balance<T>,
}
```

**`Balance<T>`**: Storable value (can be held in structs)

```rust wrap theme={null}
public struct Balance<phantom T> has store {
    value: u64,
}
```

**`TreasuryCap<T>`**: Authority to mint/burn coins

```rust wrap theme={null}
public struct TreasuryCap<phantom T> has key, store {
    id: UID,
    total_supply: Supply<T>,
}
```

**`CoinMetadata<T>`**: Token information (name, symbol, decimals)

```rust wrap theme={null}
public struct CoinMetadata<T> has key, store {
    id: UID,
    decimals: u8,
    name: string::String,
    symbol: ascii::String,
    description: string::String,
    icon_url: Option<Url>,
}
```

### OFT Integration

The OFT uses these types:

```rust wrap theme={null}
public struct OFT<phantom T> has key {
    // ...
    treasury: OFTTreasury<T>,     // Holds TreasuryCap OR Balance escrow
    coin_metadata: address,       // Reference to CoinMetadata<T> object
    // ...
}
```

**[Phantom Type Parameter](https://move-book.com/move-basics/generics#phantom-type-parameters)**: `<phantom T>` means:

* `T` is the coin type (e.g., `MY_COIN`)
* `phantom` = T doesn't appear in any field directly
* Enables type safety without storing `T` values

### OFT Types

Sui OFTs use a flexible enum pattern that supports two token management strategies, depending on whether you're creating a new token or bridging an existing one.

#### OFT Structure

The OFT uses a generic type parameter and includes built-in support for optional features:

```rust wrap theme={null}
/// Omnichain Fungible Token - enables seamless crosschain token transfers
public struct OFT<phantom T> has key {
    id: UID,
    upgrade_version: u64,
    oapp_object: address,           // Associated OApp for messaging
    admin_cap: address,             // AdminCap owner address
    migration_cap: address,         // Migration capability
    oft_cap: CallCap,               // Capability for crosschain calls
    treasury: OFTTreasury<T>,       // ← Enum: determines mint/burn vs lock/unlock
    coin_metadata: address,         // Reference to CoinMetadata<T>
    decimal_conversion_rate: u64,   // 10^(local - shared decimals)
    shared_decimals: u8,            // Crosschain precision

    // Optional features (always present, opt-in to configure)
    pausable: Pausable,             // Starts unpaused (false)
    fee: OFTFee,                    // Starts with 0% fees
    inbound_rate_limiter: RateLimiter,   // Starts with no limits
    outbound_rate_limiter: RateLimiter,  // Starts with no limits
}
```

All OFTs include these fields, but they start in safe default states. Configuration is **optional** and done via admin functions after deployment.

#### Treasury Enum

The `OFTTreasury<T>` enum determines token management strategy:

```rust wrap theme={null}
public enum OFTTreasury<phantom T> has store {
    /// Standard OFT: mints/burns using treasury capability
    OFT {
        treasury_cap: TreasuryCap<T>,  // Grants mint/burn authority
    },
    /// Adapter OFT: escrows/releases existing tokens
    OFTAdapter {
        escrow: Balance<T>,  // Token balance pool
    },
}
```

#### Choosing Mint/Burn vs Lock/Unlock

| Model           | When to Use                           | Initialization Method                           |
| --------------- | ------------------------------------- | ----------------------------------------------- |
| **Mint/Burn**   | You have/control the `TreasuryCap<T>` | `oft.initOftMoveCall()` with TREASURY parameter |
| **Lock/Unlock** | You DON'T have the `TreasuryCap<T>`   | `oft.initOftAdapterMoveCall()` without TREASURY |

#### 1. Mint/Burn

This model manages token supply by minting new tokens on the destination chain and burning them on the source chain.

**When to use**:

* You own or can obtain the `TreasuryCap<T>` for the token
* You're comfortable with dynamic supply distribution across chains
* Works for both new tokens AND existing tokens where you control the TreasuryCap

<Info>
  ### TreasuryCap on Sui

  On Sui, [`TreasuryCap<T>`](https://docs.sui.io/standards/coin#treasury-capability) is an owned object that can be transferred between addresses. If you created a token previously or received the TreasuryCap from someone else, you can use the mint/burn model even for "existing" tokens. Only addresses with access to the `TreasuryCap` can mint and burn the token supply.
</Info>

**Mechanism**:

* **Send**: Burns tokens on source chain (reduces total supply)
* **Receive**: Mints tokens on destination chain (increases total supply)

**Initialization** (via SDK):

```typescript wrap theme={null}
// SDK handles internal treasury enum construction
const [adminCap, migrationCap] = oft.initOftMoveCall(
  initTx,
  TOKEN_TYPE,
  TICKET,
  OAPP,
  TREASURY, // ← Your TreasuryCap transferred to OFT internally
  METADATA,
  6, // shared_decimals
);
```

#### 2. Lock/Unlock

The lock/unlock model enables omnichain bridging by escrowing tokens on the source chain and releasing them on the destination, maintaining fixed supply on Sui.

**When to use**:

* You DON'T have access to the `TreasuryCap<T>` (frozen, held by DAO, or inaccessible)
* Token supply on Sui must remain fixed
* You need to bridge a token where you lack mint/burn authority

**Mechanism**:

* **Send**: Locks tokens in OFT's escrow balance (removes from circulation)
* **Receive**: Releases tokens from escrow balance (returns to circulation)

**Initialization** (via SDK):

```typescript wrap theme={null}
// SDK handles internal treasury enum construction
const [adminCap, migrationCap] = oft.initOftAdapterMoveCall(
  initTx,
  TOKEN_TYPE,
  TICKET,
  OAPP,
  METADATA, // ← No TreasuryCap needed for adapter
  6, // shared_decimals
);
```

<Warning>
  Only deploy **one** OFT Adapter per token mesh. Multiple adapters fragment liquidity and can lead to token loss if supply is insufficient on the destination chain.
</Warning>

## Core Operations

The core operations of an Omnichain Fungible Token (OFT) on Sui enable seamless value transfer across multiple blockchains. At a high level, these consist of sending tokens to another chain and receiving them from peers, all while maintaining strict security and interoperability guarantees.

### Sending Tokens

Sending tokens is the primary function OFTs provide, allowing users to transfer assets from the current chain to a specified recipient on a different blockchain. This operation burns or locks tokens on the source chain, constructs a crosschain message, and leverages the LayerZero protocol to initiate delivery to the destination chain.

```rust wrap theme={null}
public fun send<T>(
    self: &mut OFT<T>,
    oapp: &mut OApp,
    sender: &OFTSender,              // Authorization context (from oft_sender module)
    send_param: &SendParam,          // Complete send parameters
    coin_provided: &mut Coin<T>,     // Coin to debit tokens from
    native_coin_fee: Coin<SUI>,      // Fee payment in SUI
    zro_coin_fee: Option<Coin<ZRO>>, // Optional ZRO payment
    refund_address: Option<address>, // Optional refund address
    clock: &Clock,                   // Clock for rate limiting
    ctx: &mut TxContext,
): (Call<EndpointSendParam, MessagingReceipt>, OFTSendContext)
```

**Returns**: A tuple containing:

1. `Call<EndpointSendParam, MessagingReceipt>` - Route through Endpoint, then confirm
2. `OFTSendContext` - Context for confirming the send operation

**Process**:

1. Debit tokens from sender's coin (burns or escrows based on OFT type)
2. Apply fee if configured, remove dust for decimal precision
3. Build OFT message with recipient and amount in shared decimals
4. Create Call to send via LayerZero Endpoint
5. (Optional) Rate limiter tracks outbound flow

### Receiving Tokens

Receiving tokens on Sui involves securely processing incoming crosschain messages, validating the source and payload, and minting or unlocking tokens to deliver them to the intended recipient.

```rust wrap theme={null}
public fun lz_receive<T>(
    self: &mut OFT<T>,
    oapp: &OApp,                     // Associated OApp for validation
    call: Call<LzReceiveParam, Void>,// Call from Executor via Endpoint
    clock: &Clock,                   // Clock for rate limiting
    ctx: &mut TxContext,
)
```

**Process**:

1. Executor delivers Call object via Endpoint
2. OApp validates Call came from authorized Endpoint and peer
3. OFT decodes message to extract recipient and amount in shared decimals
4. Converts amount to local decimals
5. Credits tokens (mints or releases from escrow based on OFT type)
6. Rate limiter tracks inbound flow
7. Transfers credited tokens to recipient

**For compose functionality**: Use `lz_receive_with_compose()` which additionally requires:

* `compose_queue: &mut ComposeQueue`
* `composer_manager: &mut OFTComposerManager`

## Decimal Precision

OFTs use **local decimals** (per-chain precision) and **shared decimals** (crosschain precision) to handle token transfers across blockchains with different decimal standards. For complete details on how this works, see [OFT Technical Reference](/v2/concepts/technical-reference/oft-reference#shared-decimals).

### Sui-Specific Constraint: u64 Balance Limit

<Warning>
  ### u64 Balance Overflow

  Sui's coin framework uses `u64` for all token balances, imposing a hard limit of `2^64 - 1 = 18,446,744,073,709,551,615`. If you attempt to mint or transfer amounts exceeding this value, the transaction will abort. This is a **blockchain VM constraint** that cannot be bypassed.

  **Impact on decimals**:

  ```
  Maximum supply = (2^64 - 1) / (10^decimals)
  ```

  Choose your decimals carefully during token deployment.
</Warning>

### Recommended Configuration for Sui

| Local Decimals    | Max Total Supply  | Recommendation |
| ----------------- | ----------------- | -------------- |
| 6                 | \~18.4 trillion   | ✅ Recommended  |
| 9                 | \~18.4 billion    | ✅ Recommended  |
| 18 (EVM standard) | \~18 whole tokens | ❌ Avoid on Sui |

**Shared Decimals**: Use `6` (default) for most use cases.

<Tip>
  ### Deployment Planning

  Before calling `coin::create_currency()`:

  1. Calculate your maximum token supply
  2. Choose local decimals: Ensure `max_supply * 10^decimals < 2^64`
  3. Use `shared_decimals = 6` during OFT initialization (standard)

  For detailed information on shared decimals, decimal conversion, and dust handling, see [OFT Technical Reference](/v2/concepts/technical-reference/oft-reference#shared-decimals).
</Tip>

## Registration with Endpoint

After initializing your OFT, you must register it with the LayerZero Endpoint to enable crosschain messaging.

### Using OFT SDK

```typescript wrap theme={null}
import {SDK} from '@layerzerolabs/lz-sui-sdk-v2';
import {OFT} from '@layerzerolabs/lz-sui-oft-sdk-v2';
import {Stage} from '@layerzerolabs/lz-definitions';
import {Transaction} from '@mysten/sui/transactions';

const sdk = new SDK({client, stage: Stage.MAINNET});
const oft = new OFT(sdk, OFT_PKG, OFT_OBJECT, TOKEN_TYPE, OAPP);

const regTx = new Transaction();

// SDK auto-generates lz_receive_info internally!
await oft.registerOAppMoveCall(
  regTx,
  TOKEN_TYPE, // "0xTOKEN_PKG::myoft::MYOFT"
  OFT_OBJECT, // OFT object ID
  OAPP, // OApp object ID
  '0xfbece0b75d097c31b9963402a66e49074b0d3a2a64dd0ed666187ca6911a4d12', // OFTComposerManager
);

const regResult = await client.signAndExecuteTransaction({
  transaction: regTx,
  signer: keypair,
  options: {showObjectChanges: true},
});

// Wait for finality
await client.waitForTransaction({digest: regResult.digest});

console.log('✅ Registration complete:', regResult.digest);
```

**What this does**:

* Creates `MessagingChannel` shared object
* Stores registry entry keyed by your package ID
* Auto-generates proper `lz_receive_info` with all required PTB instructions
* No manual info generation needed!

**OFTComposerManager**: This shared object routes compose messages to appropriate handlers.

OFTComposerManager address on mainnet: `0xfbece0b75d097c31b9963402a66e49074b0d3a2a64dd0ed666187ca6911a4d12`
OFTComposerManager address on testnet: `0x90384f5f6034604f76ac99bbdd25bc3c9c646a6e13a27f14b530733a8e98db99`

## Configuration

After registration, configure your OFT to enable crosschain token transfers.

### Using OApp SDK for Configuration on Sui

All configuration is done through the base SDK's OApp instance. Configure security settings **before** setting peers to open the pathway.

<Info>
  ### Endpoint IDs

  The examples below use EID `30184` (Base Mainnet). For a complete list of endpoint IDs across all supported chains, see [Deployed Contracts](/v2/deployments/deployed-contracts).
</Info>

```typescript wrap theme={null}
import {
  SDK,
  PACKAGE_ULN_302_ADDRESS,
  OBJECT_ULN_302_ADDRESS,
  PACKAGE_DVN_LAYERZERO_ADDRESS,
  OAppUlnConfigBcs,
} from '@layerzerolabs/lz-sui-sdk-v2';
import {Stage} from '@layerzerolabs/lz-definitions';

const sdk = new SDK({client, stage: Stage.MAINNET});
const oapp = sdk.getOApp(OFT_PKG); // Use OFT package ID

// Step 1: Set Send Library (recommended - custom send message library)
const sendLibTx = new Transaction();
await oapp.setSendLibraryMoveCall(
  sendLibTx,
  30184, // Destination EID
  customSendLibraryAddress,
);
await client.signAndExecuteTransaction({transaction: sendLibTx, signer: keypair});

// Step 1: Set Receive Library (recommended - custom receive message library)
const receiveLibTx = new Transaction();
await oapp.setReceiveLibraryMoveCall(
  receiveLibTx,
  30184, // Source EID
  customReceiveLibraryAddress,
  0, // Grace period
);
await client.signAndExecuteTransaction({transaction: receiveLibTx, signer: keypair});

// Step 2: Configure Receive DVN (recommended - receive verification)
const receiveConfig = OAppUlnConfigBcs.serialize({
  use_default_confirmations: false,
  use_default_required_dvns: false,
  use_default_optional_dvns: true,
  uln_config: {
    confirmations: 15,
    // Replace <SECONDARY_PROVIDER> with a non-LayerZero-Labs DVN; see /v2/deployments/dvn-addresses
    required_dvns: [
      PACKAGE_DVN_LAYERZERO_ADDRESS[Stage.MAINNET],
      PACKAGE_DVN_<SECONDARY_PROVIDER>_ADDRESS[Stage.MAINNET],
    ],
    optional_dvns: [],
    optional_dvn_threshold: 0,
  },
}).toBytes();

const receiveConfigTx = new Transaction();
const receiveConfigCall = await oapp.setConfigMoveCall(
  receiveConfigTx,
  PACKAGE_ULN_302_ADDRESS[Stage.MAINNET],
  30184, // Destination EID
  3, // CONFIG_TYPE_RECEIVE_ULN
  receiveConfig,
);

receiveConfigTx.moveCall({
  target: `${PACKAGE_ULN_302_ADDRESS[Stage.MAINNET]}::uln_302::set_config`,
  arguments: [receiveConfigTx.object(OBJECT_ULN_302_ADDRESS[Stage.MAINNET]), receiveConfigCall],
});

await client.signAndExecuteTransaction({transaction: receiveConfigTx, signer: keypair});

// Step 2: Configure Send DVN (recommended - send verification)
const sendConfig = OAppUlnConfigBcs.serialize({
  use_default_confirmations: false,
  use_default_required_dvns: false,
  use_default_optional_dvns: true,
  uln_config: {
    confirmations: 15,
    // Replace <SECONDARY_PROVIDER> with a non-LayerZero-Labs DVN; see /v2/deployments/dvn-addresses
    required_dvns: [
      PACKAGE_DVN_LAYERZERO_ADDRESS[Stage.MAINNET],
      PACKAGE_DVN_<SECONDARY_PROVIDER>_ADDRESS[Stage.MAINNET],
    ],
    optional_dvns: [],
    optional_dvn_threshold: 0,
  },
}).toBytes();

const sendConfigTx = new Transaction();
const sendConfigCall = await oapp.setConfigMoveCall(
  sendConfigTx,
  PACKAGE_ULN_302_ADDRESS[Stage.MAINNET],
  30184,
  2, // CONFIG_TYPE_SEND_ULN (outbound messages)
  sendConfig,
);

sendConfigTx.moveCall({
  target: `${PACKAGE_ULN_302_ADDRESS[Stage.MAINNET]}::uln_302::set_config`,
  arguments: [sendConfigTx.object(OBJECT_ULN_302_ADDRESS[Stage.MAINNET]), sendConfigCall],
});

await client.signAndExecuteTransaction({transaction: sendConfigTx, signer: keypair});

// Step 3: Set Enforced Options (optional - minimum gas requirements)
const options = Options.newOptions()
  .addExecutorLzReceiveOption(80000, 0) // 80k gas for destination
  .toBytes();

const optionsTx = new Transaction();
await oapp.setEnforcedOptionsMoveCall(
  optionsTx,
  30184, // Destination EID
  1, // Message type (1 = SEND)
  options,
);
await client.signAndExecuteTransaction({transaction: optionsTx, signer: keypair});

// Step 4: Configure OFT Settings (optional - rate limits)
const rateLimitTx = new Transaction();
await oft.setRateLimitMoveCall(
  rateLimitTx,
  30184, // Destination EID
  false, // Outbound
  1000000n, // 1M tokens per window
  86400n, // 24 hours
);
await client.signAndExecuteTransaction({transaction: rateLimitTx, signer: keypair});

// Step 4: Configure OFT Settings (optional - fees)
const feeTx = new Transaction();
await oft.setFeeBpsMoveCall(feeTx, 30184, 30); // 0.3% fee
await client.signAndExecuteTransaction({transaction: feeTx, signer: keypair});

// Step 5: Set Peer LAST (required - opens pathway for messaging)
const peerTx = new Transaction();
await oapp.setPeerMoveCall(
  peerTx,
  30184, // Destination EID (e.g., Base)
  Buffer.from('0000000000000000000000006D2e17A05B9Ac62b8499f4bF4757e261005c03A5', 'hex'),
);
await client.signAndExecuteTransaction({transaction: peerTx, signer: keypair});
```

**Configuration order**:

1. **Set Libraries** (recommended) - Custom send/receive message libraries
2. **Configure DVNs** (recommended) - Send and receive verification
3. **Set Enforced Options** (optional) - Minimum gas requirements
4. **Configure OFT Settings** (optional) - Rate limits, fees
5. **Set Peer** (required) - Opens pathway for messaging (call this last!)

<Tip>
  For complete DVN configuration details and gas recommendations, see [Configuration Guide](/v2/developers/sui/configuration/dvn-executor-config).
</Tip>

### Configuring Remote Chains to Send to Sui

When configuring OFTs on other chains (e.g., EVM, Solana) to send tokens **to Sui**, follow standard LayerZero configuration but note these Sui-specific requirements:

**1. Use Package ID as Peer**:

```solidity wrap theme={null}
// On EVM: Use Sui OFT PACKAGE ID (not object ID!)
myOFT.setPeer(
    30378,  // Sui mainnet EID
    bytes32(0x061a47bf...)  // Your Sui OFT Package ID
);
```

**2. Set Enforced Options for Sui Destination**:

Based on gas profiling, configure appropriate gas limits for Sui:

```solidity wrap theme={null}
// On EVM: Set enforced options for Sui pathway
import { Options } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/ExecutorOptions.sol";

bytes memory options = Options.newOptions()
    .addExecutorLzReceiveOption(5000, 0);  // 5k gas units, no msg.value

myOFT.setEnforcedOptions(
    EnforcedOptionParam({
        eid: 30378,           // Sui mainnet
        msgType: SEND,
        options: options
    })
);
```

**Gas requirements**:

* Sui's `lz_receive` uses 2,000-5,000 MIST for computation
* Use 5,000 gas units for safe buffer
* No `msg.value` needed (Sui handles storage internally)

**3. Standard DVN Configuration**:

DVN configuration on remote chains follows standard LayerZero patterns - no Sui-specific changes needed. See the platform-specific implementation guides for EVM and Solana configuration.

## Example Usage

### Sending Tokens (TypeScript SDK)

```typescript wrap theme={null}
import {OFT} from '@layerzerolabs/lz-sui-oft-sdk-v2';

// Quote the fee
const {nativeFee} = await oft.quote({
  dstEid: 30101, // Ethereum
  to: recipientBytes32,
  amountLD: BigInt(1000000), // 1 token (6 decimals)
  options: optionsBytes,
});

// Send tokens
const receipt = await oft.send({
  dstEid: 30101,
  to: recipientBytes32,
  amountLD: BigInt(1000000),
  minAmountLD: BigInt(950000), // 5% slippage
  nativeFee,
  options: optionsBytes,
});
```

For more SDK usage, see [OFT SDK Documentation](/v2/developers/sui/oft/sdk).

## Best Practices & Troubleshooting

**Deployment**:

* Use pure LayerZero OFT source without modifications
* Always wait for transaction finality: `await client.waitForTransaction({ digest })`
* Use SDK factory: `sdk.getOApp(packageId)` (never `new OApp(...)`)
* Use SDK address exports with `Stage.MAINNET` (no hardcoded addresses)

**Security**:

* Test with small amounts before production
* Validate peer addresses match package IDs (not object IDs)
* Configure DVNs before setting peers
* Only deploy one OFT Adapter per token

**Common Errors**:

* `oapp_registry::get_messaging_channel abort code: 1` → Using object ID instead of package ID as peer
* `InvalidBCSBytes in command 0` → Use `OAppUlnConfigBcs.serialize()` for DVN config
* `UnusedValueWithoutDrop` → Use `oft.registerOAppMoveCall()` for proper lz\_receive\_info

For gas profiling and detailed configuration, see [Configuration Guide](/v2/developers/sui/configuration/dvn-executor-config).

## Next Steps

* [OFT SDK Documentation](/v2/developers/sui/oft/sdk) - Complete SDK methods and TypeScript integration
* [Configuration Guide](/v2/developers/sui/configuration/dvn-executor-config) - DVN, executor, and gas configuration
* [OApp Overview](/v2/developers/sui/oapp/overview) - Base messaging standard
* [Technical Overview](/v2/developers/sui/technical-overview) - Sui fundamentals and Call pattern
* [Protocol Overview](/v2/developers/sui/protocol-overview) - Complete message workflows
* [Troubleshooting](/v2/developers/sui/troubleshooting/common-errors) - Common deployment issues
