Skip to main content
Version: Endpoint V2

Common Errors

This page lists common errors you may encounter when developing LayerZero applications on Sui, along with their causes and solutions.

Deployment Issues

Git Dependencies Failed

Error Message:

Error: Package dependency does not specify published address
Error: Failed to resolve dependencies

Cause: Git dependencies for LayerZero packages don't work due to missing Move.toml manifests in subdirectories.

Solution: Use local dependencies instead:

# Clone LayerZero repo
git clone https://github.com/LayerZero-Labs/LayerZero-v2.git

# Update Move.toml to use local paths
[dependencies]
OApp = { local = "../LayerZero-v2/packages/layerzero-v2/sui/contracts/oapps/oapp" }
EndpointV2 = { local = "../LayerZero-v2/packages/layerzero-v2/sui/contracts/endpoint-v2" }
# ... other packages

Or use published package addresses (see Deployed Contracts).

Unpublished Dependencies Error

Error Message:

Error: Modules in package '<package>' were not published with the '--with-unpublished-dependencies' flag

Cause: Package has dependencies that aren't published on-chain.

Solution: Add the flag when publishing:

sui client publish --with-unpublished-dependencies --gas-budget 200000000

Package Size Exceeded

Error Message:

Error: Package size (260 KB) exceeds maximum allowed size (250 KB)

Cause: Your package exceeds Sui's 250 KiB limit per package object. See Sui transaction limits.

Solutions:

  1. Split into multiple packages
  2. Remove unused dependencies
  3. Optimize data structures
  4. Move large constants off-chain

Example Split:

// Package 1: Core logic
module my_oapp::core {
// Essential functions
}

// Package 2: Utilities
module my_oapp::utils {
// Helper functions
}

Insufficient Gas for Deployment

Error Message:

Error: Insufficient gas: needed 150000000, available 100000000

Cause: Deployment requires more gas than budgeted.

Solution: Increase gas budget:

sui client publish --gas-budget 200000000

Deployment typically requires:

  • Simple packages: 50-100M gas
  • Complex packages: 100-200M gas
  • With dependencies: 200M+ gas

Upgrade Authority Issues

Error Message:

Error: UpgradeCap not found or not owned by signer

Cause: The signer doesn't own the UpgradeCap for the package.

Solution:

  1. Verify you're using the correct account
  2. Check UpgradeCap ownership:
sui client objects | grep UpgradeCap
  1. Transfer UpgradeCap if needed

Configuration Issues

Channel Not Initialized

Error Message:

Error: Channel not initialized for endpoint ID 30101

Cause: Attempting to send message before initializing the messaging channel.

Solution: Initialize channel first:

sui client call \
--package $PACKAGE \
--module oapp \
--function initialize_channel \
--args $OAPP_OBJECT $ADMIN_CAP 30101 \
--gas-budget 10000000

Peer Not Set

Error Message:

Error: Peer address not configured for endpoint ID 30101

Cause: No peer OApp address configured for the destination chain.

Solution: Set peer address:

sui client call \
--package $PACKAGE \
--module oapp \
--function set_peer \
--args $OAPP_OBJECT $ADMIN_CAP 30101 $PEER_ADDRESS_BYTES \
--gas-budget 10000000

Address Format: Ensure peer address is 32 bytes (pad EVM addresses).

Library Configuration Missing

Error Message:

Error: Send library not configured for endpoint ID 30101

Cause: Custom library set but not properly configured.

Solution: Either:

  1. Use default libraries (don't set custom)
  2. Or properly configure custom library:
sui client call \
--package $PACKAGE \
--module oapp \
--function set_send_library \
--args $OAPP_OBJECT $ADMIN_CAP 30101 $LIBRARY_ADDRESS \
--gas-budget 10000000

Configuration Errors

Peer Address Error (oapp_registry abort)

Error Message:

Error: oapp_registry::get_messaging_channel abort code: 1
Error: MoveAbort in module oapp_registry

Cause: Remote chain is using wrong receiver address - likely using object ID instead of package ID.

Solution: On Sui, peer addresses must be package IDs, not object IDs.

Find Your Correct Package ID:

# Query your OApp/OFT object
sui client object <YOUR_OAPP_OBJECT_ID> --json | jq '.data.type'

# Output example:
# "0x061a47bffa630b8cd3735f8479edf7ab7897863fb3b796e77ebb8786af6f1bfc::oapp::OApp"
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# This is your package ID - use as peer address!

Update Peer on Remote Chain:

// On EVM/Solana/other chains, use Sui package ID:
await oapp.setPeer(
30230, // Sui mainnet EID
'0x061a47bffa630b8cd3735f8479edf7ab7897863fb3b796e77ebb8786af6f1bfc', // Package ID
);

Why Package ID?

  • Sui OApps use CapType::Package for CallCap
  • Registry and verification systems key by package address
  • Object IDs are instance-specific, package ID is deployment-specific

Package ID vs Object ID Confusion

Error Message:

Error: Transaction was not signed by the correct sender
Error: Object ID does not exist

Cause: Using package ID when object ID is required (or vice versa).

Key Differences:

  • Package ID: Address of published code (immutable bytecode)
  • Object ID: Address of object instance (mutable state)

Example:

// - Wrong: Using package ID as object
tx.object(packageId); // Package is not an object!

// - Correct: Use object ID
tx.object(oappObjectId); // The OApp object instance

How to Find:

# View transaction outputs after publishing
sui client publish --json

# objectChanges array shows:
# - "published" type = package ID
# - "created" type = object IDs

Invalid BCS Bytes Error

Error Message:

Error: InvalidBCSBytes
Error: Unable to deserialize config
Error: Failed to deserialize argument at index 6

Cause: Using tx.pure() instead of SDK's asBytes() helper for byte array parameters.

Solution: Use the SDK's asBytes() helper:

import {SDK, OAppUlnConfigBcs} from '@layerzerolabs/lz-sui-sdk-v2';

// CRITICAL: Import asBytes helper
const {asBytes} = await import('@layerzerolabs/lz-sui-sdk-v2');

// Encode configuration
const config = OAppUlnConfigBcs.serialize({
use_default_confirmations: false,
use_default_required_dvns: false,
use_default_optional_dvns: true,
uln_config: {
confirmations: 15,
required_dvns: [dvnAddress],
optional_dvns: [],
optional_dvn_threshold: 0,
},
}).toBytes();

const tx = new Transaction();

// - WRONG: Using tx.pure() causes InvalidBCSBytes
tx.pure(config, 'vector<u8>');

// - CORRECT: Use asBytes() helper
asBytes(tx, config);

// In context:
tx.moveCall({
target: '...',
arguments: [
// ... other args
asBytes(tx, config), // ← This works
],
});

Why asBytes() is Required:

The SDK's asBytes() function performs proper BCS vector wrapping:

// Actual implementation from SDK utils/index.ts
export function asBytes(
tx: Transaction,
bytes: Uint8Array | TransactionArgument,
): TransactionArgument {
if (isTransactionArgument(bytes)) {
return bytes;
}
// Wraps in BCS vector encoding
return tx.pure(bcs.vector(bcs.u8()).serialize(Array.from(bytes)).toBytes());
}

What it does:

  • Takes raw bytes and wraps them in BCS vector format
  • Handles Transaction Argument pass-through
  • Ensures proper deserialization in Move's vector<u8> type

Why tx.pure() fails:

  • Direct tx.pure(bytes, 'vector<u8>') doesn't apply BCS vector wrapping
  • Move deserializer expects BCS-encoded vector format
  • Results in InvalidBCSBytes error

Common Scenarios Requiring asBytes():

    • DVN/ULN configuration
    • Execution options
    • OApp info parameters
    • Any vector<u8> config parameter

Execution Errors

Executor Transaction Fails (UnusedValueWithoutDrop)

Error Message:

Executor transaction simulation reverted
UnusedValueWithoutDrop { result_idx: 3, secondary_idx: 0 }
Error during lz_receive execution

Cause: Executor can't properly build the PTB to call your OApp/OFT's lz_receive() function.

Most Common Reason for OFTs: Missing or incorrect lz_receive_info during registration.

Solution for OFTs:

  1. Generate proper lz_receive_info:
const tx = new Transaction();
const [lzReceiveInfo] = tx.moveCall({
target: `${oftPackage}::oft_ptb_builder::lz_receive_info`,
typeArguments: [tokenType],
arguments: [
tx.object(oftObjectId),
tx.object(endpointObjectId),
tx.object('0xfbece0b75d097c31b9963402a66e49074b0d3a2a64dd0ed666187ca6911a4d12'), // OFTComposerManager
tx.object('0x6'), // Clock
],
});

const result = await client.devInspectTransactionBlock({
transactionBlock: tx,
sender: yourAddress,
});
const lzReceiveInfoBytes = bcs.vector(bcs.u8()).parse(...);
  1. Update OApp info in registry:
import {asBytes} from '@layerzerolabs/lz-sui-sdk-v2';

const tx = new Transaction();
tx.moveCall({
target: `${oappPackage}::endpoint_calls::set_oapp_info`,
arguments: [
tx.object(oappObjectId),
tx.object(adminCapId),
tx.object(endpointObjectId),
asBytes(tx, oappInfoBytes), // Includes lz_receive_info
],
});

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

Prevention: Always provide lz_receive_info during initial OFT registration (see OFT Overview).

OApp Registry Error

Error Message:

Error: oapp_registry::get_messaging_channel abort code: 1
Error: MoveAbort in module oapp_registry

Cause: Remote chain is using wrong receiver address - using object ID instead of package ID.

Solution:

On Sui, peer addresses must be package IDs:

# Find your package ID
sui client object <YOUR_OAPP_OBJECT_ID> --json | jq '.data.type'
# Example: "0x061a47bf...::oapp::OApp"
# ^^^^^^^^^^^^
# Use this package ID as peer on remote chains

Update peer on remote chain:

// On EVM
oapp.setPeer(30230, bytes32(0x061a47bffa630b8cd3735f8479edf7ab7897863fb3b796e77ebb8786af6f1bfc)); // Package ID

Why: Sui uses Package CallCaps where callCap.id() returns the package address.

Runtime Errors

Call Object Not Consumed

Error Message:

Error: unused value without 'drop' ability
Error: unused value of type 'call::call::Call<...>'

Cause: A Call object was not properly consumed before the transaction ended.

Root Causes:

  1. Missing confirmation call (e.g., confirm_lz_send)
  2. PTB doesn't route the Call through all required modules
  3. Call object created but never destroyed

Solution:

// - Incorrect: Call not confirmed
let call = oapp::send(&mut oapp, &call_cap, ...);
// Transaction ends → ERROR

// - Correct: Call confirmed and destroyed
let call = oapp::send(&mut oapp, &call_cap, ...);
// PTB processes the Call through Endpoint/ULN/Workers
let (params, receipt) = oapp::confirm_lz_send(&oapp, &call_cap, call);

Debug Checklist:

  • Every Call creation has a corresponding confirm call
  • PTB includes all required routing steps
  • No early returns that skip confirmation
  • All Call objects are destroyed before transaction ends

Invalid Recipient

Error Message:

Error: Invalid recipient: object not owned by recipient address

Cause: Trying to send tokens to an invalid or non-existent address.

Solution:

  1. Verify recipient address is valid
  2. For token sends, check if recipient needs an account created
  3. Ensure address format is correct (32 bytes)

Gas Estimation Failures

Error Message:

Error: Unable to estimate gas for transaction

Cause: Transaction simulation failed during gas estimation.

Solutions:

  1. Check transaction parameters are valid
  2. Verify all required objects exist
  3. Ensure signer has necessary permissions
  4. Try with higher gas budget

Debug:

# Dry run to see simulation errors
sui client call ... --json --dry-run

Transaction Issues

PTB Construction Failures

Error Message:

Error: Invalid PTB: missing required call

Cause: Programmable Transaction Block doesn't include all required calls.

Solution: Verify PTB structure:

// Correct PTB structure for send
const tx = new Transaction();

// 1. Call OApp
tx.moveCall({
target: `${oappPackage}::oapp::send`,
arguments: [
/* ... */
],
});

// 2. PTB will route Hot Potatoes automatically
// 3. Confirm calls are added by the builder

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

Object Ownership Errors

Error Message:

Error: Object 0x... is not owned by sender
Error: InvalidObjectOwnership

Cause: Trying to use an owned object that belongs to a different address.

Solutions:

  1. Verify object ownership:
sui client object <OBJECT_ID> --json | jq '.data.owner'

Output types:

  • {"AddressOwner": "0x..."} - Owned by specific address
  • "Shared" - Shared object (accessible to anyone)
  • "Immutable" - Immutable object (read-only)
  1. Use correct signer: Ensure the transaction signer owns the object

  2. Check object type:

Example:

// - Correct: AdminCap owned by signer
public fun set_peer(
oapp: &mut OApp, // Shared object (anyone can reference)
admin_cap: &AdminCap, // Owned object (must own to use)
...
)

Storage Rebate Confusion

Error Message (not actually an error):

Gas used: -500000 (negative)

Cause: Transaction freed storage, resulting in a rebate.

Explanation: This is normal behavior, not an error. When storage is freed:

  • You get a rebate for the freed storage
  • Net gas cost can be negative
  • Base budget of 1000 is still required

Example:

// Deleting object frees storage
let MyObject { id, data } = obj;
object::delete(id); // Triggers rebate

SDK Errors

Connection Timeout

Error Message:

Error: Request timeout: No response from RPC

Cause: RPC endpoint is slow or unresponsive.

Solutions:

  1. Use a different RPC endpoint
  2. Increase timeout:
const client = new SuiClient({
url: 'https://fullnode.mainnet.sui.io:443',
timeout: 60000, // 60 seconds
});
  1. Consider using a private RPC provider

Invalid Object ID

Error Message:

Error: Invalid object ID format

Cause: Object ID is not properly formatted.

Solution: Ensure object IDs are 32-byte hex strings:

// - Correct
const objectId = '0x1234...'; // 64 hex chars (32 bytes)

// - Incorrect
const objectId = '0x123'; // Too short
const objectId = '1234...'; // Missing 0x prefix

Type Mismatch

Error Message:

Error: Type mismatch: expected '0x...::coin::Coin<0x...::token::TOKEN>', got '0x...::coin::Coin<0x2::sui::SUI>'

Cause: Wrong coin type passed to function.

Solution: Verify coin types match:

// Check coin type
const coin = await client.getObject({id: coinId});
console.log('Coin type:', coin.data?.type);

// Use correct coin type
const result = await oft.send({
tokenMint: '0x...::token::TOKEN', // Must match
// ...
});

Debugging Tips

Enable Verbose Logging

# Sui CLI with verbose output
sui client call ... --json | jq .

Check Transaction Effects

const result = await client.signAndExecuteTransaction({
transaction: tx,
options: {
showEffects: true,
showEvents: true,
showObjectChanges: true,
},
});

console.log('Effects:', result.effects);
console.log('Events:', result.events);
console.log('Object changes:', result.objectChanges);

Inspect Objects

# View object details
sui client object $OBJECT_ID --json

# View all objects for an address
sui client objects --json

Use Sui Explorer

Navigate to SuiScan to:

  • View transaction details
  • Check object states
  • Inspect event logs
  • Verify package deployments

Test on Devnet First

Always test on devnet before testnet/mainnet:

# Switch to devnet
sui client switch --env devnet

# Test your calls
sui client call ... --gas-budget 20000000

Getting Help

If you continue to experience issues:

  1. Check Documentation: Review Sui Documentation
  2. Search Discord: Look for similar issues in LayerZero Discord
  3. Ask for Help: Post in Discord with:
    • Error message
    • Transaction hash (if available)
    • Code snippet
    • What you've tried

Next Steps