Sui OFT
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.
What is an OFT on Sui?
An OFT on Sui is a Move package that extends the OApp functionality to enable cross-chain token transfers. It integrates with Sui's native coin type system (Coin<T>, Balance<T>, 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.
Deployment
OFT deployment on Sui uses a two-package pattern: your token + pure LayerZero OFT source.
This deployment guide demonstrates the mint/burn approach, where you provide the TreasuryCap and the OFT mints/burns tokens during cross-chain 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 for details.
Prerequisites:
- Sui CLI installed (via suiup)
- Node.js and npm for TypeScript SDK
- 1-2 SUI for gas fees
If you haven't used Sui before, start with Getting Started with Sui to understand the object model, package structure, and development basics.
Step 1: Create and Deploy Your Token
Create token package:
mkdir my-token
cd my-token
sui move new myoft
Implement token (sources/myoft.move):
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 cross-chain 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:
# 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 without modifications.
Recommended approach (git dependencies):
# Copy OFT source to your project
mkdir oft
cd oft
Create Move.toml with git dependencies:
[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:
# 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):
# 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()).
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):
sui client object <OFT_OBJECT_ID> --json | jq -r '.data.type' | cut -d':' -f1
Alternative: Deploy directly from the cloned repository using --with-unpublished-dependencies flag (requires all dependencies in correct relative paths).
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 to avoid deploying multiple adapters for the same token.
Step 3: Initialize OFT via SDK
Consume the ticket using the OFT SDK. This example uses mint/burn initialization by passing the TreasuryCap:
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});
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 for details.
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
public struct Coin<phantom T> has key, store {
id: UID,
balance: Balance<T>,
}
Balance<T>: Storable value (can be held in structs)
public struct Balance<phantom T> has store {
value: u64,
}
TreasuryCap<T>: Authority to mint/burn coins
public struct TreasuryCap<phantom T> has key, store {
id: UID,
total_supply: Supply<T>,
}
CoinMetadata<T>: Token information (name, symbol, decimals)
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:
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: <phantom T> means:
Tis the coin type (e.g.,MY_COIN)phantom= T doesn't appear in any field directly- Enables type safety without storing
Tvalues
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:
/// Omnichain Fungible Token - enables seamless cross-chain 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 cross-chain 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, // Cross-chain 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:
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
On Sui, TreasuryCap<T> 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.
Mechanism:
- Send: Burns tokens on source chain (reduces total supply)
- Receive: Mints tokens on destination chain (increases total supply)
Initialization (via SDK):
// 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):
// 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
);
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.
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 cross-chain message, and leverages the LayerZero protocol to initiate delivery to the destination chain.
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:
Call<EndpointSendParam, MessagingReceipt>- Route through Endpoint, then confirmOFTSendContext- Context for confirming the send operation
Process:
- Debit tokens from sender's coin (burns or escrows based on OFT type)
- Apply fee if configured, remove dust for decimal precision
- Build OFT message with recipient and amount in shared decimals
- Create Call to send via LayerZero Endpoint
- (Optional) Rate limiter tracks outbound flow
Receiving Tokens
Receiving tokens on Sui involves securely processing incoming cross-chain messages, validating the source and payload, and minting or unlocking tokens to deliver them to the intended recipient.
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:
- Executor delivers Call object via Endpoint
- OApp validates Call came from authorized Endpoint and peer
- OFT decodes message to extract recipient and amount in shared decimals
- Converts amount to local decimals
- Credits tokens (mints or releases from escrow based on OFT type)
- Rate limiter tracks inbound flow
- Transfers credited tokens to recipient
For compose functionality: Use lz_receive_with_compose() which additionally requires:
compose_queue: &mut ComposeQueuecomposer_manager: &mut OFTComposerManager
Decimal Precision
OFTs use local decimals (per-chain precision) and shared decimals (cross-chain precision) to handle token transfers across blockchains with different decimal standards. For complete details on how this works, see OFT Technical Reference.
Sui-Specific Constraint: u64 Balance Limit
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.
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.
Before calling coin::create_currency():
- Calculate your maximum token supply
- Choose local decimals: Ensure
max_supply * 10^decimals < 2^64 - Use
shared_decimals = 6during OFT initialization (standard)
For detailed information on shared decimals, decimal conversion, and dust handling, see OFT Technical Reference.
Registration with Endpoint
After initializing your OFT, you must register it with the LayerZero Endpoint to enable cross-chain messaging.
Using OFT SDK
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
MessagingChannelshared object - Stores registry entry keyed by your package ID
- Auto-generates proper
lz_receive_infowith all required PTB instructions - No manual info generation needed!
OFTComposerManager: This shared object routes compose messages to appropriate handlers. Use the address shown above for mainnet.
Configuration
After registration, configure your OFT to enable cross-chain 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.
The examples below use EID 30184 (Base Mainnet). For a complete list of endpoint IDs across all supported chains, see Deployed Contracts.
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,
required_dvns: [PACKAGE_DVN_LAYERZERO_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,
required_dvns: [PACKAGE_DVN_LAYERZERO_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:
- Set Libraries (recommended) - Custom send/receive message libraries
- Configure DVNs (recommended) - Send and receive verification
- Set Enforced Options (optional) - Minimum gas requirements
- Configure OFT Settings (optional) - Rate limits, fees
- Set Peer (required) - Opens pathway for messaging (call this last!)
For complete DVN configuration details and gas recommendations, see Configuration Guide.
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:
// 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:
// 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_receiveuses 2,000-5,000 MIST for computation - Use 5,000 gas units for safe buffer
- No
msg.valueneeded (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)
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.
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)(nevernew 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 peerInvalidBCSBytes in command 0→ UseOAppUlnConfigBcs.serialize()for DVN configUnusedValueWithoutDrop→ Useoft.registerOAppMoveCall()for proper lz_receive_info
For gas profiling and detailed configuration, see Configuration Guide.
Next Steps
- OFT SDK Documentation - Complete SDK methods and TypeScript integration
- Configuration Guide - DVN, executor, and gas configuration
- OApp Overview - Base messaging standard
- Technical Overview - Sui fundamentals and Call pattern
- Protocol Overview - Complete message workflows
- Troubleshooting - Common deployment issues