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 creatingCallobjects (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:
- IOTA Multisig Addresses: Native 1-of-n or k-of-n multisig
- Shared Control Objects: Create a shared configuration object requiring multiple approvals
- 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
- 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.