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:
- Split into multiple packages
- Remove unused dependencies
- Optimize data structures
- 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:
- Verify you're using the correct account
- Check UpgradeCap ownership:
sui client objects | grep UpgradeCap
- 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:
- Use default libraries (don't set custom)
- 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::Packagefor 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
InvalidBCSByteserror
Common Scenarios Requiring asBytes():
-
- DVN/ULN configuration
-
- Execution options
-
- OApp info parameters
-
- Any
vector<u8>config parameter
- Any
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:
- 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(...);
- 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:
- Missing confirmation call (e.g.,
confirm_lz_send) - PTB doesn't route the
Callthrough all required modules Callobject 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
Callcreation has a corresponding confirm call - PTB includes all required routing steps
- No early returns that skip confirmation
- All
Callobjects 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:
- Verify recipient address is valid
- For token sends, check if recipient needs an account created
- 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:
- Check transaction parameters are valid
- Verify all required objects exist
- Ensure signer has necessary permissions
- 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:
- 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)
-
Use correct signer: Ensure the transaction signer owns the object
-
Check object type:
- Owned objects (
AdminCap,CallCap): Must be owned by signer - Shared objects (
OApp,EndpointV2): Accessible by anyone, use references (&or&mut) - Immutable objects (
CoinMetadata): Read-only references only
- Owned objects (
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:
- Use a different RPC endpoint
- Increase timeout:
const client = new SuiClient({
url: 'https://fullnode.mainnet.sui.io:443',
timeout: 60000, // 60 seconds
});
- 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:
- Check Documentation: Review Sui Documentation
- Search Discord: Look for similar issues in LayerZero Discord
- Ask for Help: Post in Discord with:
- Error message
- Transaction hash (if available)
- Code snippet
- What you've tried