5. MoveCall - counter::lz_receive
- Receives the Call object
- Validates and processes
- Destroys the Call
Objects used:
- 0x5f24...0c8e: Executor shared object (immutable)
- 0x00a7...9fc2: Executor CallCap (owned)
- 0xd45b...bf91: EndpointV2 shared object (immutable)
- 0x9b01...1843: MessagingChannel shared object (mutable)
- 0x6903...8c4d: Counter OApp shared object (mutable)
- 0x224b...fbb3: Counter Peer shared object (immutable)
- 0x608a...6f27: Counter's internal state (mutable)
// Events emitted:
- PacketDeliveredEvent
```
***
## Key IOTA-Specific Patterns
### Object Ownership in Message Flow
| Object Type | Ownership | Access Pattern | Example |
| ------------------ | --------- | -------------------------- | ---------------------------------- |
| `EndpointV2` | Shared | Anyone reads, admin writes | `&EndpointV2` or `&mut EndpointV2` |
| `MessagingChannel` | Shared | Anyone reads, owner writes | `&mut MessagingChannel` |
| `OApp` | Shared | Anyone reads, admin writes | `&mut OApp` |
| `CallCap` | Owned | Must own to use | Owned by OApp module or user |
| `AdminCap` | Owned | Must own to use | Owned by admin address |
| `Call` | Neither | Must be consumed in PTB | Created and destroyed in same TX |
### Call Pattern vs EVM/Solana
| Aspect | EVM | Solana | IOTA |
| ------------------------ | --------------------- | ------------------------------ | --------------------------------- |
| **Cross-Contract Calls** | `delegatecall` | CPI (Cross-Program Invocation) | `Call` objects |
| **Authorization** | `msg.sender` | Signer checks + PDAs | `CallCap` validation |
| **Return Values** | Function returns | CPI returns | `Call.complete()` sets result |
| **Call Hierarchy** | Call stack (implicit) | CPI depth limit (4) | `Call` parent/child relationships |
| **Atomicity** | Transaction revert | Transaction revert | PTB revert |
### Nonce Management
**EVM**:
```solidity wrap theme={null}
// Mapping-based nonce storage
mapping(address => mapping(uint32 => mapping(bytes32 => uint64))) outboundNonce;
```
**Solana**:
```rust wrap theme={null}
// PDA account per pathway
#[account(seeds = [NONCE_SEED, sender, dst_eid, receiver], bump)]
pub nonce: Account<'info, Nonce>,
```
**IOTA**:
```rust wrap theme={null}
// Table within MessagingChannel, nested in Channel struct
public struct MessagingChannel has key {
channels: Table, // Maps (eid, remote_oapp) → Channel
}
public struct Channel has store {
outbound_nonce: u64, // Incremented on each send
lazy_inbound_nonce: u64, // Last executed inbound nonce
inbound_payload_hashes: Table, // Maps nonce → hash
}
```
### Fee Payment Model
**EVM**:
```solidity wrap theme={null}
// Direct transfer in msg.value
Transfer.native(executor, executorFee);
```
**Solana**:
```rust wrap theme={null}
// Token account transfer
transfer(from_account, to_account, amount);
```
**IOTA**:
```rust wrap theme={null}
// Coin object splitting and transfer
let fee_coin = coin::split(&mut provided_coin, fee_amount, ctx);
transfer::public_transfer(fee_coin, recipient_address);
```
***
## Configuration Management
### Send Library Configuration
OApps can set custom send libraries per destination:
```rust wrap theme={null}
/// From endpoint_v2 (called by OApp with AdminCap)
public fun set_send_library(
self: &mut EndpointV2,
caller: &CallCap,
oapp: address,
dst_eid: u32,
new_lib: address,
) {
self.assert_authorized(caller.id(), oapp);
self.message_lib_manager.set_send_library(oapp, dst_eid, new_lib);
}
```
**Default Fallback**:
```rust wrap theme={null}
/// From message_lib_manager
public(package) fun get_send_library(
self: &MessageLibManager,
sender: address,
dst_eid: u32,
): (address, bool) {
// Try OApp-specific config first
let key = SendLibraryKey { sender, dst_eid };
if (self.send_libraries.contains(key)) {
return (self.send_libraries[key], false) // Custom library
};
// Fall back to default
let default_lib = self.default_send_libraries[dst_eid];
(default_lib, true) // Default library
}
```
### DVN Configuration
OApps configure DVN sets through the ULN:
```rust wrap theme={null}
/// ULN configuration structure
public struct UlnConfig has copy, drop, store {
confirmations: u64, // Block confirmations required
required_dvn_count: u8, // Number of required DVNs
optional_dvn_count: u8, // Number of optional DVNs
optional_dvn_threshold: u8, // How many optional DVNs must verify
required_dvns: vector, // Required DVN addresses
optional_dvns: vector, // Optional DVN addresses
}
```
**Setting Configuration** (via Endpoint):
```rust wrap theme={null}
/// From endpoint_v2::set_config()
public fun set_config(
self: &mut EndpointV2,
caller: &CallCap, // OApp or delegate
oapp: address,
config_type: u32,
eid: u32,
config: vector,
ctx: &mut TxContext,
) {
self.assert_authorized(caller.id(), oapp);
// Get message library
let (lib, _) = if (is_send_config(config_type)) {
self.message_lib_manager.get_send_library(oapp, eid)
} else {
self.message_lib_manager.get_receive_library(oapp, eid, clock)
};
// Create Call to library's set_config
let set_config_param = message_lib_set_config::create_param(oapp, config_type, eid, config);
let call = call::create(caller, lib, true, set_config_param, ctx);
// Library processes configuration immediately (one-way call)
// No confirmation needed
}
```
***
## Recovery Operations
The Endpoint provides several recovery mechanisms for stuck or problematic messages.
### Skip
Increments the lazy nonce without executing the message:
```rust wrap theme={null}
/// From endpoint_v2::skip()
public fun skip(
self: &EndpointV2,
caller: &CallCap,
messaging_channel: &mut MessagingChannel,
src_eid: u32,
sender: Bytes32,
nonce: u64,
) {
self.assert_authorized(caller.id(), messaging_channel.oapp());
messaging_channel.skip(src_eid, sender, nonce);
}
```
**Inside `messaging_channel::skip()`**:
```rust wrap theme={null}
public(package) fun skip(
self: &mut MessagingChannel,
src_eid: u32,
sender: Bytes32,
nonce: u64,
) {
let channel_key = ChannelKey { remote_eid: src_eid, remote_oapp: sender };
let channel = &mut self.channels[channel_key];
// Validate nonce is next expected
assert!(nonce == channel.lazy_inbound_nonce + 1, EInvalidNonce);
// Increment lazy nonce (skipping this message)
channel.lazy_inbound_nonce = nonce;
event::emit(InboundNonceSkippedEvent {
src_eid,
sender,
receiver: self.oapp,
nonce,
});
}
```
### Nilify
Removes verification but keeps nonce ordering:
```rust wrap theme={null}
public fun nilify(
self: &EndpointV2,
caller: &CallCap,
messaging_channel: &mut MessagingChannel,
src_eid: u32,
sender: Bytes32,
nonce: u64,
payload_hash: Bytes32,
) {
self.assert_authorized(caller.id(), messaging_channel.oapp());
messaging_channel.nilify(src_eid, sender, nonce, payload_hash);
}
```
### Burn
Permanently blocks a nonce (irreversible):
```rust wrap theme={null}
public fun burn(
self: &EndpointV2,
caller: &CallCap,
messaging_channel: &mut MessagingChannel,
src_eid: u32,
sender: Bytes32,
nonce: u64,
payload_hash: Bytes32,
) {
self.assert_authorized(caller.id(), messaging_channel.oapp());
messaging_channel.burn(src_eid, sender, nonce, payload_hash);
}
```
***
## Comparison with EVM and Solana
### Architecture Comparison
| Component | EVM | Solana | IOTA |
| --------------------- | ---------------------- | ------------------------ | ------------------------------ |
| **Code Organization** | Solidity contracts | Rust programs | Move packages/modules |
| **State Storage** | Contract storage slots | PDA accounts | Shared/owned objects |
| **Nonce Management** | Nested mappings | PDA per pathway | Table in MessagingChannel |
| **Message Channel** | Contract storage | PDA accounts | MessagingChannel shared object |
| **Call Pattern** | delegatecall | CPI | `Call` objects |
| **Authorization** | msg.sender | Signers + PDA derivation | CallCap validation |
| **Fee Payment** | msg.value transfer | Token account ops | Coin object splitting |
| **Atomicity** | Transaction revert | Transaction revert | PTB revert |
### Send Flow Comparison
| Step | EVM | Solana | IOTA |
| ------------------ | --------------------- | ----------------------- | ------------------------- |
| **Initiate** | OApp.send() internal | OApp CPI to Endpoint | OApp creates Call object |
| **Nonce** | Mapping increment | PDA account write | Table field increment |
| **Library Call** | Direct function call | CPI to SendUln302 | Child Call creation |
| **Worker Calls** | Direct function calls | CPI to each worker | Child Call per worker |
| **Fee Collection** | Transfer to library | Record in library | Coin splitting |
| **Event** | `emit PacketSent` | `emit_cpi!(PacketSent)` | `event::emit(PacketSent)` |
### Receive Flow Comparison
| Step | EVM | Solana | IOTA |
| ------------------- | ----------------------------------- | ---------------------------------- | ------------------------------------ |
| **Entry Point** | Executor calls Endpoint.lzReceive | Executor invokes with all accounts | Executor calls execute\_lz\_receive |
| **Clear Payload** | Endpoint clears before calling OApp | OApp CPIs back to Endpoint.clear | Endpoint clears before creating Call |
| **OApp Invocation** | delegatecall to OApp.lzReceive | Instruction with account list | Call object to OApp module |
| **Validation** | Modifier checks | Account constraints + CPI auth | Call validation + peer check |
| **Processing** | Override \_lzReceive | Implement lz\_receive instruction | Implement lz\_receive function |
***
## Event Monitoring
### Events Emitted During Send
1. **ExecutorFeePaidEvent** (from ULN):
```rust wrap theme={null}
public struct ExecutorFeePaidEvent has copy, drop {
guid: Bytes32,
executor: address,
fee: FeeRecipient,
}
```
2. **DVNFeePaidEvent** (from ULN):
```rust wrap theme={null}
public struct DVNFeePaidEvent has copy, drop {
guid: Bytes32,
dvns: vector,
fees: vector,
}
```
3. **PacketSentEvent** (from MessagingChannel):
```rust wrap theme={null}
public struct PacketSentEvent has copy, drop {
encoded_packet: vector, // Full packet with header + payload
options: vector, // Execution options
send_library: address, // Library that processed send
}
```
4. **OFTSentEvent** (from OFT, if applicable):
```rust wrap theme={null}
public struct OFTSentEvent has copy, drop {
guid: Bytes32,
dst_eid: u32,
from_address: address,
amount_sent_ld: u64, // Amount in local decimals
amount_received_ld: u64, // Amount after dust removal
}
```
### Events Emitted During Verification
1. **PayloadVerifiedEvent** (per DVN):
```rust wrap theme={null}
public struct PayloadVerifiedEvent has copy, drop {
dvn: address,
header: vector,
confirmations: u64,
proof_hash: Bytes32,
}
```
2. **PacketVerifiedEvent** (after commit):
```rust wrap theme={null}
public struct PacketVerifiedEvent has copy, drop {
src_eid: u32,
sender: Bytes32,
nonce: u64,
receiver: address,
payload_hash: Bytes32,
}
```
### Events Emitted During Receive
1. **PacketDeliveredEvent**:
```rust wrap theme={null}
public struct PacketDeliveredEvent has copy, drop {
src_eid: u32,
sender: Bytes32,
receiver: address,
nonce: u64,
}
```
2. **OFTReceivedEvent** (from OFT, if applicable):
```rust wrap theme={null}
public struct OFTReceivedEvent has copy, drop {
guid: Bytes32,
src_eid: u32,
to_address: address,
amount_received_ld: u64,
}
```
***
## Capabilities and Authorization
### CallCap Pattern
`CallCap` is IOTA's capability-based authorization for creating `Call` objects:
```rust wrap theme={null}
/// From call_cap module
public struct CallCap has key, store {
id: UID,
package_id: address, // Package that owns this capability
}
/// Create a package-level CallCap using one-time witness
public fun new_package_cap(otw: &T, ctx: &mut TxContext): CallCap {
CallCap {
id: object::new(ctx),
package_id: package::from_witness(otw),
}
}
```
**Usage in Validation**:
```rust wrap theme={null}
/// OApp validates its CallCap
fun assert_oapp_cap(self: &OApp, cap: &CallCap) {
assert!(self.oapp_cap.id() == cap.id(), EInvalidOAppCap);
}
/// Endpoint validates library CallCap
fun assert_send_library(lib_cap: &CallCap, expected_lib: address) {
assert!(lib_cap.id() == expected_lib, EUnauthorizedSendLibrary);
}
```
### AdminCap Pattern
`AdminCap` authorizes administrative operations:
```rust wrap theme={null}
public struct AdminCap has key, store {
id: UID,
}
/// Setting a peer requires AdminCap
public fun set_peer(
self: &mut OApp,
admin_cap: &AdminCap, // Must own this object
eid: u32,
peer: Bytes32,
) {
// AdminCap ownership proves authorization
self.peer.set_peer(self.oapp_object_address(), eid, peer);
}
```
**Ownership Transfer**:
```rust wrap theme={null}
// Transfer AdminCap to new admin
transfer::public_transfer(admin_cap, new_admin_address);
```
***
## PTB Construction Patterns
### Simple Send PTB
```typescript wrap theme={null}
const tx = new Transaction();
// 1. Split fee from gas coin
const [feeCoin] = tx.splitCoins(tx.gas, [tx.pure.u64(feeAmount)]);
// 2. Create send parameters
const sendParam = tx.moveCall({
target: `${oappPackage}::oapp::create_send_param`,
arguments: [
tx.pure.u32(dstEid),
tx.pure.vector('u8', receiverBytes),
tx.pure.vector('u8', messageBytes),
tx.pure.vector('u8', optionsBytes),
feeCoin,
tx.pure.option('object', null), // No ZRO payment
tx.pure.address(refundAddress),
],
});
// 3. Call OApp send (returns Call object)
const sendCall = tx.moveCall({
target: `${oappPackage}::oapp::send`,
arguments: [tx.object(oappObjectId), tx.object(callCapObjectId), sendParam],
});
// 4. Route through Endpoint (processes Call)
const libCall = tx.moveCall({
target: `${endpointPackage}::endpoint_v2::send`,
arguments: [tx.object(endpointObjectId), tx.object(messagingChannelId), sendCall],
});
// 5-N. Worker assignments (handled by PTB builder)
// N+1. Confirm send (destroys Call, extracts receipt)
tx.moveCall({
target: `${oappPackage}::oapp::confirm_lz_send`,
arguments: [
tx.object(oappObjectId),
tx.object(callCapObjectId),
sendCall, // Original Call object (now completed)
],
});
await client.signAndExecuteTransaction({transaction: tx});
```
### Receive PTB
```typescript wrap theme={null}
const tx = new Transaction();
// 1. Decode parameters
const senderBytes32 = tx.moveCall({
target: `${utilsPackage}::bytes32::from_bytes`,
arguments: [tx.pure.vector('u8', senderBytes)],
});
const guidBytes32 = tx.moveCall({
target: `${utilsPackage}::bytes32::from_bytes`,
arguments: [tx.pure.vector('u8', guidBytes)],
});
// 2. Create empty value option (no native transfer)
const noValue = tx.moveCall({
target: '0x1::option::none',
typeArguments: ['0x2::coin::Coin<0x2::iota::SUI>'],
arguments: [],
});
// 3. Execute lz_receive via Executor
const lzReceiveCall = tx.moveCall({
target: `${executorPackage}::executor_worker::execute_lz_receive`,
arguments: [
tx.object(executorObjectId), // Executor shared object
tx.object(executorCapId), // Executor CallCap
tx.object(endpointObjectId), // Endpoint shared object
tx.object(messagingChannelId), // Messaging channel
tx.pure.u32(srcEid),
senderBytes32,
tx.pure.u64(nonce),
guidBytes32,
tx.pure.vector('u8', messageBytes),
tx.pure.vector('u8', extraDataBytes),
noValue,
],
});
// 4. OApp processes (receives Call object from step 3)
tx.moveCall({
target: `${oappPackage}::counter::lz_receive`,
arguments: [
tx.object(counterObjectId), // OApp shared object
tx.object(peerObjectId), // Peer validation
lzReceiveCall, // Call from executor
],
});
await client.signAndExecuteTransaction({transaction: tx});
```
***
## IOTA-Specific Considerations
### Object Abilities
IOTA's [ability system](https://docs.iota.org/developer/iota-101/move-overview/structs-and-abilities/abilities-intro) controls what can be done with types:
| Ability | Meaning | LayerZero Usage |
| ------- | ------------------------------ | ---------------------------------- |
| `key` | Can be stored at top-level | `OApp`, `EndpointV2`, `AdminCap` |
| `store` | Can be stored in other structs | `Peer`, `Channel`, `UlnConfig` |
| `copy` | Can be copied | `ChannelKey`, `MessagingFee` |
| `drop` | Can be ignored/discarded | One-time witnesses, config structs |
**Call Object Abilities**:
```rust wrap theme={null}
public struct Call {
// Has NO abilities - cannot be dropped or stored
// Must be explicitly destroyed via destroy() or complete_and_destroy()
}
```
This enforces the hot potato pattern—`Call` objects must be handled.
### Phantom Type Parameters
IOTA uses [phantom type parameters](https://move-book.com/move-basics/generics/#phantom-type-parameters) for type safety without storage:
```rust wrap theme={null}
/// OFT uses phantom T for the coin type
public struct OFT has key {
id: UID,
treasury: OFTTreasury, // T only appears in nested types
// ...
}
/// TreasuryCap also uses phantom T
public struct TreasuryCap has key, store {
id: UID,
total_supply: Supply,
}
```
The `phantom` keyword means `T` is for type safety only—not stored directly.
### Table vs Vector
IOTA uses [`Table`](https://docs.iota.org/references/framework/table) for dynamic key-value storage:
```rust wrap theme={null}
/// Peer mappings by EID
public struct Peer has store {
peers: Table, // EID → peer address
}
/// vs fixed-size vector
required_dvns: vector, // Known size, stored directly
```
**Trade-offs**:
* `Table`: Dynamic size, gas per access, better for sparse data
* `vector`: Fixed size, cheaper access, better for dense data
***
## Summary
LayerZero on IOTA achieves crosschain messaging through:
1. **Object-Based State**: Shared objects (`EndpointV2`, `MessagingChannel`, `OApp`) enable parallel execution
2. **Capability Authorization**: `CallCap` and `AdminCap` replace `msg.sender` checks
3. **Call Pattern**: `Call` objects enable dynamic routing without `delegatecall`
4. **PTB Composition**: Atomic multi-step workflows ensure message integrity
5. **Type Safety**: Move's ability system and phantom types provide compile-time guarantees
**Key Differences from Other VMs**:
* No inheritance (explicit capability validation)
* No dynamic dispatch (Call pattern workaround)
* Object ownership model (shared vs owned vs immutable)
* Coin object model (split/merge instead of balance transfer)
* Table-based storage (not mappings or PDAs)
For implementation guides and code examples, see:
* [OApp Implementation](/v2/developers/iota/oapp/overview) - Build custom crosschain applications
* [OFT Implementation](/v2/developers/iota/oft/overview) - Deploy crosschain tokens
* [OFT SDK](/v2/developers/iota/oft/sdk) - Complete SDK methods and patterns
* [Configuration Guide](/v2/developers/iota/configuration/dvn-executor-config) - DVN, executor, and gas configuration
* [Technical Overview](/v2/developers/iota/technical-overview) - IOTA fundamentals and architecture
# IOTA L1 Fundamentals for LayerZero Developers
Source: https://docs.layerzero.network/v2/developers/iota/technical-overview
Overview of IOTA L1 Fundamentals for Developers on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables...
This page introduces the IOTA-specific concepts you need to understand before building LayerZero applications. If you're coming from EVM or Solana, this guide explains how IOTA differs and why LayerZero's implementation works the way it does.
**What you'll learn**:
* IOTA's object model vs EVM's account model
* Why dynamic dispatch doesn't work and how the Call pattern solves it
* Capabilities for authorization instead of `msg.sender`
* Programmable Transaction Blocks (PTBs) for atomic multi-step execution
* Gas model differences and rebate mechanism
For complete protocol workflows with detailed code, see [Protocol Overview](/v2/developers/iota/protocol-overview). For hands-on implementation, see [OApp](/v2/developers/iota/oapp/overview) or [OFT](/v2/developers/iota/oft/overview) guides.
## VM Architecture
IOTA uses the Move programming language and employs an [object-based model](https://docs.iota.org/developer/iota-101/objects/object-model) rather than the account-based model used by EVM chains. This fundamental difference requires different patterns for implementing crosschain functionality.
### IOTA Object Model
IOTA organizes state into [**objects**](https://docs.iota.org/developer/iota-101/objects/object-model) with different [ownership types](https://docs.iota.org/developer/iota-101/objects/object-ownership). For an introduction to IOTA's object model, see [Getting Started](/v2/developers/iota/getting-started#object-ownership-types).
LayerZero uses all three ownership types:
* **Shared**: `EndpointV2`, `MessagingChannel`, `OApp`, `OFT` (accessible by anyone, mutable by authorized)
* **Owned**: `AdminCap`, `CallCap` (belong to specific address, used for authorization)
* **Immutable**: Published packages, `CoinMetadata` (read-only, never change)
Each object has:
* **Unique ID** ([`UID`](https://docs.iota.org/developer/iota-101/objects/uid-id)): Globally unique identifier
* [**Abilities**](https://docs.iota.org/developer/iota-101/move-overview/structs-and-abilities/abilities-intro): Define what operations are allowed (`key`, `store`, `copy`, `drop`)
* **Type**: Determines structure and behavior
### No Dynamic Dispatch
Unlike EVM chains that support dynamic dispatch through `delegatecall`, **IOTA does not support dynamic dispatch**. Function calls must target modules known at compile time.
**Why this matters**: The LayerZero Endpoint needs to call back into OApp modules whose addresses vary per deployment—not known when the Endpoint is published. This architectural constraint requires a different approach.
### Call Pattern (Hot Potato)
LayerZero solves the dynamic dispatch limitation using a capability-based pattern called "[hot potato](https://docs.iota.org/developer/iota-101/move-overview/patterns/hot-potato)."
To achieve dynamic routing, LayerZero uses the **Call pattern**—a capability-based [hot potato implementation](https://docs.iota.org/developer/iota-101/move-overview/patterns/hot-potato).
The `Call` struct:
* Has **no** `drop` or `store` abilities (cannot be ignored or saved)
* Can only be created by the caller module
* Must be consumed by the designated callee
* Enforces proper sequencing through lifecycle states
* Returns results back to the caller
**Call Lifecycle**:
```
Active → Creating (child calls) → Waiting → Active → Completed → Destroyed
```
This ensures atomicity: if any step fails, the entire PTB reverts.
### Programmable Transaction Blocks (PTBs)
IOTA's execution model centers around [**Programmable Transaction Blocks**](https://docs.iota.org/developer/iota-101/transactions/ptb/programmable-transaction-blocks)—atomic command sequences that:
* Execute multiple Move function calls
* Pass objects and results between calls
* Guarantee all-or-nothing execution
* Enable complex multi-contract workflows
* Support up to 1024 commands per block
## Message Flow Overview
LayerZero messages on IOTA flow through multiple modules using the Call pattern within a Programmable Transaction Block.
**High-Level Flow**:
```
Send: OApp → Endpoint → ULN302 → Workers → Confirmation chain
Receive: Executor → Endpoint (clear) → OApp (validate & process)
```
**Key Mechanisms**:
* **Call pattern**: Dynamic routing through `Call` objects
* **PTB coordination**: All steps happen atomically in one transaction
* **Capability validation**: Each module validates CallCap ownership
* **Storage management**: MessagingChannel tracks nonces and payload hashes
### Complete Protocol Details
For detailed send/verify/receive workflows with contract code, struct definitions, and transaction analysis, see [Protocol Overview](/v2/developers/iota/protocol-overview).
## Transaction Execution Model
IOTA supports two types of function calls, each serving different purposes in the LayerZero protocol.
### Static Calls
Used when the target module is known at compile time:
* Direct function invocation within a PTB
* No intermediate `Call` object needed
* Example: OApp calling Endpoint (Endpoint object ID is known)
```rust wrap theme={null}
// Direct call (static)
endpoint::init_channel(&mut endpoint, &call_cap, remote_eid);
```
### Call Pattern (Dynamic Routing)
Used when the target module is not known at compile time:
* Caller creates a `Call` object
* PTB routes the `Call` to the appropriate module
* Recipient processes and completes the `Call`
* Caller confirms the `Call` to extract results
* Example: Endpoint routing to OApp (OApp object ID varies per deployment)
```rust wrap theme={null}
// Create Call
let call = oapp::lz_send(&mut oapp, &call_cap, ...);
// PTB routes Call through Endpoint
// Confirm to extract results
let (_, receipt) = oapp::confirm_lz_send(&oapp, &call_cap, call);
```
### Atomicity Guarantees
All operations within a PTB are atomic:
* If any step fails, the entire transaction reverts
* No partial state changes
* Enables complex multi-step operations with safety guarantees
**For Implementation Details**: See [Protocol Overview](/v2/developers/iota/protocol-overview) for complete workflows including:
* Nonce management and packet construction
* Worker assignment and fee aggregation
* DVN verification and threshold checking
* Message delivery and payload clearing
## State Management Model
LayerZero on IOTA uses shared and owned objects to manage configuration and message state, rather than EVM-style storage slots.
### LayerZero State Storage
State is organized into objects with different ownership types, each serving specific purposes:
| State Type | Storage Location | Ownership Type |
| ---------------------- | --------------------------------- | ------------------------------------------ |
| **Endpoint** | `EndpointV2` shared object | Shared (anyone can read, admin can modify) |
| **OApp Configuration** | `OApp` shared object | Shared (owner via `AdminCap`) |
| **OApp Peer Mappings** | `Peer` struct within `OApp` | Embedded (has `store` ability) |
| **Messaging Channels** | `MessagingChannel` shared objects | Shared (created per OApp) |
| **Library Configs** | Objects within `Uln302` | Shared object fields |
| **Admin Authority** | `AdminCap` owned objects | Owned (transferable to new admin) |
**Key Concepts**:
* [**Shared objects**](https://docs.iota.org/developer/iota-101/objects/object-ownership/shared): Created with `transfer::share_object()`, accessible to all transactions
* [**Owned objects**](https://docs.iota.org/developer/iota-101/objects/object-ownership/address-owned): Created with `transfer::transfer()`, belong to specific addresses
* **Embedded structs**: Fields within objects (e.g., `Peer`, `EnforcedOptions`)
* [**Tables**](https://docs.iota.org/references/framework/table): Dynamic collections stored within objects (e.g., peer mappings by EID)
### Object-Based Configuration
Configuration is stored in **struct fields** and **Tables**, not storage slots:
```rust wrap theme={null}
public struct OApp has key {
id: UID,
oapp_cap: CallCap, // Capability for calls
admin_cap: address, // Reference to AdminCap owner
peer: Peer, // Embedded peer mappings (Table)
enforced_options: EnforcedOptions, // Embedded options config
sending_call: Option, // Track in-progress sends
}
```
### Initialization Requirements
Before sending messages, you must:
1. **Register the OApp**: Call `endpoint::register_oapp()` to create a `MessagingChannel`
2. **Initialize channels**: Call `endpoint::init_channel()` for each remote EID
3. **Set peer addresses**: Call `oapp::set_peer()` for each destination
4. **(Optional)** Set send/receive libraries (uses Endpoint defaults if not set)
5. **(Optional)** Configure ULN parameters (uses library defaults if not set)
## Security & Permission Model
IOTA's security model differs fundamentally from EVM's `msg.sender` approach, using owned objects to prove authorization.
### Capability-Based Authorization
Instead of checking the transaction sender, IOTA functions require [capability objects](https://docs.iota.org/developer/iota-101/move-overview/patterns/capabilities) as parameters:
| Capability | Type | Purpose |
| ---------------- | ----- | ------------------------------------------------------ |
| `CallCap` | Owned | Authorizes creating `Call` objects and calling modules |
| `AdminCap` | Owned | Grants admin rights (set peers, configure options) |
| `TreasuryCap` | Owned | Grants mint/burn authority for coin type `T` |
| `UpgradeCap` | Owned | Authorizes package upgrades |
**Capability Pattern**:
```rust wrap theme={null}
public fun set_peer(
self: &mut OApp,
admin_cap: &AdminCap, // Must provide AdminCap to prove authorization
eid: u32,
peer: Bytes32,
)
```
### CallCap Type System
`CallCap` objects have two types that determine their identifier:
```rust wrap theme={null}
/// From call_cap module
public enum CapType {
Individual, // ID = UID address (object-specific)
Package(address), // ID = Package address (package-wide)
}
public fun id(self: &CallCap): address {
match (self.cap_type) {
CapType::Individual => self.id.to_address(), // Returns object UID
CapType::Package(package) => package, // Returns package address!
}
}
```
**LayerZero OApps/OFTs use Package CallCaps**:
```rust wrap theme={null}
// Created with one-time witness
call_cap::new_package_cap(&otw, ctx) // Creates Package type
// Returns package address
oapp_cap.id() // → package address, not object UID
```
**Why This Matters**:
The registry architecture explains why package IDs are used throughout:
```rust wrap theme={null}
/// From oapp_registry.move
public struct OAppRegistry has store {
// Maps OApp package address to its complete information
oapps: Table, // ← Keyed by package address!
}
public(package) fun get_messaging_channel(
self: &OAppRegistry,
oapp: address // Package address expected
): address {
let registration = table_ext::borrow_or_abort!(&self.oapps, oapp, EOAppNotRegistered);
registration.messaging_channel
}
```
**Impact on LayerZero**:
* Registry lookups use `callCap.id()` → package address
* `MessagingChannel.oapp` field stores package address
* **Peer addresses must be package IDs** (not object IDs)
* Verification checks receiver against package address
* Remote chains send to package address, not object
This is the fundamental reason why IOTA peer addresses are package IDs, not object IDs.
### Why This Matters for Configuration
When you deploy an OApp/OFT and configure peers:
**On IOTA side**:
```typescript wrap theme={null}
import {SDK} from '@layerzerolabs/lz-iotal1-sdk-v2';
import {Stage} from '@layerzerolabs/lz-definitions';
const sdk = new SDK({client, stage: Stage.MAINNET});
const oapp = sdk.getOApp(yourPackageId); // Package ID, not object ID!
```
**On remote EVM side**:
```solidity wrap theme={null}
// Use IOTA PACKAGE ID as peer
myOApp.setPeer(
30378, // IOTA mainnet EID
bytes32(0x061a47bf...) // Your IOTA PACKAGE ID
);
```
**What happens when message arrives**:
1. Remote chain sends to your package ID
2. IOTA Endpoint looks up package ID in registry
3. Finds your MessagingChannel
4. Routes message to your OApp object
This registry architecture is why peers must be package IDs.
### Validation Pattern
OApps validate `CallCap` ownership to ensure calls are authorized:
```rust wrap theme={null}
public fun send(
self: &OApp,
oapp_cap: &CallCap, // Proves caller owns this OApp
...
) {
self.assert_oapp_cap(oapp_cap); // Validates CallCap belongs to this OApp
// ... business logic
}
fun assert_oapp_cap(self: &OApp, cap: &CallCap) {
assert!(self.oapp_cap.id() == cap.id(), EInvalidOAppCap);
}
```
This replaces Solidity's inheritance-based validation with explicit capability checks.
### Receive Path Security
When receiving messages, the OApp validates:
1. **Call Authorization**: The `Call` must come from the authorized Endpoint
2. **Peer Validation**: Message sender must match configured peer for source EID
3. **Message Integrity**: DVNs have verified the message before delivery
```rust wrap theme={null}
public fun lz_receive(
self: &mut OApp,
call: Call,
) {
// Validate Call came from Endpoint
let (callee, param, _) = call.destroy(&self.oapp_cap);
assert!(callee == endpoint_address(), EOnlyEndpoint);
// Validate sender is the configured peer
let peer = self.peer.get_peer(param.src_eid);
assert!(param.sender == peer, EOnlyPeer);
// Process message...
}
```
### Common Security Risks
* **Missing capability validation**: Not checking `CallCap` or `AdminCap`
* **Capability loss**: Transferring or losing owned capability objects
* **Incorrect peer configuration**: Setting wrong peer addresses
* **Bypassing validation**: Skipping `assert_oapp_cap()` checks
## Gas Model
IOTA's gas system differs from EVM by separating storage and computation costs, with a unique rebate mechanism.
### Storage Gas
* Charged for storing data onchain
* **Rebate mechanism**: When storage is freed, gas is refunded
* This can result in **negative gas utilization** for transactions that free storage
### Computation Gas
* Charged for execution/computation
* **Base Budget**: Every transaction requires a minimum of 1000 gas units
* Priority fees can be added during network congestion
For detailed gas information, see:
* [IOTA Gas Pricing](https://docs.iota.org/developer/iota-101/tokenomics/gas-pricing)
* [IOTA Gas in IOTA](https://docs.iota.org/developer/iota-101/tokenomics/gas-in-iota)
## Key IOTA Concepts for LayerZero
IOTA provides system-level objects and features that LayerZero leverages for crosschain messaging.
### Clock Object
The [Clock](https://docs.iota.org/references/framework/clock) is a system singleton object at address `0x6`:
```rust wrap theme={null}
// Access in functions
public fun some_function(clock: &Clock) {
let timestamp_ms = clock.timestamp_ms();
// Use for timeout validation, rate limiting, etc.
}
// In PTB
tx.object('0x6') // Reference to Clock
```
Used in LayerZero for:
* Library timeout validation
* Rate limiter windows
* Message expiration checks
### Event System
IOTA [events](https://docs.iota.org/developer/iota-101/using-events) are emitted and indexed for off-chain monitoring:
```rust wrap theme={null}
use iota::event;
public struct MessageSentEvent has copy, drop {
guid: Bytes32,
dst_eid: u32,
message: vector,
}
// Emit event
event::emit(MessageSentEvent { guid, dst_eid, message });
```
**Monitoring Events**:
```typescript wrap theme={null}
// Subscribe to events
const unsubscribe = await client.subscribeEvent({
filter: {Package: packageId},
onMessage: (event) => {
console.log('Event:', event);
},
});
```
***
## Key Takeaways
1. **No Dynamic Dispatch**: IOTA doesn't support dynamic dispatch; use Call pattern instead
2. **PTB-Centric**: All crosschain operations happen within Programmable Transaction Blocks
3. **Explicit Validation**: Replace EVM inheritance with explicit validation checks
4. **Object-Based State**: Configuration stored in object fields, not EVM-style storage slots
5. **Atomicity**: PTBs guarantee all-or-nothing execution
6. **Dual Gas Model**: Separate charges for storage and computation, with storage rebates
## Next Steps
* [OApp Implementation Guide](/v2/developers/iota/oapp/overview) - Build custom crosschain applications
* [OFT Implementation Guide](/v2/developers/iota/oft/overview) - Deploy crosschain tokens
* [OFT SDK](/v2/developers/iota/oft/sdk) - TypeScript SDK methods and patterns
* [Configuration Guide](/v2/developers/iota/configuration/dvn-executor-config) - DVN and executor configuration
* [Protocol Overview](/v2/developers/iota/protocol-overview) - Complete protocol workflows
* [IOTA Development Guidance](/v2/developers/iota/technical-reference/iota-guidance) - Best practices
# IOTA L1 Development Guidance
Source: https://docs.layerzero.network/v2/developers/iota/technical-reference/iota-guidance
Technical reference for IOTA L1 Development Guidance. Complete API documentation with functions, parameters, and usage examples. LayerZero enables secure...
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](https://docs.iota.org/developer/references/cli):
```bash wrap theme={null}
cargo install --locked --git https://github.com/iotaledger/iota.git --branch mainnet iota
```
Verify installation:
```bash wrap theme={null}
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](https://docs.iota.org/developer/iota-101/move-overview/package-upgrades/introduction) details:
```toml wrap theme={null}
[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
```bash wrap theme={null}
iota move build
```
### Running Tests
```bash wrap theme={null}
iota move test
```
### Local Development
Start a local IOTA network:
```bash wrap theme={null}
iota start
```
## Deployment
### Deploying to Testnet
```bash wrap theme={null}
iota client publish \
--gas-budget 100000000 \
--json
```
### Deploying to Mainnet
```bash wrap theme={null}
iota client switch --env mainnet
iota client publish \
--gas-budget 100000000 \
--json
```
### Deployment Script Example
```bash wrap theme={null}
#!/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](https://docs.iota.org/developer/iota-101/move-overview/package-upgrades/upgrade).
**[`UpgradeCap`](https://docs.iota.org/developer/iota-101/move-overview/package-upgrades/upgrade)**: Owned object granting upgrade authority
```rust wrap theme={null}
/// 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**:
```bash wrap theme={null}
iota client transfer \
--to \
--object-id \
--gas-budget 10000000
```
**Upgrade a Package**:
```bash wrap theme={null}
iota client upgrade \
--upgrade-capability \
--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`: Authorizes minting/burning coins (for OFT mint/burn type)
* `UpgradeCap`: Authorizes package upgrades (transfer with caution)
**Transfer Pattern**:
```rust wrap theme={null}
// 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:
```rust wrap theme={null}
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**:
```bash wrap theme={null}
# Create multisig address with multiple public keys
iota keygen multi-sig \
--pks \
--weights 1 1 1 \
--threshold 2
```
## Resource & Fee Models
See [IOTA Gas Model](https://docs.iota.org/developer/stardust/units#iota) for complete details.
### Storage Gas
Charged for storing data onchain:
```rust wrap theme={null}
// 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:
```rust wrap theme={null}
// 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:
```rust wrap theme={null}
// 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](https://docs.iota.org/developer/iota-101/transactions):
* 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:
```rust wrap theme={null}
// Skip a message
public entry fun skip(
oapp: &mut OApp,
admin_cap: &AdminCap,
src_eid: u32,
sender: vector,
nonce: u64,
)
// Clear a message
public entry fun clear(
oapp: &mut OApp,
admin_cap: &AdminCap,
src_eid: u32,
sender: vector,
nonce: u64,
)
// Nilify a message
public entry fun nilify(
oapp: &mut OApp,
admin_cap: &AdminCap,
src_eid: u32,
sender: vector,
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:
```typescript wrap theme={null}
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
```bash wrap theme={null}
# Run unit tests
iota move test
# Run integration tests on testnet
iota client call --package $PKG ... --json
```
### 2. Monitor Gas Usage
```bash wrap theme={null}
# Use --gas-budget appropriately
iota client call \
--gas-budget 20000000 \ # Start higher
--json
```
### 3. Handle Rebates Correctly
```rust wrap theme={null}
// Don't assume gas cost is always positive
// Rebates can make net cost negative
```
### 4. Version Your Packages
```toml wrap theme={null}
[package]
name = "my_oapp"
version = "1.0.0" # Increment on upgrades
```
### 5. Secure Your Keys
```bash wrap theme={null}
# 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
* [IOTA Documentation](https://docs.iota.org/)
* [IOTA Move Documentation](https://docs.iota.org/developer/iota-101/move-overview/move-overview)
* [IOTA GitHub](https://github.com/iotaledger/iota)
* [LayerZero IOTA Contracts](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/iota)
## Next Steps
* [OApp Overview](/v2/developers/iota/oapp/overview)
* [OFT Overview](/v2/developers/iota/oft/overview)
* [Configuration Guide](/v2/developers/iota/configuration/dvn-executor-config)
* [Troubleshooting](/v2/developers/iota/troubleshooting/common-errors)
# Common Errors
Source: https://docs.layerzero.network/v2/developers/iota/troubleshooting/common-errors
Common issues and solutions for Common Errors. Troubleshoot problems and find answers to frequently asked questions. LayerZero enables secure crosschain...
This page lists common errors you may encounter when developing LayerZero applications on IOTA L1, 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:
```bash wrap theme={null}
# 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/iota/contracts/oapps/oapp" }
EndpointV2 = { local = "../LayerZero-v2/packages/layerzero-v2/iota/contracts/endpoint-v2" }
# ... other packages
```
Or use published package addresses (see [Deployed Contracts](/v2/deployments/chains/iota)).
### Unpublished Dependencies Error
**Error Message**:
```
Error: Modules in package '' were not published with the '--with-unpublished-dependencies' flag
```
**Cause**: Package has dependencies that aren't published onchain.
**Solution**: Add the flag when publishing:
```bash wrap theme={null}
iota 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 IOTA's 250 KiB limit per package object. See [IOTA transaction limits](https://docs.iota.org/developer/iota-101/transactions).
**Solutions**:
1. Split into multiple packages
2. Remove unused dependencies
3. Optimize data structures
4. Move large constants off-chain
**Example Split**:
```rust wrap theme={null}
// 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:
```bash wrap theme={null}
iota 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:
```bash wrap theme={null}
iota client objects | grep UpgradeCap
```
3. 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:
```bash wrap theme={null}
iota 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:
```bash wrap theme={null}
iota 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:
```bash wrap theme={null}
iota 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 IOTA, peer addresses must be **package IDs**, not object IDs.
**Find Your Correct Package ID**:
```bash wrap theme={null}
# Query your OApp/OFT object
iota client object --json | jq '.data.type'
# Output example:
# "0x061a47bffa630b8cd3735f8479edf7ab7897863fb3b796e77ebb8786af6f1bfc::oapp::OApp"
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# This is your package ID - use as peer address!
```
**Update Peer on Remote Chain**:
```typescript wrap theme={null}
// On EVM/Solana/other chains, use IOTA package ID:
await oapp.setPeer(
30230, // IOTA mainnet EID
'0x061a47bffa630b8cd3735f8479edf7ab7897863fb3b796e77ebb8786af6f1bfc', // Package ID
);
```
**Why Package ID?**
* IOTA 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**:
```typescript wrap theme={null}
// - 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**:
```bash wrap theme={null}
# View transaction outputs after publishing
iota 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:
```typescript wrap theme={null}
import {SDK, OAppUlnConfigBcs} from '@layerzerolabs/lz-iotal1-sdk-v2';
// CRITICAL: Import asBytes helper
const {asBytes} = await import('@layerzerolabs/lz-iotal1-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');
// - 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:
```typescript wrap theme={null}
// 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` type
**Why `tx.pure()` fails**:
* Direct `tx.pure(bytes, 'vector')` 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` 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**:
```typescript wrap theme={null}
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('0xfe5be5a2d5b11e635e3e4557bb125fb24a3dd09111eded06fd6058b2aee1d054'), // OFTComposerManager (IOTA mainnet)
tx.object('0x6'), // Clock
],
});
const result = await client.devInspectTransactionBlock({
transactionBlock: tx,
sender: yourAddress,
});
const lzReceiveInfoBytes = bcs.vector(bcs.u8()).parse(...);
```
2. **Update OApp info in registry**:
```typescript wrap theme={null}
import {asBytes} from '@layerzerolabs/lz-iotal1-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](/v2/developers/iota/oft/overview#registration-with-endpoint)).
### 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 IOTA, peer addresses must be **package IDs**:
```bash wrap theme={null}
# Find your package ID
iota client object --json | jq '.data.type'
# Example: "0x061a47bf...::oapp::OApp"
# ^^^^^^^^^^^^
# Use this package ID as peer on remote chains
```
Update peer on remote chain:
```solidity wrap theme={null}
// On EVM
oapp.setPeer(30230, bytes32(0x061a47bffa630b8cd3735f8479edf7ab7897863fb3b796e77ebb8786af6f1bfc)); // Package ID
```
**Why**: IOTA 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**:
```rust wrap theme={null}
// - 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**:
```bash wrap theme={null}
# Dry run to see simulation errors
iota 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:
```typescript wrap theme={null}
// 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**:
```bash wrap theme={null}
iota client object --json | jq '.data.owner'
```
Output types:
* `{"AddressOwner": "0x..."}` - Owned by specific address
* `"Shared"` - Shared object (accessible to anyone)
* `"Immutable"` - Immutable object (read-only)
2. **Use correct signer**: Ensure the transaction signer owns the object
3. **Check object type**:
* [**Owned objects**](https://docs.iota.org/developer/iota-101/objects/object-ownership/address-owned) (`AdminCap`, `CallCap`): Must be owned by signer
* [**Shared objects**](https://docs.iota.org/developer/iota-101/objects/object-ownership/shared) (`OApp`, `EndpointV2`): Accessible by anyone, use references (`&` or `&mut`)
* [**Immutable objects**](https://docs.iota.org/developer/iota-101/objects/object-ownership/immutable) (`CoinMetadata`): Read-only references only
**Example**:
```rust wrap theme={null}
// - 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**:
```rust wrap theme={null}
// 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:
```typescript wrap theme={null}
const client = new IOTAClient({
url: 'https://fullnode.mainnet.iota.io:443',
timeout: 60000, // 60 seconds
});
```
3. 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:
```typescript wrap theme={null}
// - 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::iota::SUI>'
```
**Cause**: Wrong coin type passed to function.
**Solution**: Verify coin types match:
```typescript wrap theme={null}
// 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
```bash wrap theme={null}
# IOTA CLI with verbose output
iota client call ... --json | jq .
```
### Check Transaction Effects
```typescript wrap theme={null}
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
```bash wrap theme={null}
# View object details
iota client object $OBJECT_ID --json
# View all objects for an address
iota client objects --json
```
### Use IOTA Explorer
Navigate to [IOTA Explorer](https://iotascan.com/) to:
* View transaction details
* Check object states
* Inspect event logs
* Verify package deployments
### Test on Devnet First
Always test on devnet before testnet/mainnet:
```bash wrap theme={null}
# Switch to devnet
iota client switch --env devnet
# Test your calls
iota client call ... --gas-budget 20000000
```
## Getting Help
If you continue to experience issues:
1. **Check Documentation**: Review [IOTA Documentation](https://docs.iota.org/)
2. **Search Discord**: Look for similar issues in [LayerZero Discord](https://discord.com/invite/ktbvm8Nkcr)
3. **Ask for Help**: Post in Discord with:
* Error message
* Transaction hash (if available)
* Code snippet
* What you've tried
## Next Steps
* [FAQ](/v2/developers/iota/troubleshooting/faq)
* [IOTA Guidance](/v2/developers/iota/technical-reference/iota-guidance)
* [Configuration Guide](/v2/developers/iota/configuration/dvn-executor-config)
* [Technical Overview](/v2/developers/iota/technical-overview)
# IOTA L1 FAQ
Source: https://docs.layerzero.network/v2/developers/iota/troubleshooting/faq
Common issues and solutions for IOTA L1 FAQ. Troubleshoot problems and find answers to frequently asked questions. LayerZero enables secure crosschain...
Frequently asked questions about developing LayerZero applications on IOTA L1.
## General Questions
IOTA Move lacks native dynamic dispatch (unlike EVM's `delegatecall`). The `Call` 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 IOTA's architecture, see the [IOTA documentation on PTBs](https://docs.iota.org/developer/iota-101/transactions/ptb/programmable-transaction-blocks).
The key difference is that IOTA uses `Call` objects and PTBs instead of `delegatecall`. In EVM, the relayer calls `Endpoint.lzReceive()` which delegates to the OApp. In IOTA, the Executor calls `Endpoint.lz_receive()` which creates a `Call` object that the OApp destroys and processes via explicit PTB routing. Both execution models are permissionless.
For architectural details, see [Technical Overview](/v2/developers/iota/technical-overview) and [Protocol Overview](/v2/developers/iota/protocol-overview).
## Development Questions
No. LayerZero deploys and maintains the `EndpointV2` shared object on IOTA L1. 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
OFTs use **shared decimals** to handle precision differences:
```
Local Decimals: Token decimals on current chain (e.g., 9)
Shared Decimals: Crosschain precision (default: 6)
Conversion Rate: 10^(local - shared)
```
When sending:
1. Amount is divided by conversion rate (removes dust)
2. Truncated amount is sent crosschain
3. Destination multiplies by its conversion rate
See [OFT Overview](/v2/developers/iota/oft/overview#decimal-precision) for examples.
Yes, use an **OFT Adapter** (lock/unlock model):
```rust wrap theme={null}
public struct OFTAdapter {
escrow: Balance, // Locked tokens
// No treasury_cap needed
}
```
For new tokens, use **mint/burn OFT** for better efficiency.
## Gas and Fees
IOTA 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
Negative gas is **normal** when storage is freed:
```rust wrap theme={null}
// 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](/v2/developers/iota/technical-overview#gas-model) for details.
Recommended gas budgets:
| Operation | Gas Budget |
| ------------------ | ------------ |
| Initialize channel | 10,000,000 |
| Set peer | 10,000,000 |
| Configure DVNs | 15,000,000 |
| Send message | 20,000,000 |
| Receive message | 15,000,000 |
| Deploy package | 100,000,000+ |
Start higher and reduce based on actual usage.
## Configuration Questions
No, defaults are available:
```bash wrap theme={null}
# 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
Use the TypeScript SDK:
```typescript wrap theme={null}
// 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 IOTA CLI cannot easily parse complex return values.
Yes, if you retain the `AdminCap`:
```bash wrap theme={null}
# Update peer
iota client call \
--function set_peer \
--args $OAPP $ADMIN_CAP $NEW_EID $NEW_PEER \
...
# Update DVNs
iota client call \
--function set_send_uln_config \
--args $OAPP $ADMIN_CAP $EID ... \
...
```
Without AdminCap, configuration is immutable.
## SDKs and Tooling
LayerZero provides two TypeScript SDKs:
1. **[@layerzerolabs/lz-iotal1-sdk-v2](https://www.npmjs.com/package/@layerzerolabs/lz-iotal1-sdk-v2)**
* Core Endpoint interactions
* OApp functionality
* Configuration management
2. **[@layerzerolabs/lz-iotal1-oft-sdk-v2](https://www.npmjs.com/package/@layerzerolabs/lz-iotal1-oft-sdk-v2)**
* OFT-specific operations
* Token transfers
* Balance queries
See [OFT SDK](/v2/developers/iota/oft/sdk) for usage examples.
The IOTA 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:
```typescript wrap theme={null}
import {IOTAClient} from '@iota/iota-sdk/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-iotal1-sdk-v2';
const peer = await oapp.getPeer(client, remoteEid);
```
Not currently. Package publication and configuration require:
1. **Publish packages**: Using `iota client publish`
2. **Call entry functions**: Invoke configuration functions via `iota client call` or SDK
3. **Custom scripts**: Write TypeScript scripts for automated workflows
See [Configuration Guide](/v2/developers/iota/configuration/dvn-executor-config) for manual setup instructions.
## Troubleshooting
This error means a `Call` object wasn't properly consumed in your PTB:
```rust wrap theme={null}
// - 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.
You're trying to send to a destination chain without a `MessagingChannel`:
```bash wrap theme={null}
# Fix: Initialize the channel first
iota client call \
--package \
--module endpoint_v2 \
--function init_channel \
--args