Skip to main content
Version: Endpoint V2

Sui FAQ

Frequently asked questions about developing LayerZero applications on Sui.

General Questions

Why does Sui use the Call (Hot Potato) pattern?

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.

How is the receive path different from EVM?

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

How do I verify my Sui package?

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.

Do I need to deploy my own Endpoint?

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

How do I handle decimal precision for OFTs?

OFTs use shared decimals to handle precision differences:

Local Decimals:  Token decimals on current chain (e.g., 9)
Shared Decimals: Cross-chain precision (default: 6)

Conversion Rate: 10^(local - shared)

When sending:

  1. Amount is divided by conversion rate (removes dust)
  2. Truncated amount is sent cross-chain
  3. Destination multiplies by its conversion rate

See OFT Overview for examples.

Can I use existing tokens with LayerZero?

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

What are the gas considerations for cross-chain messages?

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

How do I handle negative gas utilization rates?

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.

How much should I budget for gas?

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

Do I need to set custom DVNs and Executors?

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

How do I check my current configuration?

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.

Can I change configuration after deployment?

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

What SDKs are available for Sui?

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.

Why can't I use the Sui CLI to query state?

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);

Is there a deploy/wire tool for Sui?

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

Why is my transaction failing with "unused value without drop ability"?

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.

What does "Channel not initialized" mean?

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.

How do I recover from a stuck message?

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

How do I secure my OApp?

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

What are common security pitfalls?

    • 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