Skip to main content
Frequently asked questions about developing LayerZero applications on Sui.

General Questions

Sui Move lacks native dynamic dispatch (unlike EVM’s delegatecall). The Call<Param, Result> pattern provides an alternative by creating structs without drop or store abilities that must be consumed, using capability-based authorization, and enforcing call sequences through lifecycle states while ensuring atomicity within Programmable Transaction Blocks.For a detailed explanation of the Call pattern and Sui’s architecture, see the Sui documentation on PTBs.
The key difference is that Sui uses Call objects and PTBs instead of delegatecall. In EVM, the relayer calls Endpoint.lzReceive() which delegates to the OApp. In Sui, the Executor calls Endpoint.lz_receive() which creates a Call<LzReceiveParam, Void> object that the OApp destroys and processes via explicit PTB routing. Both execution models are permissionless.For architectural details, see Technical Overview and Protocol Overview.

Development Questions

Use SuiScan:Method 1 - Web Interface:
  1. Navigate to SuiScan verification page
  2. Enter package address
  3. Upload source files
  4. Wait for verification
Method 2 - API:
curl -X POST https://suiscan.xyz/api/verify \
  -d '{"packageId": "0x...", "source": "..."}'
See Sui Guidance for details.
No. LayerZero deploys and maintains the EndpointV2 shared object on Sui. You only need to:
  1. Publish your OApp or OFT package
  2. Register your OApp with the Endpoint (creates a MessagingChannel)
  3. Configure pathways to other chains
OFTs use shared decimals to handle precision differences:
Local Decimals:  Token decimals on current chain (e.g., 9)
Shared Decimals: Crosschain precision (default: 6)

Conversion Rate: 10^(local - shared)
When sending:
  1. Amount is divided by conversion rate (removes dust)
  2. Truncated amount is sent crosschain
  3. Destination multiplies by its conversion rate
See OFT Overview for examples.
Yes, use an OFT Adapter (lock/unlock model):
public struct OFTAdapter {
    escrow: Balance<EXISTING_TOKEN>, // Locked tokens
    // No treasury_cap needed
}
For new tokens, use mint/burn OFT for better efficiency.

Gas and Fees

Sui uses a dual gas model:Storage Gas:
  • Charged for creating objects
  • Refunded when objects are deleted
  • Can result in negative net gas
Computation Gas:
  • Charged for execution
  • Not refunded
For LayerZero:
  • Minimum 1000 base gas units
  • Budget 5-20M for typical operations
  • Source chain pays destination execution
Negative gas is normal when storage is freed:
// Freeing storage triggers rebate
let MyObject { id, data } = obj;
object::delete(id); // Rebate > gas used
Key Points:
  • This is not an error
  • Still need minimum 1000 base budget
  • Net cost can be negative
  • Rebate goes to transaction sender
See Technical Overview for details.
Recommended gas budgets:
OperationGas Budget
Initialize channel10,000,000
Set peer10,000,000
Configure DVNs15,000,000
Send message20,000,000
Receive message15,000,000
Deploy package100,000,000+
Start higher and reduce based on actual usage.

Configuration Questions

No, defaults are available:
# Minimal configuration (uses defaults)
initialize_channel(...)  # Required
set_peer(...)           # Required
# That's it! Uses default DVNs and Executor
Custom configuration is optional for:
  • Specific security requirements
  • Custom DVN sets
  • Private executors
Use the TypeScript SDK:
// Get peer
const peer = await oapp.getPeer(remoteEid);

// Get DVN config
const config = await oapp.getSendConfig(remoteEid);

console.log({
  peer: Buffer.from(peer).toString('hex'),
  requiredDVNs: config.requiredDVNs,
  optionalDVNs: config.optionalDVNs,
});
The Sui CLI cannot easily parse complex return values.
Yes, if you retain the AdminCap:
# Update peer
sui client call \
    --function set_peer \
    --args $OAPP $ADMIN_CAP $NEW_EID $NEW_PEER \
    ...

# Update DVNs
sui client call \
    --function set_send_uln_config \
    --args $OAPP $ADMIN_CAP $EID ... \
    ...
Without AdminCap, configuration is immutable.

SDKs and Tooling

LayerZero provides two TypeScript SDKs:
  1. @layerzerolabs/lz-sui-sdk-v2
    • Core Endpoint interactions
    • OApp functionality
    • Configuration management
  2. @layerzerolabs/lz-sui-oft-sdk-v2
    • OFT-specific operations
    • Token transfers
    • Balance queries
See OFT SDK for usage examples.
The Sui CLI can read simple fields but has limitations for complex queries:
  • Doesn’t easily parse return values from view functions
  • Manual decoding needed for bytes arrays and nested structs
  • No built-in formatting for complex types
Solution: Use TypeScript SDK for complex state queries:
import {SuiClient} from '@mysten/sui.js/client';

// Query OApp object fields
const oapp = await client.getObject({
  id: oappObjectId,
  options: {showContent: true},
});

// Or use LayerZero SDK helpers
import {OApp} from '@layerzerolabs/lz-sui-sdk-v2';
const peer = await oapp.getPeer(client, remoteEid);
Not currently. Package publication and configuration require:
  1. Publish packages: Using sui client publish
  2. Call entry functions: Invoke configuration functions via sui client call or SDK
  3. Custom scripts: Write TypeScript scripts for automated workflows
See Configuration Guide for manual setup instructions.

Troubleshooting

This error means a Call object wasn’t properly consumed in your PTB:
// - Incorrect: Call object not confirmed
let call = oapp::send(&mut oapp, &oapp_cap, ...);
// Transaction ends without destroying call → ERROR

// - Correct: Call object confirmed
let call = oapp::send(&mut oapp, &oapp_cap, ...);
// PTB routes call through Endpoint/ULN/Workers
let (_, receipt) = oapp::confirm_lz_send(&oapp, &oapp_cap, call);
Solution: Every Call returned must be confirmed/destroyed before the transaction completes.
You’re trying to send to a destination chain without a MessagingChannel:
# Fix: Initialize the channel first
sui client call \
    --package <ENDPOINT_PACKAGE> \
    --module endpoint_v2 \
    --function init_channel \
    --args <ENDPOINT_OBJECT> <CALL_CAP> <REMOTE_EID> \
    --gas-budget 10000000
The Endpoint creates a dedicated MessagingChannel shared object for each OApp.
Use recovery entry functions on the Endpoint (requires AdminCap):Skip a message (increment nonce without execution):
sui client call \
    --package <ENDPOINT_PACKAGE> \
    --module endpoint_v2 \
    --function skip \
    --args <ENDPOINT_OBJECT> <ADMIN_CAP> <OAPP_ADDRESS> <SRC_EID> <SENDER_BYTES32> <NONCE> \
    --gas-budget 10000000
Clear a message (mark as delivered without execution):
sui client call \
    --function clear \
    --args <ENDPOINT_OBJECT> <ADMIN_CAP> <OAPP_ADDRESS> <SRC_EID> <SENDER_BYTES32> <NONCE> \
    --gas-budget 10000000
See Common Errors for more recovery options.

Security Questions

Follow these capability-based security practices:
  1. Validate CallCap in All Functions:
public fun send(
    self: &OApp,
    oapp_cap: &CallCap,  // - Require capability
    ...
) {
    self.assert_oapp_cap(oapp_cap);  // - Validate ownership
    // ...
}

fun assert_oapp_cap(self: &OApp, cap: &CallCap) {
    assert!(self.oapp_cap.id() == cap.id(), EInvalidOAppCap);
}
  1. Validate Call Objects:
public fun lz_receive(self: &mut OApp, call: Call<LzReceiveParam, Void>) {
    // - Validate Call came from authorized Endpoint
    let (callee, param, _) = call.destroy(&self.oapp_cap);
    assert!(callee == endpoint_address(), EOnlyEndpoint);

    // - Validate sender is configured peer
    let peer = self.peer.get_peer(param.src_eid);
    assert!(param.sender == peer, EOnlyPeer);
}
  1. Secure Capability Objects:
  • Store CallCap in package module storage (not transferred)
  • Use multisig or hardware wallet for AdminCap
  • Never expose capabilities publicly
  • Transfer AdminCap carefully (use transfer::public_transfer)
  1. Protect UpgradeCap:
  • Keep upgrade authority secure
  • Consider freezing upgrades after deployment (package::make_immutable)
  • Use multisig for mainnet upgrade authority
    • Missing CallCap validation in functions
    • Not validating Call object source (callee address)
    • Skipping peer validation in lz_receive
    • Losing capability objects (no recovery possible)
    • Wrong peer addresses configured
    • Exposing AdminCap or CallCap publicly
See OApp Best Practices for details.

Next Steps