Skip to main content
Version: Endpoint V2

IOTA L1 Development Guidance

This page provides development guidance for building LayerZero applications on IOTA, covering toolchain setup, operational practices, and technical constraints.

Toolchain Setup

IOTA CLI

Tested Version: iota@v1.54.1

Install the IOTA CLI:

cargo install --locked --git https://github.com/iotaledger/iota.git --branch mainnet iota

Verify installation:

iota --version
# iota 1.54.1-...

Project Structure

Typical IOTA Move project structure:

my-oapp/
├── Move.toml # Package manifest
├── sources/
│ ├── oapp.move # Main OApp logic
│ ├── config.move # Configuration
│ └── ...
├── tests/
│ └── oapp_tests.move # Unit tests
└── scripts/
└── deploy.sh # Deployment scripts

Move.toml Configuration

For package structure details:

[package]
name = "my_oapp"
version = "0.0.1"

[dependencies]
IOTA = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "mainnet" }
LayerZeroEndpoint = { git = "https://github.com/LayerZero-Labs/LayerZero-v2.git", subdir = "packages/layerzero-v2/iota/contracts/endpoint-v2", rev = "main" }
LayerZeroOApp = { git = "https://github.com/LayerZero-Labs/LayerZero-v2.git", subdir = "packages/layerzero-v2/iota/contracts/oapps/oapp", rev = "main" }

[addresses]
my_oapp = "0x0"
iota = "0x2"

Development Environment

Building Contracts

iota move build

Running Tests

iota move test

Local Development

Start a local IOTA network:

iota start

Deployment

Deploying to Testnet

iota client publish \
--gas-budget 100000000 \
--json

Deploying to Mainnet

iota client switch --env mainnet
iota client publish \
--gas-budget 100000000 \
--json

Deployment Script Example

#!/bin/bash

# Build
echo "Building..."
iota move build

# Deploy
echo "Deploying..."
RESULT=$(iota client publish \
--gas-budget 100000000 \
--json)

# Extract package ID
PACKAGE_ID=$(echo $RESULT | jq -r '.objectChanges[] | select(.type=="published") | .packageId')

echo "Package ID: $PACKAGE_ID"

# Save to file
echo $PACKAGE_ID > deployed_package.txt

Operational Practices

Package Upgrades

IOTA packages are immutable by default but can be made upgradeable.

UpgradeCap: Owned object granting upgrade authority

/// Automatically created when publishing with --with-unpublished-dependencies
public struct UpgradeCap has key, store {
id: UID,
package: ID, // Package being controlled
version: u64, // Current version
policy: u8, // Upgrade policy (compatible, additive, dep_only)
}

Transfer Upgrade Authority:

iota client transfer \
--to <NEW_OWNER_ADDRESS> \
--object-id <UPGRADE_CAP_OBJECT_ID> \
--gas-budget 10000000

Upgrade a Package:

iota client upgrade \
--upgrade-capability <UPGRADE_CAP_OBJECT_ID> \
--gas-budget 200000000

Key Point: Upgraded packages maintain compatibility with objects created by previous versions, provided you follow IOTA's upgrade policies.

Capability Management

LayerZero uses multiple capability objects:

For OApp/OFT Packages:

  • CallCap: Authorizes creating Call objects (usually stored in package module)
  • AdminCap: Authorizes admin operations (transfer to new admin as needed)
  • MigrationCap: Authorizes migrating OApp/OFT to new implementations (store securely)
  • TreasuryCap<T>: Authorizes minting/burning coins (for OFT mint/burn type)
  • UpgradeCap: Authorizes package upgrades (transfer with caution)

Transfer Pattern:

// Transfer owned object to new owner
transfer::public_transfer(admin_cap, new_admin_address);

No Safe Transfer: IOTA doesn't have EVM's safeTransfer callback. Transfers are direct:

transfer::public_transfer(object, recipient);  // Direct, no callback

Multisig Patterns

For multi-party control, use:

  1. IOTA Multisig Addresses: Native 1-of-n or k-of-n multisig
  2. Shared Control Objects: Create a shared configuration object requiring multiple approvals
  3. Third-Party Solutions: IOTA Wallet multisig, protocol-specific multisig

Example using address derivation:

# Create multisig address with multiple public keys
iota keygen multi-sig \
--pks <PUBKEY1> <PUBKEY2> <PUBKEY3> \
--weights 1 1 1 \
--threshold 2

Resource & Fee Models

See IOTA Gas Model for complete details.

Storage Gas

Charged for storing data on-chain:

// Creating objects costs storage gas
let obj = MyObject { id: object::new(ctx), data: ... };
transfer::share_object(obj); // Storage charged here

Computation Gas

Charged for execution:

// Complex logic costs computation gas
public fun complex_operation(...) {
// Each instruction consumes gas
let result = heavy_computation();
// ...
}

Rebate Mechanism

When storage is freed, gas is refunded:

// Deleting objects triggers rebate
let MyObject { id, data } = obj;
object::delete(id); // Storage rebate issued

Important: This can result in negative gas utilization for net storage reduction.

Base Budget

Every transaction requires a minimum of 1000 gas units, even if net cost is negative due to rebates.

Technical Constraints

Package Size Limit

Maximum size per package: 250 KiB

If your package exceeds this:

  • Split into multiple packages
  • Reduce unused code
  • Optimize data structures

Transaction Size Constraints

See IOTA Transaction Limits:

  • Max objects per transaction: 256
  • Max events per transaction: 1024
  • Max argument size: 128 KB

Compute Limits

Gas limits vary by network:

  • Testnet: Lower limits
  • Mainnet: Higher limits

For LayerZero operations, budget at least:

  • Simple send: 5,000,000 gas
  • Complex send: 20,000,000 gas
  • Receive: 10,000,000 gas

Network Resource Limits

Monitor these limits:

  • Object count per address: Unlimited, but impacts query performance
  • Storage per address: Unlimited, but costs scale linearly
  • Transaction throughput: ~5,000 TPS (network-wide)

Network Considerations

Finality

IOTA uses a checkpoint-based finality system:

  • Soft finality: Certificate of transaction (milliseconds)
  • Hard finality: Checkpoint inclusion (~2-3 seconds)

For LayerZero verification, DVNs wait for checkpoint finality.

RPC Infrastructure

Public RPCs:

  • Mainnet: https://fullnode.mainnet.iota.io:443
  • Testnet: https://fullnode.testnet.iota.io:443
  • Devnet: https://fullnode.devnet.iota.io:443

Private RPC Providers:

  • Ankr
  • QuickNode
  • Blast API

For production, use private RPCs for better reliability and rate limits.

Channel Management

Recovery Methods

LayerZero provides recovery methods for stuck messages:

// Skip a message
public entry fun skip(
oapp: &mut OApp,
admin_cap: &AdminCap,
src_eid: u32,
sender: vector<u8>,
nonce: u64,
)

// Clear a message
public entry fun clear(
oapp: &mut OApp,
admin_cap: &AdminCap,
src_eid: u32,
sender: vector<u8>,
nonce: u64,
)

// Nilify a message
public entry fun nilify(
oapp: &mut OApp,
admin_cap: &AdminCap,
src_eid: u32,
sender: vector<u8>,
nonce: u64,
)

Authorization: All recovery methods require the AdminCap object.

Querying State with TypeScript SDKs

The IOTA CLI has limitations for querying state. Use TypeScript SDKs instead:

import {IOTAClient} from '@iota/iota-sdk/client';
import {OApp} from '@layerzerolabs/lz-iotal1-sdk-v2';

const client = new IOTAClient({url: 'https://fullnode.mainnet.iota.io:443'});

// Query peer configuration
const peer = await oapp.getPeer(client, 30101);

// Query nonce
const nonce = await oapp.getInboundNonce(client, 30101, senderBytes32);

// Query configuration
const config = await oapp.getConfig(client, 30101);

IOTA CLI Limitations

The IOTA CLI cannot easily:

  • Parse complex return values from view functions
  • Handle nested data structures
  • Decode bytes arrays

Workaround: Use the TypeScript SDK for all state queries.

Best Practices

1. Test Thoroughly

# Run unit tests
iota move test

# Run integration tests on testnet
iota client call --package $PKG ... --json

2. Monitor Gas Usage

# Use --gas-budget appropriately
iota client call \
--gas-budget 20000000 \ # Start higher
--json

3. Handle Rebates Correctly

// Don't assume gas cost is always positive
// Rebates can make net cost negative

4. Version Your Packages

[package]
name = "my_oapp"
version = "1.0.0" # Increment on upgrades

5. Secure Your Keys

# Use hardware wallets for mainnet
# Keep upgrade capabilities secure
# Use multisig for critical operations

Common Gotchas

1. Negative Gas Utilization

When storage is freed, transactions can have negative net gas cost. Budget at least 1000 base units.

2. Package Size Exceeded

Error: Package size exceeds maximum

Solution: Split into multiple packages or optimize code.

3. Object Ownership Errors

Error: Invalid object ownership

Solution: Verify object is owned by signer or is shared.

4. Insufficient Gas

Error: Insufficient gas

Solution: Increase --gas-budget parameter.

Additional Resources

Next Steps