Skip to main content
The Omnichain Fungible Token (OFT) Standard allows fungible tokens to be transferred across multiple blockchains without asset wrapping or middlechains. Read more on OFTs in our glossary page: OFT.

What is an OFT on IOTA?

An OFT on IOTA is a Move package that extends the OApp functionality to enable crosschain token transfers. It integrates with IOTA’s native coin type system (Coin<T>, Balance<T>, TreasuryCap<T>) while providing LayerZero’s omnichain capabilities. This guide will walk you through deploying an OFT on IOTA L1. To understand how OFTs integrate with IOTA L1’s coin system and the differences between mint/burn and lock/unlock token management strategies, see Integration with IOTA L1 Coin System.

Deployment

OFT deployment on IOTA uses a two-package pattern: your token + pure LayerZero OFT source.

Mint/Burn Example

This deployment guide demonstrates the mint/burn approach, where you provide the TreasuryCap and the OFT mints/burns tokens during crosschain transfers. This works for both new tokens and existing tokens where you control the TreasuryCap.If you DON’T have the TreasuryCap (frozen, held by DAO, etc.), use the lock/unlock (adapter) approach instead. See Choosing Mint/Burn vs Lock/Unlock for details.
Prerequisites:

Install IOTA CLI

For other platforms, see the IOTA Installation Guide.

Create Wallet and Get IOTA

To fund your wallet, use the IOTA faucet for testnet or acquire IOTA from an exchange for mainnet.

New to IOTA?

If you haven’t used IOTA before, start with Getting Started with IOTA to understand the object model, package structure, and development basics.

Step 1: Create and Deploy Your Token

Create token package:
Implement token (sources/myoft.move):
Deploy your token:

Gas Budgets

Gas budgets are specified in NANOS (1 IOTA = 1,000,000,000 NANOS). The budget is the maximum you’re willing to spend; actual costs are typically much lower:
  • Token package: ~0.5 IOTA budget, actual cost ~0.013 IOTA
  • OFT package: ~1 IOTA budget, actual cost ~0.186 IOTA

Step 2: Deploy LayerZero OFT Package

Deploy the pure LayerZero OFT source without modifications. Recommended approach (git dependencies):
Create Move.toml with git dependencies:
Copy OFT source files:
Deploy (dependencies auto-fetched from GitHub):
This automatically creates an OFTInitTicket (via oft_impl::init()).

Save Your Package ID!

The OFT Package ID (not object ID) is what you’ll use as the peer address on remote chains. Remote chains must use this package ID to send messages to your IOTA OFT.Finding package ID from object (if you didn’t save it):
Alternative: Deploy directly from the cloned repository using --with-unpublished-dependencies flag (requires all dependencies in correct relative paths).

Multiple OFTs

If deploying multiple OFTs (e.g., different tokens), repeat this OFT package deployment for each token. Each token gets its own OFT package instance. For adapter OFTs, see choosing between mint/burn and lock/unlock models to avoid deploying multiple adapters for the same token.

Step 3: Initialize OFT via SDK

Consume the ticket using the OFT SDK. This example uses mint/burn initialization by passing the TreasuryCap:

Lock/Unlock Alternative

To initialize an OFT Adapter for an existing token (lock/unlock model), use oft.initOftAdapterMoveCall() instead, which does not require the TREASURY parameter. See Integration with IOTA Coin System for details.

Integration with IOTA L1 Coin System

OFTs integrate seamlessly with IOTA L1’s native coin framework, using standard types for token management.

IOTA L1 Coin Type System

The IOTA L1 framework provides these core types for token functionality: Coin<T>: Owned coin object with a value
Balance<T>: Storable value (can be held in structs)
TreasuryCap<T>: Authority to mint/burn coins
CoinMetadata<T>: Token information (name, symbol, decimals)

OFT Integration

The OFT uses these types:
Phantom Type Parameter: <phantom T> means:
  • T is the coin type (e.g., MY_COIN)
  • phantom = T doesn’t appear in any field directly
  • Enables type safety without storing T values

OFT Types

IOTA OFTs use a flexible enum pattern that supports two token management strategies, depending on whether you’re creating a new token or bridging an existing one.

OFT Structure

The OFT uses a generic type parameter and includes built-in support for optional features:
All OFTs include these fields, but they start in safe default states. Configuration is optional and done via admin functions after deployment.

Treasury Enum

The OFTTreasury<T> enum determines token management strategy:

Choosing Mint/Burn vs Lock/Unlock

1. Mint/Burn

This model manages token supply by minting new tokens on the destination chain and burning them on the source chain. When to use:
  • You own or can obtain the TreasuryCap<T> for the token
  • You’re comfortable with dynamic supply distribution across chains
  • Works for both new tokens AND existing tokens where you control the TreasuryCap

TreasuryCap on IOTA

On IOTA, TreasuryCap<T> is an owned object that can be transferred between addresses. If you created a token previously or received the TreasuryCap from someone else, you can use the mint/burn model even for “existing” tokens. Only addresses with access to the TreasuryCap can mint and burn the token supply.
Mechanism:
  • Send: Burns tokens on source chain (reduces total supply)
  • Receive: Mints tokens on destination chain (increases total supply)
Initialization (via SDK):

2. Lock/Unlock

The lock/unlock model enables omnichain bridging by escrowing tokens on the source chain and releasing them on the destination, maintaining fixed supply on IOTA L1. When to use:
  • You DON’T have access to the TreasuryCap<T> (frozen, held by DAO, or inaccessible)
  • Token supply on IOTA must remain fixed
  • You need to bridge a token where you lack mint/burn authority
Mechanism:
  • Send: Locks tokens in OFT’s escrow balance (removes from circulation)
  • Receive: Releases tokens from escrow balance (returns to circulation)
Initialization (via SDK):
Only deploy one OFT Adapter per token mesh. Multiple adapters fragment liquidity and can lead to token loss if supply is insufficient on the destination chain.

Core Operations

The core operations of an Omnichain Fungible Token (OFT) on IOTA enable seamless value transfer across multiple blockchains. At a high level, these consist of sending tokens to another chain and receiving them from peers, all while maintaining strict security and interoperability guarantees.

Sending Tokens

Sending tokens is the primary function OFTs provide, allowing users to transfer assets from the current chain to a specified recipient on a different blockchain. This operation burns or locks tokens on the source chain, constructs a crosschain message, and leverages the LayerZero protocol to initiate delivery to the destination chain.
Returns: A tuple containing:
  1. Call<EndpointSendParam, MessagingReceipt> - Route through Endpoint, then confirm
  2. OFTSendContext - Context for confirming the send operation
Process:
  1. Debit tokens from sender’s coin (burns or escrows based on OFT type)
  2. Apply fee if configured, remove dust for decimal precision
  3. Build OFT message with recipient and amount in shared decimals
  4. Create Call to send via LayerZero Endpoint
  5. (Optional) Rate limiter tracks outbound flow

Receiving Tokens

Receiving tokens on IOTA involves securely processing incoming crosschain messages, validating the source and payload, and minting or unlocking tokens to deliver them to the intended recipient.
Process:
  1. Executor delivers Call object via Endpoint
  2. OApp validates Call came from authorized Endpoint and peer
  3. OFT decodes message to extract recipient and amount in shared decimals
  4. Converts amount to local decimals
  5. Credits tokens (mints or releases from escrow based on OFT type)
  6. Rate limiter tracks inbound flow
  7. Transfers credited tokens to recipient
For compose functionality: Use lz_receive_with_compose() which additionally requires:
  • compose_queue: &mut ComposeQueue
  • composer_manager: &mut OFTComposerManager

Decimal Precision

OFTs use local decimals (per-chain precision) and shared decimals (crosschain precision) to handle token transfers across blockchains with different decimal standards. For complete details on how this works, see OFT Technical Reference.

IOTA-Specific Constraint: u64 Balance Limit

u64 Balance Overflow

IOTA’s coin framework uses u64 for all token balances, imposing a hard limit of 2^64 - 1 = 18,446,744,073,709,551,615. If you attempt to mint or transfer amounts exceeding this value, the transaction will abort. This is a blockchain VM constraint that cannot be bypassed.Impact on decimals:
Choose your decimals carefully during token deployment.
Shared Decimals: Use 6 (default) for most use cases.

Deployment Planning

Before calling coin::create_currency():
  1. Calculate your maximum token supply
  2. Choose local decimals: Ensure max_supply * 10^decimals < 2^64
  3. Use shared_decimals = 6 during OFT initialization (standard)
For detailed information on shared decimals, decimal conversion, and dust handling, see OFT Technical Reference.

Registration with Endpoint

After initializing your OFT, you must register it with the LayerZero Endpoint to enable crosschain messaging.

Using OFT SDK

What this does:
  • Creates MessagingChannel shared object
  • Stores registry entry keyed by your package ID
  • Auto-generates proper lz_receive_info with all required PTB instructions
  • No manual info generation needed!
OFTComposerManager: This shared object routes compose messages to appropriate handlers. OFTComposerManager address on mainnet: 0xfe5be5a2d5b11e635e3e4557bb125fb24a3dd09111eded06fd6058b2aee1d054 OFTComposerManager address on testnet: 0x90384f5f6034604f76ac99bbdd25bc3c9c646a6e13a27f14b530733a8e98db99

Finding Current Addresses

The canonical addresses are available in the SDK deployment files at @layerzerolabs/lz-iotal1-sdk-v2/deployments/iotal1-mainnet/object-OFTComposerManager.json. If you encounter TypeError: Cannot convert undefined to a BigInt during registration, verify you’re using the correct OFTComposerManager address for your network.

Configuration

After registration, configure your OFT to enable crosschain token transfers.

Using OApp SDK for Configuration on IOTA

All configuration is done through the base SDK’s OApp instance. Configure security settings before setting peers to open the pathway.

Endpoint IDs

IOTA L1 Endpoint IDs:
  • IOTA Mainnet: 30423
  • IOTA Testnet: 40423
The examples below use EID 30184 (Base Mainnet) as the destination. For a complete list of endpoint IDs across all supported chains, see Deployed Contracts.
Configuration order:
  1. Set Libraries (recommended) - Custom send/receive message libraries
  2. Configure DVNs (recommended) - Send and receive verification
  3. Set Enforced Options (optional) - Minimum gas requirements
  4. Configure OFT Settings (optional) - Rate limits, fees
  5. Set Peer (required) - Opens pathway for messaging (call this last!)
For complete DVN configuration details and gas recommendations, see Configuration Guide.

Configuring Remote Chains to Send to IOTA

When configuring OFTs on other chains (e.g., EVM, Solana) to send tokens to IOTA, follow standard LayerZero configuration but note these IOTA-specific requirements: 1. Use Package ID as Peer:
2. Set Enforced Options for IOTA Destination: Based on gas profiling, configure appropriate gas limits for IOTA:
Gas requirements:
  • IOTA’s lz_receive uses 2,000-5,000 units for computation
  • Use 5,000 gas units for safe buffer
  • No msg.value needed (IOTA handles storage internally)
3. Standard DVN Configuration: DVN configuration on remote chains follows standard LayerZero patterns - no IOTA-specific changes needed. See the platform-specific implementation guides for EVM and Solana configuration.

Example Usage

Sending Tokens (TypeScript SDK)

For more SDK usage, see OFT SDK Documentation.

Best Practices & Troubleshooting

Deployment:
  • Use pure LayerZero OFT source without modifications
  • Always wait for transaction finality: await client.waitForTransaction({ digest })
  • Use SDK factory: sdk.getOApp(packageId) (never new OApp(...))
  • Use SDK address exports where available (e.g., PACKAGE_ULN_302_ADDRESS[Stage.MAINNET])
  • For addresses not exported by SDK (e.g., OFTComposerManager), verify against SDK deployment files at @layerzerolabs/lz-iotal1-sdk-v2/deployments/
Security:
  • Test with small amounts before production
  • Validate peer addresses match package IDs (not object IDs)
  • Configure DVNs before setting peers
  • Only deploy one OFT Adapter per token
Common Errors:
  • oapp_registry::get_messaging_channel abort code: 1 → Using object ID instead of package ID as peer
  • InvalidBCSBytes in command 0 → Use OAppUlnConfigBcs.serialize() for DVN config
  • UnusedValueWithoutDrop → Use oft.registerOAppMoveCall() for proper lz_receive_info
For gas profiling and detailed configuration, see Configuration Guide.

Next Steps