# Get list Source: https://docs.layerzero.network/api-reference/ofts/get-list /openapi/oft-mainnet.json get /list Get list of available OFTs with their deployments across chains # Get transfer Source: https://docs.layerzero.network/api-reference/ofts/get-transfer /openapi/oft-mainnet.json get /transfer Create an OFT transfer transaction between chains # Get openapi Source: https://docs.layerzero.network/api-reference/openapi/get-openapi /openapi/oft-mainnet.json get /openapi OpenAPI specs. # Zero Source: https://docs.layerzero.network/chain/index Zero is the first multi-core world computer. It uses ZK proofs to decouple execution from verification, replacing redundant replication with a heterogeneous architecture. Zero is the first multi-core world computer. Current blockchains are single-threaded and homogeneous — every validator re-executes every transaction. Zero uses ZK proofs to decouple execution from verification, replacing redundant replication with a heterogeneous architecture. Zero was announced on February 10, 2026. This page covers Zero's architecture at a high level. Details may change as development continues. ## Validator classes Zero has two validator classes: * **Block Validators** run on consumer hardware. They verify ZK proofs rather than re-executing transactions, so hardware requirements stay low regardless of network throughput. * **Block Producers** are optional higher-performance nodes that execute transactions within zones and generate ZK proofs. Because verification is decoupled from execution, adding throughput does not increase the work Block Validators need to do. The network scales without raising hardware requirements. ## Scaling approach Zero targets **2 million transactions per second (TPS) per Zone**. Four purpose-built components handle the main bottlenecks: | Bottleneck | Solution | What it does | | ---------------- | -------- | --------------------------------------------------------- | | State storage | QMDB | State updates with minimal disk I/O | | Parallel compute | FAFO | Transaction scheduling across CPU cores | | ZK proving | Jolt Pro | Real-time proof generation that keeps pace with execution | | Networking | SVID | Data distribution across validators | ## Atomicity Zones **Atomicity Zones** are to Zero what concurrent processes are to a modern CPU. Each zone is a separate execution environment — smart contracts, trading, payments — that processes transactions and produces ZK proofs. All zones share Zero's validator set and the same security guarantees. Zones scale independently. Adding a new zone increases total network capacity without degrading the performance of existing zones. ## Timeline | Milestone | Timing | | ---------------- | ----------------- | | **Announcement** | February 10, 2026 | | **Testnet** | Prior to mainnet | | **Mainnet** | 2026 | ## Get involved Learn more about building on Zero. Read the technical positioning paper. Read the announcement blog post. ### Community * [Twitter/X](https://x.com/layerzero_core) * [Blog](https://layerzero.network/blog) * [Telegram](https://t.me/joinchat/VcqxYkStIDsyN2Rh) # Bug Bounty Program Source: https://docs.layerzero.network/community/bug-bounty-support LayerZero Bug Bounty Program on Immunefi with up to $15M in rewards for security vulnerability disclosures. Help secure the entire omnichain ecosystem. LayerZero has an absolute commitment to continuously evaluating and improving security. To demonstrate this we are pleased to run the largest live bug bounty program in the world at up to \$15M! You can read more about the program and make reports via [Immunefi](https://immunefi.com/bounty/layerzero). To date, LayerZero has awarded almost \$1M to whitehats that have made disclosures. Additional V2 program details will be reflected shortly. # Add Crosschain Features Source: https://docs.layerzero.network/crosschain/features/overview Extend your application with crosschain capabilities. From integrating existing assets to building custom messaging systems. This section covers how to add crosschain capabilities to your application. The features are ordered from **most commonly used** (and easiest to integrate) to **most advanced** (requiring deeper systems understanding). ## Why This Order? Most developers coming to LayerZero want to **transfer tokens across chains**. The question is: how much customization do you need? | If you want to... | Use this | Complexity | | ------------------------------------------------------------------- | ---------------------- | ---------- | | Transfer existing assets (USDC, ETH, USDT, USDT0, USDe, WBTC, etc.) | **Stargate**, **OFTs** | Lowest | | Transfer tokens + execute logic on destination | **Composer** | Low | | Manage crosschain liquidity in vaults | **OVault** | Medium | | Build completely custom crosschain logic | **OApp** | Higher | | Read data from other chains | **lzRead** | Higher | *** ## Transfer Existing Assets **Best for:** Integrating transfers of assets that are already crosschain enabled. If the asset you need already exists as an OFT or in Stargate pools, you can integrate directly without deploying anything. ### Stargate Assets Stargate provides unified liquidity pools for major assets across 60+ chains. The hard work—issuing the asset, managing liquidity, handling edge cases—is already done. **Assets:** ETH, USDC, USDT, and more via unified pools. How Stargate's Hydra system enables unified liquidity. Add Stargate transfers to your application. ### Existing OFTs Other teams have deployed OFTs that you can integrate directly. These maintain unified supply across chains through LayerZero messaging. **Assets:** [USDT0, USDe, WBTC, and more](/v2/deployments/oft-ecosystem-stargate-assets). See all OFT deployments available to integrate. Managed OFT deployments with enhanced security. **Don't see your asset?** You can [deploy your own OFT](/crosschain/issue-asset/overview) to make any token crosschain. *** ## Composer **Best for:** Token transfers + arbitrary logic in a single transaction. Composers let you bundle a token transfer with additional calldata that executes on the destination chain. Send tokens AND trigger a swap, deposit into a protocol, or call any contract function. **Example use cases:** * Transfer USDC and swap to ETH on arrival * Bridge tokens directly into a lending protocol * Crosschain purchases (send payment + execute buy) How composed messages work under the hood. Implement composable transfers on EVM chains. *** ## OVault **Best for:** Crosschain vault and liquidity management. OVault provides a standard for managing assets across multiple chains from a unified interface. Deposit on one chain, manage liquidity across many. Crosschain vault architecture and design. Implement OVault on EVM chains. *** ## OApp (Custom Messaging) **Best for:** Building completely custom crosschain systems. OApp is the base standard for arbitrary crosschain messaging. You define the message format, the sending logic, and the receiving logic. Full flexibility, but you're responsible for the design. **When to use OApp:** * You need to send non-token data across chains * You're building crosschain governance, oracles, or coordination systems * You need complete control over message handling **OApp requires systems design knowledge.** You're defining message schemas, handling failures, and managing state across chains. If you just need token transfers, use Stargate, OFT, or Composer instead. Understand the OApp message lifecycle. Build custom messaging on EVM chains. Common patterns: A→B, A→B→A, composed messages. **Multi-chain support:** OApp is available on [Solana](/v2/developers/solana/oapp/overview), [Sui](/v2/developers/sui/oapp/overview), [IOTA](/v2/developers/iota/oapp/overview), and [Aptos](/v2/developers/aptos-move/contract-modules/oapp). *** ## lzRead (Crosschain Queries) **Best for:** Reading data from other chains without transferring assets. lzRead allows your contracts to query state from other blockchains. Instead of sending a message and waiting for a response, you can pull data directly. **Example use cases:** * Check token balances on another chain * Read oracle prices from a different network * Verify state before executing logic How crosschain reads work. Implement crosschain queries on EVM. *** ## Comparison Table | Feature | Token Transfer | Custom Data | Compose Logic | Complexity | | ------------ | -------------------- | --------------- | ------------- | ---------- | | **Stargate** | Yes (pooled assets) | No | No | Lowest | | **Composer** | Yes (OFT-based) | Yes (calldata) | Yes | Low | | **OVault** | Yes (vault deposits) | Limited | Yes | Medium | | **OApp** | Manual | Yes (anything) | Yes | Higher | | **lzRead** | No | Yes (read-only) | No | Higher | *** ## Next Steps If you want to issue your own crosschain token instead of using existing ones. Set up DVNs and Executors for your crosschain application. # Crosschain Development Source: https://docs.layerzero.network/crosschain/index Build omnichain applications with LayerZero. Transfer tokens, compose crosschain operations, and send arbitrary messages across 150+ blockchains. LayerZero is an **omnichain interoperability protocol** that enables secure communication between different blockchains. You can transfer tokens, send arbitrary messages, or build custom crosschain applications on top of it. **New to LayerZero?** If you want to understand how the protocol works before building, start with [LayerZero Overview](/v2/concepts/getting-started/what-is-layerzero) for a high-level introduction, or dive into [Core Concepts](/v2/concepts/getting-started/what-is-layerzero) for the technical details. ## What Can You Build? Most developers come to LayerZero for one of these use cases, ordered from most common to most advanced: ### 1. Transfer Existing Crosschain Assets The easiest path is integrating assets that already exist on LayerZero. Stargate has liquidity pools for [USDC](/v2/deployments/oft-ecosystem-stargate-assets) and [ETH](/v2/deployments/oft-ecosystem-stargate-assets) across 60+ chains. Other teams have issued OFTs like [USDT0](/v2/deployments/oft-ecosystem-stargate-assets), [USDe](/v2/deployments/oft-ecosystem-stargate-assets), and [WBTC](/v2/deployments/oft-ecosystem-stargate-assets) that you can integrate directly. Unified liquidity pools for ETH and USDC. No deployment needed. See all OFT deployments (USDT0, USDe, WBTC, etc.) available to integrate. ### 2. Issue Your Own Crosschain Asset Deploy your own Omnichain Fungible Token (OFT) that works natively across 150+ chains. OFTs maintain unified supply across all chains through debit/credit mechanisms. No bridges, no wrapped tokens. Learn how OFTs enable native crosschain token transfers. Step-by-step guide to issuing your own crosschain token. ### 3. Compose Token Transfers with Logic Composers let you bundle a token transfer with calldata that executes on the destination chain. Send tokens and trigger a swap, deposit into a vault, or call any contract in a single crosschain transaction. Combine token transfers with arbitrary contract calls. Crosschain vault standard for unified liquidity management. ### 4. Build Custom Crosschain Systems LayerZero supports arbitrary message passing for developers who need complete control. You can build crosschain governance, oracles, games, or anything else. Send arbitrary data between contracts on any chain. Pull data from other chains into your smart contracts. **OApp and lzRead require more systems understanding.** You're designing the message format, handling edge cases, and building the receive logic yourself. Start with Stargate or OFTs if you just need token transfers. *** ## Choose Your Path Learn the core concepts before building. Use Stargate or integrate existing OFTs. Deploy OFTs across chains. Your token, unified supply. Use Composers to bundle transfers with contract calls. Use OApp for arbitrary crosschain messaging. *** ## Platform Support LayerZero supports multiple blockchain platforms with native implementations: | Platform | Language | Chains | | --------------- | ----------- | ------------------------------------------------- | | **EVM** | Solidity | Ethereum, Arbitrum, Optimism, Base, and 100+ more | | **Solana** | Rust/Anchor | Solana mainnet and devnet | | **Sui** | Move | Sui mainnet | | **IOTA** | Move | IOTA L1 | | **Aptos** | Move | Aptos mainnet | | **Hyperliquid** | Solidity | Hyperliquid L1 | *** ## Developer Resources Bootstrap a new LayerZero project with our CLI tool. Explore example implementations and reference code. View all supported networks and contract addresses. Debug crosschain messages and resolve common errors. # Issue Crosschain Assets Source: https://docs.layerzero.network/crosschain/issue-asset/overview Create and deploy Omnichain Fungible Tokens (OFTs) that work natively across 150+ blockchains without bridges or wrapped assets. Deploy your own **Omnichain Fungible Token (OFT)** that works natively across 150+ blockchains. No bridges, no wrapped tokens. Understand the architecture. See existing OFT deployments. *** ## Should You Issue an OFT? Before deploying your own OFT, consider whether you actually need to: | Scenario | Recommendation | | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | | You need to transfer USDC or ETH | Use [Stargate](/v2/concepts/applications/stargate-finance) - assets already exist | | You want to integrate existing OFTs (USDT0, USDe, WBTC) | Just integrate them - see [OFT Ecosystem](/v2/deployments/oft-ecosystem-stargate-assets) | | You're issuing a **new token** that needs to be crosschain | **Yes, deploy an OFT** | | You have an **existing token** that needs crosschain support | **Yes, use OFT LockUnlock** (or BurnMint if token has mint/burn) | **Check if the asset is already an OFT.** If so (Stargate assets, USDT0, USDe), you can integrate directly without deploying your own. *** ## How OFTs Work When tokens move between chains, they are **removed from circulation on the source chain** and **added to circulation on the destination chain**. This keeps the global token supply constant regardless of which chains hold balances. OFT mechanism showing tokens removed from source chain and added to destination OFT mechanism showing tokens removed from source chain and added to destination 1. **Source chain:** Tokens are removed from circulation 2. **LayerZero message:** Transfer details are sent crosschain via DVNs 3. **Destination chain:** Equivalent tokens are added to circulation for the recipient This maintains a **unified supply** across all chains. *** ## Transfer Mechanisms: BurnMint vs LockUnlock There are two ways to remove tokens from circulation and add them back: | Mechanism | Remove from Circulation | Add to Circulation | Constraint | | -------------- | --------------------------------- | ----------------------------------- | ------------------------------------- | | **BurnMint** | Tokens are **burned** (destroyed) | Tokens are **minted** (created) | None - deploy on any number of chains | | **LockUnlock** | Tokens are **locked** in escrow | Tokens are **released** from escrow | **Only ONE lockbox per mesh** | ### Why BurnMint is Preferred With BurnMint, tokens are destroyed on the source chain and created on the destination: * **No stored value risk**: No pool of tokens in escrow that could be targeted * **Unlimited scalability**: Deploy to any number of chains without constraints * **Simple supply accounting**: Total supply = sum of all chain balances ### When LockUnlock is Required LockUnlock is necessary when you have an **existing token without mint/burn capabilities**. Instead of destroying tokens, they're held in an escrow (the "lockbox"). On the destination, OFT tokens are minted that represent claims on the locked tokens. OFT LockUnlock mechanism - tokens locked in escrow on source, minted on destination OFT LockUnlock mechanism - tokens locked in escrow on source, minted on destination ### The Single Lockbox Rule **You can only have ONE LockUnlock deployment in your entire omnichain mesh.** All other chains must use BurnMint. **Why?** The lockbox must contain enough tokens to satisfy all possible redemptions. Consider what happens with multiple lockboxes: 1. Lockbox A on Ethereum holds 1M tokens 2. Lockbox B on Arbitrum holds 500K tokens 3. Users on other chains hold 1.5M OFT tokens total If all OFT holders try to redeem to Ethereum, Lockbox A only has 1M tokens - 500K redemptions would fail. This creates a **"run on the bank"** scenario where: * Messages are successfully sent requesting redemption * The lockbox doesn't have sufficient supply to fulfill them * Transactions revert, leaving users with tokens they can't redeem With a **single lockbox**, the entire circulating supply on external chains is always backed 1:1 by the lockbox. BurnMint deployments on other chains don't need backing because they destroy/create tokens rather than holding reserves. **Advanced: Multiple lockboxes are possible with additional mechanisms.** Stargate Pools use a credit-based rebalancing mechanism that limits maximum transfers per pathway, preventing runs on any single pool. However, this adds significant complexity to your deployment and requires careful liquidity management. For most use cases, a single lockbox with BurnMint on other chains is the recommended approach. **Prefer BurnMint when possible.** If your existing token has mint/burn capabilities (or you can add them), use BurnMint to avoid the single lockbox constraint. *** ## Deployment Process Select your blockchain platform from the guides below. Deploy your OFT (or OFTAdapter) on each chain you want to support. Connect your deployments using LayerZero DevTools so they recognize each other as peers. Set up [DVNs and Executors](/v2/developers/evm/configuration/dvn-executor-config) for your pathways. Send test transfers between chains to verify everything works. *** ## Platform Guides Ethereum, Arbitrum, Base, and 100+ EVM chains. SPL tokens with the Anchor framework. Move language on Sui. Move language on IOTA L1. Move language on Aptos. Dual HyperEVM/HyperCore architecture. *** ## Advanced Topics Send native gas tokens crosschain. Omnichain Non-Fungible Tokens for crosschain NFT transfers. Bundle token transfers with swaps, deposits, or other actions. Manage OFT liquidity across chains with OVault. *** ## Common Questions LayerZero supports 150+ chains. You can deploy your OFT to any combination of supported chains. Start with a few and expand as needed. Yes. Deploy your OFT to the new chain, run `lz:oapp:wire` to connect it, and your existing deployments don't need any changes. Deployment costs vary by chain (gas fees). Crosschain transfers cost LayerZero messaging fees (DVN + Executor fees), typically $0.01-$1 depending on the pathway. OFT uses "shared decimals" (default: 6) for crosschain transfers. The contracts handle conversion automatically. See the [OFT Technical Reference](/v2/concepts/technical-reference/oft-reference) for details. Yes. OFTs implement the standard OFT interface, which is compatible with Stargate's bridge UI at [stargate.finance](https://stargate.finance). # Introduction Source: https://docs.layerzero.network/index Official LayerZero Documentation. Build omnichain applications with crosschain messaging. Developer guides, API references, and deployment resources.

LayerZero Documentation

Crosschain interoperability, blockchain infrastructure, and financial applications

Blockchain Infrastructure

Crosschain & Interoperability

Financial Applications & Services

I'm Just Exploring

# V1 Deployed Endpoints, Message Libraries, and Relayer Source: https://docs.layerzero.network/v1/deployments/deployed-contracts Learn about V1 Deployed Endpoints, Message Libraries, and Relayer in LayerZero V2. Understand the architecture, core concepts, and how it enables omnichain i... **LayerZero V1 is deprecated.** This page is provided for reference and legacy integrations only. For new projects, please use [LayerZero V2](/v2/deployments/deployed-contracts). Below you can find a description of the main LayerZero V1 contracts and find the corresponding deployment information for each blockchain network LayerZero V1 supports. **Endpoint Id** values have no relation to **Chain Id** values. Since LayerZero spans both EVM and non-EVM chains, each Endpoint contract has a unique identifier known as the `endpointId` for determining which chain's endpoint to send to or receive messages from. When using LayerZero V1 contract methods, be sure to use the correct `endpointId` listed below: * `1xx`: refer to mainnet chains * `10xxx`: refer to testnet chains ## Contract Description | **Contract Name** | **Description** | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **EndpointV1** | The primary entrypoint into LayerZero V1 responsible for managing crosschain communications. It orchestrates message sending and receiving between various smart contract connections. | | **UltraLightNodeV2** | The Ultra Light Node (ULN) message library contract responsible for verifying and validating crosschain messages using oracle and relayer attestations. | | **RelayerV2** | A contract responsible for relaying crosschain messages. The relayer picks up messages from the source chain and delivers them to the destination chain's endpoint. | | **NonceContract** | Tracks message nonces to ensure proper message ordering and prevent replay attacks across chains. | | **SendUln301** | A send message library contract for LayerZero V1 that handles outbound message preparation and validation. | | **ReceiveUln301** | A receive message library contract for LayerZero V1 that handles inbound message verification and delivery. | ## Sepolia Testnet Endpoint The Sepolia testnet endpoint is connected to Ethereum, Arbitrum, and Optimism mainnets only. | **Property** | **Value** | | ----------------- | -------------------------------------------- | | **EndpointId** | `161` | | **Endpoint** | `0x7cacBe439EaD55fa1c22790330b12835c6884a91` | | **RelayerV2** | `0x306B9a8953B9462F8b826e6768a93C8EA7454965` | | **ULNV2** | `0x41Bdb4aa4A63a5b2Efc531858d3118392B1A1C3d` | | **NonceContract** | `0xc097ab8CD7b053326DFe9fB3E3a31a0CCe3B526f` | ## Checking Default Configs To see the default configuration for a given pathway (i.e., from `Chain A` to `Chain B`), you can use [LayerZero Scan's Default Checker](https://layerzeroscan.com/tools/defaults?version=V1). # Download LLM Files Source: https://docs.layerzero.network/v2/ai-resources AI and LLM resources for LayerZero development. Optimized documentation for AI assistants and code generation tools. Crosschain development with LayerZero V2. The documentation build process generates files optimized for large language models. These files contain either an index of all pages or the full content of the docs. | Category | Description | File | | ------------------ | ------------------------------------------- | ------------------------------------------------------------- | | Index | Navigation index of all documentation pages | [llms.txt](https://docs.layerzero.network/llms.txt) | | Full Documentation | Full content of all documentation pages | [llms-full.txt](https://docs.layerzero.network/llms-full.txt) | > **Note**: The `llms-full.txt` file may exceed the input limits of some language models. If you encounter > limitations, consider using the smaller `llms.txt` index. # Omnichain Applications (OApps) & Design Patterns Source: https://docs.layerzero.network/v2/concepts/application-design-patterns Omnichain Applications (OApps) are LayerZero-specific contracts with custom business logic for sending and receiving information between chains. OApps use... **Omnichain Applications (OApps)** are LayerZero-specific contracts with custom business logic for sending and receiving information between chains. OApps use LayerZero's universal interface to implement crosschain coordination through asynchronous messaging patterns. ```mermaid wrap theme={null} graph LR subgraph "Chain A" SENDER["Sender OApp"] ENDPOINT_A["LayerZero
Endpoint"] end subgraph "Chain B" RECEIVER_B["Receiver OApp B"] ENDPOINT_B["LayerZero
Endpoint"] end subgraph "Chain C" RECEIVER_C["Receiver OApp C"] ENDPOINT_C["LayerZero
Endpoint"] end subgraph "Chain D" TARGET_D["Target Contract"] end SENDER --> ENDPOINT_A ENDPOINT_A -->|"Channel 1: Push Messaging"| ENDPOINT_B ENDPOINT_A -->|"Channel 2: Push Messaging"| ENDPOINT_C ENDPOINT_A -.->|"Channel 3: Pull Messaging"| TARGET_D ENDPOINT_B --> RECEIVER_B ENDPOINT_C --> RECEIVER_C TARGET_D -.->|"Return data"| ENDPOINT_A ``` ### Design Pattern Categories OApp design patterns help teams decide and implement their contract business logic for crosschain coordination: 1. **Architecture Patterns**: How contracts coordinate and interact with one another across multiple chains (hub-spoke or symmetric business logic) 2. **Message Flow Patterns**: How messages travel between chains (one-way push, round-trip ping-pong, batch distribution, compose workflows) 3. **Message Processing Patterns**: How to control message processing (ordered delivery, rate limiting, conditional handling) 4. **Data Access Patterns**: How to retrieve information from other chains (pull messaging, crosschain queries) **LayerZero's Unopinionated Approach**: LayerZero is unopinionated about your business logic. You can design messaging to be identical on every network, or implement asymmetric coordination patterns. Ultimately, LayerZero is an open framework for you to design any crosschain interaction for any given purpose. **Key Insight**: These patterns are implementation strategies for your contract business logic, not protocol features. You choose and combine patterns based on your application's specific coordination requirements. ## 1. Architecture Patterns How contracts coordinate and interact with one another across multiple chains. ### Hub-Spoke Architecture (Asymmetric Coordination) One chain acts as the central "hub" with coordination logic, while other chains act as "spokes" with execution logic. The hub makes decisions, aggregates data, and distributes commands. Spokes report to the hub and execute received instructions. ```mermaid wrap theme={null} graph LR subgraph "Coordination Chain" HUB[OApp Hub Contract
Coordination Logic] end subgraph "Chain A" SPOKE_A[OApp Spoke Contract
Execution Logic] end subgraph "Chain B" SPOKE_B[OApp Spoke Contract
Execution Logic] end subgraph "Chain C" SPOKE_C[OApp Spoke Contract
Execution Logic] end HUB <-->|"Commands & Reports"| SPOKE_A HUB <-->|"Commands & Reports"| SPOKE_B HUB <-->|"Commands & Reports"| SPOKE_C ```
**Use Cases**: Crosschain governance, vault deposits (deep liquidity), oracle aggregation ### Point-to-Point Architecture (Symmetric Business Logic) All contracts have identical business logic and operate as equal peers. Each contract can initiate communication with any other contract, and all contracts handle messages the same way. No central coordinator - each contract maintains its own state while staying synchronized with peers. ```mermaid wrap theme={null} graph LR subgraph "Chain A" PEER_A[OApp Contract
Identical Business Logic] end subgraph "Chain B" PEER_B[OApp Contract
Identical Business Logic] end subgraph "Chain C" PEER_C[OApp Contract
Identical Business Logic] end PEER_A <-->|"Symmetric Messaging"| PEER_B PEER_B <-->|"Symmetric Messaging"| PEER_C PEER_A <-->|"Symmetric Messaging"| PEER_C ```
**Use Cases**: Token transfers (OFT), peer-to-peer protocols ## 2. Message Flow Patterns How messages travel between chains. Push messaging sends data from source to destination chains, with the source initiating and destination processing. ### Batch Send Send one message to multiple destination chains simultaneously: ```mermaid wrap theme={null} graph LR subgraph "Chain A" USER[User] OAPP_A[OApp A] end subgraph "Chain B" OAPP_B[OApp B] end subgraph "Chain C" OAPP_C[OApp C] end subgraph "Chain D" OAPP_D[OApp D] end USER -->|"Single call"| OAPP_A OAPP_A -->|"Message 1"| OAPP_B OAPP_A -->|"Message 2"| OAPP_C OAPP_A -->|"Message 3"| OAPP_D ```
**Fee Distribution Logic**: Batch send requires overriding the base OApp fee logic since you're summing multiple quotes into one payment and distributing to each send call: ```solidity wrap theme={null} // PSEUDOCODE: Batch send with fee distribution // Override fee check from equivalency to minimum threshold function _payNative(uint256 _nativeFee) internal override returns (uint256 nativeFee) { if (msg.value < _nativeFee) revert NotEnoughNative(msg.value); return _nativeFee; } function quoteBatch(uint32[] memory dstEids, bytes memory message) public view returns (MessagingFee memory totalFee) { // Sum fees for all destinations for (uint i = 0; i < dstEids.length; i++) { MessagingFee memory fee = _quote(dstEids[i], message, options, false); totalFee.nativeFee += fee.nativeFee; totalFee.lzTokenFee += fee.lzTokenFee; } } function sendBatch(uint32[] memory dstEids, bytes memory message) external payable { // Validate total fee upfront MessagingFee memory totalFee = quoteBatch(dstEids, message); require(msg.value >= totalFee.nativeFee, "Insufficient fee"); // Distribute to each destination for (uint i = 0; i < dstEids.length; i++) { MessagingFee memory fee = _quote(dstEids[i], message, options, false); _lzSend(dstEids[i], message, options, fee, payable(msg.sender)); } } ``` **Use Cases**: Configuration updates, price feeds, state synchronization ### Ping-Pong (ABA Pattern) Chain A sends to Chain B, which calls LayerZero Endpoint within its `_lzReceive` business logic to send back to Chain A: ```mermaid wrap theme={null} graph LR subgraph "Chain A" USER[User] OAPP_A[OApp A] end subgraph "Chain B" OAPP_B[OApp B] ENDPOINT_B[LayerZero Endpoint] end USER -->|"Send message"| OAPP_A OAPP_A -->|"Crosschain message"| OAPP_B OAPP_B -->|"_lzSend() in _lzReceive logic"| ENDPOINT_B ENDPOINT_B -->|"Return message"| OAPP_A ```
**Conditional Message Handling**: The `_lzReceive` business logic can support conditional handling based on message contents. If your application requires it, you can encode conditional identifiers in your message to determine what type of processing should occur: ```solidity wrap theme={null} // PSEUDOCODE: This pattern organizes your call logic into separate internal functions // instead of putting all logic directly inside _lzReceive // Define message type constants bytes32 constant PUSH = keccak256("PUSH"); // A->B standard message bytes32 constant PING_PONG = keccak256("PING_PONG"); // A->B->A ping-pong message function _lzReceive(Origin calldata origin, bytes32 guid, bytes calldata message, address, bytes calldata) internal override { (bytes32 msgType, bytes memory businessLogic) = abi.decode(message, (bytes32, bytes)); if (msgType == PING_PONG) { _lzReceiveAndReturn(origin, businessLogic); // Move ping pong logic to separate function } else { _lzReceiveOnly(businessLogic); // Move standard logic to separate function } } function _lzReceiveAndReturn(Origin calldata origin, bytes memory _message) internal { // Process incoming message (your business logic here) processMessage(_message); // Send response back to source chain (nested _lzSend call) bytes memory response = abi.encode(PUSH, generateResponse(_message)); _lzSend(origin.srcEid, response, options, MessagingFee(0, 0), payable(address(this))); } function _lzReceiveOnly(bytes memory payload) internal { // Standard message processing without return message (your business logic here) processMessage(payload); } ``` **Critical Gas Consideration**: This pattern requires off-chain gas planning. You must quote the B→A return cost off-chain and include it in your A→B execution options: ```mermaid wrap theme={null} graph LR subgraph "Off-Chain Planning" USER[User] QUOTE[Quote B→A Cost
Off-chain RPC call] end subgraph "Chain A" OAPP_A[OApp A] end subgraph "Chain B" OAPP_B[OApp B] ENDPOINT_B[LayerZero Endpoint] end USER -->|"Get B→A quote"| QUOTE QUOTE -->|"Include return cost in options"| USER USER -->|"Send A→B with return gas"| OAPP_A OAPP_A -->|"Crosschain message
(includes B→A gas)"| OAPP_B OAPP_B -->|"_lzSend() with provided gas"| ENDPOINT_B ENDPOINT_B -->|"Return message"| OAPP_A ```
**Key Insight**: The Executor uses the `msg.value` from your `lzReceiveOption` to fund the B→A return message. You must calculate this cost off-chain before sending the initial A→B message. **Use Cases**: Crosschain authentication, conditional execution, data verification ### Call Composer Two-step, non-atomic process where the primary message stores a compose message for later execution: ```mermaid wrap theme={null} graph LR subgraph "Chain A" USER[User] OAPP_A[OApp A] end subgraph "Chain B" OAPP_B[OApp B] ENDPOINT[LayerZero Endpoint
composeMsg storage] COMPOSER[Composer Contract] end USER -->|"Send message"| OAPP_A OAPP_A -->|"Crosschain message"| OAPP_B OAPP_B -->|"endpoint.sendCompose
(stores by GUID)"| ENDPOINT ENDPOINT -.->|"Separate tx
lzCompose call"| COMPOSER ```
**Key Insight**: The OApp calls `endpoint.sendCompose()` which stores a compose message tied to the LayerZero message GUID. The composer contract is called in a separate transaction, making this a non-atomic, fault-isolated process. **Use Cases**: Token transfers with automated actions, multi-step DeFi operations ## 3. Message Processing Patterns How to control message processing. ### Ordered Delivery OApp enforces strict sequence order by comparing protocol nonce with local nonce tracking: ```mermaid wrap theme={null} graph LR subgraph "Chain A" USER[User] OAPP_A[OApp A] end subgraph "Chain B" ENDPOINT_B[LayerZero Endpoint
Protocol nonce tracking] OAPP_B[OApp B
Local nonce check] end USER -->|"Send message"| OAPP_A OAPP_A -->|"Crosschain message"| ENDPOINT_B ENDPOINT_B -->|"Unordered by default"| OAPP_B ```
**Key Insight**: The LayerZero Endpoint has its own nonce tracking but delivers messages unordered by default. To implement ordered delivery, the OApp must compare the protocol nonce (from message origin) with its own local nonce tracking and enforce sequence requirements. **Use Cases**: Financial transactions, workflow dependencies, state machines ### Rate Limiting Control in-flight capacity per channel over time windows to prevent spam and ensure controlled interactions. Rate limiters track consumed capacity that decays over time. LayerZero's default rate limiter implementation tracks "in-flight" capacity that decays linearly over time. Unlike simple counters that reset at fixed intervals, this approach provides smooth capacity recovery. ```mermaid wrap theme={null} graph LR CONFIG[Per Channel Config
Limit: 100 units
Window: 60 seconds] --> CAPACITY[In-Flight Capacity
Decays linearly over time window] CAPACITY --> CHECK{Current Capacity +
New Request ≤ Limit?} CHECK -->|"Yes"| ALLOW[Allow Message
Update in-flight amount] CHECK -->|"No"| REJECT[Reject Message
Capacity exceeded] ```
**How It Works**: When a message/token transfer occurs, the rate limiter adds the amount to "in-flight" capacity. This capacity decays linearly over the configured time window. If adding a new request would exceed the limit, the request is rejected. This provides smooth capacity recovery rather than sudden resets. **Linear Decay Visualization**: ```mermaid wrap theme={null} graph LR T0[T=0
100 units
Limit Reached] --> T15[T=15s
75 units
Partial Capacity] T15 --> T30[T=30s
50 units
Partial Capacity] T30 --> T45[T=45s
25 units
Partial Capacity] T45 --> T60[T=60s
0 units
Full Capacity] ```
**Example**: With a 100-unit limit over 60 seconds, if the limit is reached at T=0, capacity decays at \~1.67 units/second. After 30 seconds, 50 units of capacity are available for new requests. Units can either be the number of individual OApp messages, or a specific value such as amount transferred per channel. #### Outbound Rate Limiting (Source Chain) Rate check occurs before sending crosschain message. Clean failure mode with no partial states. ```mermaid wrap theme={null} graph LR subgraph "Chain A" USER[User] OAPP_A[OApp A
Outflow Limiter] CHECK_A{Within
Capacity?} REJECT_A[Revert
Transaction] end subgraph "Chain B" OAPP_B[OApp B] end USER -->|"Send message/tokens"| OAPP_A OAPP_A --> CHECK_A CHECK_A -->|"Yes"| OAPP_B CHECK_A -->|"No"| REJECT_A ```
Transaction fails immediately on source chain - user retains funds, no crosschain state changes. #### Inbound Rate Limiting (Destination Chain) Rate check occurs after crosschain message arrives. Can create partial states requiring retry handling. ```mermaid wrap theme={null} graph LR subgraph "Chain A" USER[User] OAPP_A[OApp A] end subgraph "Chain B" OAPP_B[OApp B
Inflow Limiter] CHECK_B{Within
Capacity?} REJECT_B[Revert
Transaction] PROCESS[Receive
Message] end USER -->|"Send message"| OAPP_A OAPP_A -->|"Crosschain message"| OAPP_B OAPP_B --> CHECK_B CHECK_B -->|"Yes"| PROCESS CHECK_B -->|"No"| REJECT_B ```
Transaction succeeds on source but fails on destination - creates partial state where funds may be stuck in-flight. Applications must implement retry UX patterns. ##### Rate Limiter Configuration **Definition**: Per channel configuration with a limit (number of messages or token value) over a time window. Capacity decays linearly over the window duration, allowing gradual recovery. **Examples**: * **Message limiting**: 10 messages per 60 seconds per channel * **Token limiting**: 1000 USDC per 24 hours per channel * **Combined limiting**: Both message count and token amount restrictions per channel **Use Cases**: Spam prevention, regulatory compliance, treasury protection, system stability ## 4. Data Access Patterns How to retrieve information from other chains. Pull messaging requests data from other chains and returns responses to the requesting chain. ### Data Queries (lzRead) Request and retrieve state data from contracts on other chains: ```mermaid wrap theme={null} graph LR subgraph "Chain A" USER[User] OAPP_A[OApp A] end subgraph "Chain B" TARGET[Target Contract
State Data] end USER -->|"Send message"| OAPP_A OAPP_A -->|"Crosschain query"| TARGET TARGET -->|"Return data"| OAPP_A ```
**Use Cases**: Price feeds, state verification, crosschain calculations For comprehensive pull messaging patterns and implementation details, see [Omnichain Queries (lzRead)](/v2/developers/evm/lzread/overview). ## Exit Criteria Before proceeding to Module 5, you should be able to: 1. Explain what makes an app "omnichain" vs "multi-chain" 2. Identify which pattern fits your use case 3. Design a message schema for your application 4. Implement idempotent message handling ## Further Reading ### Official Documentation * [OApp Extensions & Design Patterns](/v2/developers/evm/oapp/overview#extensions--design-patterns) - Comprehensive pattern details and advanced examples * [OApp Quickstart](/v2/developers/evm/oapp/quickstart) - Step-by-step implementation guide ### Code References * [OApp.sol](https://github.com/LayerZero-Labs/devtools/blob/main/packages/oapp-evm/contracts/oapp/OApp.sol) - Core OApp interface and implementation * [OptionsBuilder.sol](https://github.com/LayerZero-Labs/devtools/blob/main/packages/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol) - Execution options helpers * [Test Mocks](https://github.com/LayerZero-Labs/devtools/tree/main/packages/test-devtools-evm-foundry/contracts/mocks) - Working pattern implementations ### Related Modules * Module 3: [LayerZero as Master Interface](./layerzero-protocol-architecture) - The interface OApps build on * Module 6: [Value Transfer Implementations](./value-transfer-implementations) - Specialized OApp patterns for tokens # Omnichain Composers Source: https://docs.layerzero.network/v2/concepts/applications/composer-standard Learn about Omnichain Composers in LayerZero V2. Understand key concepts for building omnichain applications. LayerZero enables secure crosschain messaging. **Composability** is a core requirement for building advanced, interconnected crosschain applications. LayerZero’s framework for composability breaks complex crosschain interactions into discrete, sequential steps rather than forcing all operations into one atomic transaction. This design not only simplifies development, but also ensures that each step achieves instant and irreversible finality. ## The Need for Crosschain Composability On a single blockchain, composability is straightforward – any smart contract can call others on the same network. However, when you have many different blockchains, things get siloed. A smart contract traditionally can only compose with contracts on its own chain, making it hard to build applications that span multiple networks​. This lack of interoperability leads to fragmented liquidity and user experiences, as developers have to deploy all instances of an app on each chain to reach users there. Crosschain composability aims to remove these barriers by letting contracts on different chains interact as easily as those on one chain. In other words, it unlocks an “omnichain” world where a single unified application can live across multiple blockchains. ## Horizontal Composability in LayerZero Diagram showing horizontal composability: operations are split into discrete sequential steps as separate message packets, enabling fault isolation and independent execution contexts for each crosschain operation Diagram showing horizontal composability: operations are split into discrete sequential steps as separate message packets, enabling fault isolation and independent execution contexts for each crosschain operation * **Mitigating atomicity limitations:**\ In crosschain scenarios, an all-or-nothing (atomic) transaction may seem ideal, but if one function call fails in a long chain of operations, the entire process is reverted. Horizontal composability mitigates this risk by treating each step as a separate message, reducing the potential for cascading failures. * **Improving crosschain user experience:**\ By splitting operations into independent messages, users experience more predictable outcomes. For example, one message may transfer tokens in one operation, while a follow-up message triggers additional logic such as staking or swapping. Each step has its own execution context and error handling, ensuring that a failure in one part doesn’t necessarily cancel the entire operation of *bridging*. * **Supporting advanced workflows:**\ The framework enables sophisticated multi-chain applications. Whether coordinating token transfers with additional business logic or initiating sequential actions on different chains, horizontal composability provides the flexibility needed to build robust, complex crosschain solutions. * **Ensuring instant guaranteed finality:**\ Finality is the assurance that once a transaction is confirmed, it cannot be reversed. LayerZero’s framework guarantees that every step in a crosschain operation reaches finality as soon as it is processed. This instant, irrevocable finality is invaluable in crosschain scenarios, as it prevents inconsistencies between chains and instills user trust, making crosschain interactions as reliable as single-chain transactions. ## How Composability Works 1. **Initial message dispatch:**\ The source application initiates a crosschain call using LayerZero’s messaging protocol. This call triggers a primary state change, such as transferring tokens or updating a record. 2. **Triggering a composed message:**\ After the primary operation is processed, the receiving application constructs and dispatches a follow-up, or composed, message. This secondary message is sent as an independent packet to the [LayerZero Endpoint](../protocol/layerzero-endpoint) and includes context such as a unique identifier, source chain data, and additional parameters needed for the next action (either from the sender or application itself). 3. **Composer role:**\ The same [Executor](../permissionless-execution/executors) service that delivered the initial message packet to the receiving application calls a dedicated composer contract for composed messages. When it receives a call, the composer processes the message and executes the next step in the workflow—whether that’s another state update, executing business logic, or interacting with an external protocol. In effect, the composer acts as a coordinator that links the independent steps together. 4. **Decoupled error handling:**\ Since each step is executed as a separate transaction, a failure in one composed message does not automatically revert the original crosschain operation. This decoupling allows issues to be isolated, retried, or compensated for without impacting the overall process. ## Broad Impact Across Environments Regardless of the underlying blockchain, the core principles of horizontal composability remain consistent: * **Message-based interaction:**\ Every step in the process is communicated as an independent message. * **Separation of concerns:**\ Each operation has a clear, self-contained responsibility, enhancing modularity and simplifying debugging. * **Flexible execution:**\ Developers can set gas limits, fee configurations, and execution parameters independently for each message. This flexibility ensures that every crosschain call is optimized for its specific environment. ## Further Reading For VM-specific guides, developers can refer to: * [EVM Composer Overview](../../developers/evm/composer/overview) * [Solana Composer Overview](../../developers/solana/composer/overview) By leveraging dedicated composer contracts and a structured messaging system, LayerZero’s horizontal composability framework allows developers to build resilient and complex crosschain applications. # lzAsset Managed Service Source: https://docs.layerzero.network/v2/concepts/applications/lzasset lzAsset is chain expansion as a service for token asset issuers. LayerZero deploys and manages pre-native token versions across 150+ chains using the... lzAsset is **chain expansion as a service** for token asset issuers. LayerZero deploys and manages pre-native token versions across 150+ chains using the Omnichain Fungible Token (OFT) architecture, providing immediate composable liquidity with a seamless upgrade path when issuers enable native minting. ## Overview lzAsset leverages LayerZero's OFT contracts to expand native assets to new blockchains using a repeatable, future-proof architecture. The service acts as a universal liquidity rail with point-to-point interoperability, unlocking distribution at scale with on-demand liquidity. Once a chain is connected, asset issuers can access pre-native versions of their assets through lzAsset. For example, the native asset USDG becomes the pre-native asset for USDG0. Each pre-native token is fully backed 1:1 by the native asset and aligns with issuer standards (such as Circle's Bridged USDC Standard) for contract ownership takeover by the asset issuer. All pre-native asset supply can be redeemed for the native asset on any chain with a native asset deployment. ## Architecture lzAsset uses a two-mesh system that separates issuer-controlled deployments from LayerZero-managed infrastructure: Architecture diagram showing lzAsset two-mesh system: a Native mesh controlled by the asset issuer and an lzAsset mesh managed by LayerZero, connected through a hop OFT Adapter for transfers between native and pre-native versions Architecture diagram showing lzAsset two-mesh system: a Native mesh controlled by the asset issuer and an lzAsset mesh managed by LayerZero, connected through a hop OFT Adapter for transfers between native and pre-native versions ### Native mesh The asset issuer deploys and controls their standard OFT implementations across selected chains. These are the sanctioned deployments that the issuer directly manages, maintains upgrade authority over, and configures security settings for. ### lzAsset mesh LayerZero deploys and manages corresponding pre-native OFT implementations (e.g., USDG0, PYUSD0, AUSD0) across all supported chains. These contracts are maintained by LayerZero Labs with security configurations, DVN selection, and operational management handled as a service. ### Hop composer The two meshes interoperate through a hop OFT Adapter that enables transfers between native and pre-native versions. Users can deposit native tokens into the adapter, which mints equivalent lzAsset tokens that can then travel across the lzAsset mesh. Conversely, users can redeem lzAssets for native tokens on any chain where native deployments exist. **Security isolation**: The only funds at risk are those deposited in the hop OFT Adapter. The pre-native mesh is fully backed 1:1, and all lzAsset supply can be redeemed for native assets. This architecture limits exposure while enabling broad distribution. ## How it works lzAsset is a collection of existing OFT standards working together with a Composer on the Hub Chain to route transfers between Native and lzAsset chains. Diagram showing lzAsset contract architecture: OFT Mint & Burn on lzAsset chains, OFT Adapter (Lockbox) on Hub chain, and Hop Composer orchestrating messaging between Native and lzAsset meshes Diagram showing lzAsset contract architecture: OFT Mint & Burn on lzAsset chains, OFT Adapter (Lockbox) on Hub chain, and Hop Composer orchestrating messaging between Native and lzAsset meshes ### Core Contracts #### On lzAsset chains **OFT Mint & Burn**: Implements an OFT with mint and burn capabilities. This architecture enables a point-to-point mesh where spoke chains transfer directly to one another by burning lzAssets at the source and minting them at the destination, without routing through a hub. The contract manages access control (minter/burner roles) for security. #### On Hub Chain **OFT Adapter**: Uses a **Lock/Unlock** pattern (Lockbox) to hold native tokens in escrow. This adapter locks assets when sending to the lzAsset mesh and unlocks them when receiving, serving as the bridge between the native and pre-native ecosystems. **Hop Composer**: A contract that orchestrates messaging between the Native Mesh and the lzAsset Mesh. It receives composed messages, decodes destinations, and routes packets. It also ensures robustness with permissionless **refund** (return to source) and **retry** (re-execute with higher gas) mechanisms for failed transactions. #### On Native Chains **OFT Mint & Burn**: The standard **Mint/Burn** pattern deployed by the asset issuer. These contracts are fully controlled by the issuer and interoperate with the lzAsset mesh via the Hub Chain's Composer. ### Transfer Flows #### Within the lzAsset mesh (Direct) User sends tokens from one lzAsset chain to another 1. **Source**: Burn tokens and sends LayerZero message 2. **Destination**: Mint tokens to recipient All spoke chains are wired point-to-point, enabling direct transfers without routing through the hub. #### Between meshes: Native → lzAsset (Routes through Hub) User sends tokens from a Native chain to an lzAsset chain 1. **Native**: Burn tokens and sends LayerZero message to **Hub Chain** 2. **Hub**: OFT Lockbox locks native tokens and triggers compose 3. **Hub**: Hop Composer routes to the lzAsset chain and sends LayerZero message to **lzAsset chain** 4. **lzAsset**: Mint tokens to recipient #### Between meshes: lzAsset → Native (Routes through Hub) User sends tokens from an lzAsset chain to a Native chain 1. **lzAsset**: Burn tokens, sends LayerZero message to **Hub Chain** 2. **Hub**: OFT Lockbox unlocks native tokens, triggers compose 3. **Hub**: Hop Composer routes to Native chain and sends LayerZero message to **Native Chain** 4. **Native**: Mint tokens to recipient ## Value proposition ### For asset issuers **Rapid expansion**: Deploy to 150+ chains without building and maintaining infrastructure for each chain. LayerZero handles deployment, security configuration, monitoring, and operational maintenance. **Compliance alignment**: Pre-native deployments follow established standards for contract ownership transfer, enabling issuers to take over deployments when ready to enable native minting on new chains. **Operational simplicity**: No ongoing maintenance required. LayerZero manages bridging, routing, rebalancing, and protocol upgrades automatically. **Brand protection**: Pre-native tokens clearly indicate their status (e.g., USDG0) while maintaining association with the native asset. When issuers enable native minting, every lzAsset balance seamlessly upgrades 1:1 to the native asset. ### For chain owners **Immediate liquidity**: Launch with day-one access to deep, interoperable liquidity across LayerZero's network, already trusted for over \$100B in bridge volume. No need for incentive programs or bootstrapping. **Trusted assets**: Users interact with familiar, fully-backed stablecoins and RWAs from day one. Pre-native versions maintain full backing and redeemability. **Instant composability**: DeFi protocols can immediately build lending markets, DEX pairs, and payment systems using trusted assets. All lzAssets implement the IOFT interface for unified integration. **Eliminates cold-start problem**: New chains receive standardized, composable liquidity without waiting for individual asset issuers to deploy. Chains become attractive deployment targets for DeFi protocols from launch. ## Benefits ### Immediate liquidity Unlock day-one access to deep, interoperable liquidity across LayerZero's 150+ supported chains. Asset issuers can instantly scale to meet demand anywhere liquidity flows. Chains receive trusted assets that users already know and protocols can immediately integrate. ### Future proof Stablecoins grew from approximately \$27B market cap in 2020 to over \$250B by mid-2025, representing the fastest-growing asset class in crypto. lzAsset positions both issuers and chains to capture the next wave of tokenized value on efficient, programmable rails. ### Instant trusted composability Access fully-backed stablecoins such as USDG0, PYUSD0, and AUSD0 across issuer-selected chains with zero slippage. Mint, burn, and redeem from the deepest liquidity hubs. DeFi protocols can instantly build on top of these assets, and all transfers use LayerZero's security-first approach with configurable DVN verification. ### Operational simplicity lzAsset handles bridging, routing, rebalancing, and token upgrades automatically. Asset issuers connect once to LayerZero and inherit a fully managed, security-audited liquidity rail with zero ongoing maintenance required. Chain operators receive battle-tested infrastructure without deployment complexity. ## Upgrade path When asset issuers enable native minting on a chain currently served by lzAsset: 1. **Seamless transition**: Every lzAsset balance automatically becomes redeemable 1:1 for the native asset 2. **No user action**: Holders don't need to swap, bridge, or manually upgrade their tokens 3. **Smart contract transition**: The issuer can take over the pre-native deployment contract following established standards for ownership transfer 4. **Maintained liquidity**: All existing integrations continue working through the IOFT interface This upgrade path ensures that early adopters and DeFi protocols building on pre-native assets face no disruption when issuers expand their native deployment strategy. ## Current adopters ### USDG0 Backed by native USDG, Paxos' regulated stablecoin backed by the Global Dollar Network (including Robinhood, Kraken, Mastercard, and others). USDG0 is available pre-natively via the Stargate Finance Hydra offering, giving blockchain ecosystems day-one access to compliant USD liquidity. ### PYUSD0 Backed by native PayPal USD (PYUSD), a regulated stablecoin issued by Paxos. Bridges Web2 fintech rails with Web3 applications to expand stablecoin utility across payments and DeFi. ### AUSD0 Collateralized by U.S. Treasuries, AUSD provides instant stablecoin liquidity and powers Citrea's Bitcoin-based applications with secure, yield-backed collateral. ## Chain partners lzAsset pre-native tokens are available across LayerZero's network of 150+ chains, including major L1s, L2s, and emerging blockchain ecosystems. The service provides chains with immediate access to trusted, composable liquidity without requiring individual negotiations with each asset issuer. ## Getting started ### For asset issuers If you're a stablecoin or RWA issuer interested in rapid multi-chain expansion: 1. **Contact business development**: Email [bd@layerzerolabs.org](mailto:bd@layerzerolabs.org) with details about your asset and target chains 2. **Technical review**: LayerZero team reviews your token architecture and compliance requirements 3. **Deployment coordination**: LayerZero deploys and configures the lzAsset mesh with appropriate security settings 4. **Integration support**: Ongoing support for exchange listings, DeFi integrations, and eventual native migration ### For chain owners If you're a blockchain operator seeking deep liquidity assets for your ecosystem: 1. **Contact business development**: Email [bd@layerzerolabs.org](mailto:bd@layerzerolabs.org) with details about your chain and liquidity needs 2. **Technical integration**: Ensure your chain supports LayerZero Endpoint deployment (EVM, Solana, Aptos, Sui, or other supported VMs) 3. **Asset selection**: Work with LayerZero to identify which lzAssets best serve your ecosystem 4. **Launch coordination**: LayerZero handles deployment, security configuration, and integration with the existing lzAsset mesh ### For DeFi developers If you're building on a chain with lzAsset tokens: 1. **Review [IOFT interface](/v2/concepts/technical-reference/oft-reference)**: All lzAssets implement the standard LayerZero token interface 2. **Use existing integration patterns**: Integrate once, support all LayerZero assets including lzAssets, native OFTs, and Stargate tokens 3. **Plan for native transition**: Design integrations that continue working when lzAssets upgrade to native ## Technical implementation lzAsset tokens are standard OFT implementations managed by LayerZero Labs. From a technical perspective, they function identically to any other OFT: * Implement the IOFT interface for transfers * Support composed messages via `lzCompose()` for DeFi integration * Use LayerZero DVNs for verification and Executors for delivery * Maintain unified supply across all chains through burn/mint mechanics * Provide fee quoting via `quoteSend()` for accurate cost estimation The key difference is operational: LayerZero handles deployment, security configuration, and maintenance rather than the asset issuer. ## Security model ### Isolation through architecture The hop OFT Adapter is the only point where native assets are held. This limits exposure to the adapter contract rather than the entire lzAsset mesh. All pre-native supply is backed 1:1 by assets in the adapter. ### LayerZero security All transfers use LayerZero's security infrastructure: * **DVN verification**: Multiple independent verifiers confirm message validity * **Executor delivery**: Separate delivery mechanism from verification * **Configurable security**: LayerZero Labs configures appropriate DVN requirements for each lzAsset ### Audited infrastructure lzAsset uses battle-tested OFT contracts that have processed billions in volume. The same contracts power Stargate Hydra and numerous independent OFT deployments across the LayerZero ecosystem. ## Comparison with alternatives ### lzAsset vs. deploying your own OFT | Aspect | lzAsset | Native OFT | | ---------------------- | ------------------------- | -------------------------------------- | | Deployment time | Immediate | Weeks to months per chain | | Operational overhead | Zero (managed service) | Ongoing monitoring and maintenance | | Security configuration | Handled by LayerZero Labs | Issuer selects DVNs and manages config | | Upgrade path | Seamless to native | N/A (already native) | | Control | LayerZero-managed | Full issuer control | | Cost | Service fee | Gas costs only | ### lzAsset vs. Stargate Hydra Both use similar OFT architecture for extending liquidity. Stargate Hydra focuses on established deep liquidity assets (USDC, USDT, ETH) managed through StargatePool and StargateOFT contracts. lzAsset focuses on stablecoin and RWA issuers who want their brand represented across chains with a clear path to native deployment ownership. ## Related resources * [OFT overview](/v2/concepts/applications/oft-standard) - Understanding the underlying token standard * [Composing overview](/v2/concepts/applications/composer-standard) - Understanding composability * [Stargate Finance](/v2/concepts/applications/stargate-finance) - Related managed liquidity service * [OFT technical reference](/v2/concepts/technical-reference/oft-reference) - Deep technical details on OFT mechanics ## Contact For asset issuers and chain operators interested in lzAsset: **Email**: [bd@layerzerolabs.org](mailto:bd@layerzerolabs.org) **Include**: Asset details (name, backing, compliance status), target chains, timeline, and any specific requirements. # Core Concepts for Omnichain Applications Source: https://docs.layerzero.network/v2/concepts/applications/oapp-standard LayerZero’s Omnichain Application (OApp) standard defines a generic crosschain messaging interface that allows developers to build applications which... LayerZero’s Omnichain Application (OApp) standard defines a **generic crosschain messaging interface** that allows developers to build applications which send and receive arbitrary data across multiple blockchain networks. Although implementations differ between Developer VMs, they share the following core concepts: ## Generic Message Passing Diagram showing crosschain messaging between Network A and Network B using the OApp Standard, with an arrow indicating the message flow via LayerZero Send and Receive Diagram showing crosschain messaging between Network A and Network B using the OApp Standard, with an arrow indicating the message flow via LayerZero Send and Receive * **Send & receive interface:**\ An OApp provides interface methods to *send* messages (by encoding data into a payload) and *receive* messages (by decoding that payload and executing business logic) via the LayerZero protocol. This abstraction lets you use the same messaging pattern for a variety of use cases (e.g., DeFi, DAOs, NFT transfers). * **Custom logic on receipt:**\ Each OApp is designed so that developers can plug in their application-specific logic into the message‐handling functions. Whether you’re transferring tokens, votes, or some other data-type, the core design remains the same. ## Quoting and Payment * **Dynamic fee estimation:**\ The standard provides a mechanism to *quote* the required service fees for sending a crosschain message in both the native chain token and in the protocol token, ZRO. This quote must match the gas or fee requirements at the time of sending. * **Bundled fee model:**\ The fee paid on the source chain covers all costs: the native chain gas cost and fees for the [service workers](../workers) handling the transaction on the destination chain (e.g., Decentralized Verifier Networks and Executors). This unified fee model simplifies crosschain transactions for developers and users alike. ## Execution Options and Enforced Settings * **Message execution options:**\ When sending a message, developers can specify execution options — such as the amount of gas to be used on the destination chain or other execution parameters. These options help tailor how the crosschain message is processed once it arrives. * **Enforced options:**\ To prevent misconfigurations or inconsistent execution, OApps can enforce a set of options (like minimum gas limits) that all senders must adhere to. This ensures that messages are processed reliably and prevents unexpected reverts or failures. ## Peer and Endpoint Management * **Trusted peers:**\ Every deployed OApp must set up trusted peers on the destination chains. This pairing (stored as a simple mapping) tells the protocol where to send messages to or expect messages from. The peer’s address is stored in a format (such as `bytes32`) that is interoperable between VMs. * **Endpoint Integration:**\ All crosschain messages are sent via a [standardized protocol endpoint](../protocol/layerzero-endpoint), which handles the low-level message routing, verification management, and fee management. This endpoint acts as the bridge between disparate chains. ## Administrative and Security Controls * **Admin and delegate roles:**\ The OApp design includes built-in roles for managing and configuring the application. Typically, the contract owner (or admin) holds the authority to update peers, set execution configurations, or transfer admin rights. A separate role, the *delegate*, can be used to manage critical operations like security configuration updates and block finality settings. * **Security measures:**\ Since crosschain operations carry extra risk, developers are encouraged to use additional safeguards (e.g., governance controls, multisig wallets, or timelocks) to secure critical roles like the *delegate* and *admin* to prevent unauthorized changes. ## Composition (Re-entrancy & Extended Flows) * **Message composition:**\ Beyond simple send/receive operations, the standard can also support composing messages. This “compose” feature allows an OApp to trigger a subsequent call to itself or another contract after a message has been delivered. This is particularly useful for advanced use cases where the crosschain message results in a series of actions rather than a single event. ## VM-Specific Implementation Notes * **EVM:**\ The OApp is implemented via Solidity contracts. Developers inherit from base contracts like `OApp.sol` that provide a complete messaging interface (including enforced options and fee quoting) while allowing custom logic in the `_lzReceive` function. * **Solana:**\ Instead of inheritance, Solana relies on Cross Program Invocation (CPI) where the LayerZero Endpoint CPI is used. Developers build their OApp program around a set of core instructions that mirror the send/receive flow. * **Aptos Move:**\ The Move-based OApp splits the logic into modular components (such as `oapp::oapp`, `oapp::oapp_core`, `oapp::oapp_receive`, and `oapp::oapp_compose`). Each module encapsulates parts of the messaging process—from fee quoting to message composition—while preserving the same overall flow. ## Further Reading For VM-specific guides, developers can refer to: * [EVM OApp Quickstart](../../developers/evm/oapp/overview) * [Solana OApp Reference](../../developers/solana/oapp/overview) * [Aptos Move OApp Overview](../../developers/aptos-move/contract-modules/oapp) This section highlights that, despite differences in language and runtime, the core concepts across LayerZero’s applications remain consistent—ensuring a unified crosschain experience regardless of the underlying blockchain. # Omnichain Tokens Source: https://docs.layerzero.network/v2/concepts/applications/oft-standard Learn about Omnichain Tokens in LayerZero V2. Understand the architecture, core concepts, and how it enables omnichain interoperability. Essential informatio... LayerZero’s omnichain token standards provide a unified framework to **transfer both fungible and non-fungible tokens across different blockchain networks**. ### Omnichain Token Standards #### OFT (Omnichain Fungible Token) A standard for fungible tokens that uses LayerZero messaging to debit on the source chain **(burn or lock)** and credit on the destination chain **(mint or unlock)**, preserving a single unified global supply across all connected networks. For new tokens, regular OFTs can be used, which utilizes the **burn/mint** mechanism on all chains. For existing tokens without an owner or mint authority, the **OFT Adapter** variation can be used which uses the **lock/unlock** mechanism on the original chain. An adapter contract can lock tokens on the source chain and **mint** on the destination, enabling omnichain transfers without modifying the original token contract. #### ONFT (Omnichain Non-Fungible Token) A standard for non-fungible tokens that uses LayerZero messaging to move NFTs between chains while preserving uniqueness and ownership. ONFTs support both burn-and-mint and adapter-based lock/mint/unlock patterns. ### Omnichain Tokens Principles Regardless of whether the tokens are built on EVM, Solana, or Aptos (or other environments), the underlying design follows the same core principles. ## Unified Crosschain Transfer Mechanism * **Generic message passing:**\ Both fungible (OFT) and non-fungible (ONFT) tokens rely on a common crosschain messaging interface defined in the [OApp Standard](./oapp-standard). This interface handles the sending and receiving of token transfer data between chains, abstracting away the underlying chain differences. * **Endpoint as a bridge:**\ All crosschain token transfers rely on the LayerZero Endpoint to route messages between chains. The endpoint handles service routing to the correct workers, fee management, and enforces the application's settings on the destination chain. ## Consistent Supply and Ownership Semantics * **Unified supply model:**\ For fungible tokens, the standard ensures that the token supply remains consistent across chains. On the sending side, tokens are either *burned* or *locked*—effectively removing them from circulation—while on the receiving side the same amount is *minted* or *unlocked*. This “movement” of tokens creates a unified global supply. Diagram showing unified supply model: tokens are burned or locked on the source chain and minted or unlocked on the destination chain, maintaining consistent global supply across networks Diagram showing unified supply model: tokens are burned or locked on the source chain and minted or unlocked on the destination chain, maintaining consistent global supply across networks * **NFT Transfer Patterns:**\ Non-fungible tokens (NFTs) follow a similar pattern: * **Burn & Mint:** The NFT is burned on the source chain and re-minted on the destination chain. * **Lock & Mint/Unlock:** Alternatively, an adapter can “lock” an existing NFT and later “unlock” it on the destination, preserving the original asset while enabling crosschain functionality. ## Flexible Design Patterns * **Direct vs. Adapter approaches:**\ Developers can choose between *direct implementations* where the token contract itself handles minting/burning and *adapter patterns* (where an intermediary or mint authority lock/burns tokens on one chain and unlock or mint them on another). Both approaches maintain unified supply and allow seamless crosschain transfers. * **Composable Execution:**\ The design supports “composed” messages. This means that after the core token transfer logic is executed, additional instructions or custom business logic can be triggered on the destination chain as a separate transaction, opening the door to advanced crosschain use cases. ## Robust Fee and Security Configuration * **Fee estimation and payment:**\ A built-in fee quoting mechanism estimates the cost of crosschain transfers. Whether you’re transferring fungible tokens or NFTs, the sender is provided with an accurate fee estimate that covers source chain gas, protocol fees, and destination chain execution. * **Configurable execution options:**\ Both token standards allow developers to set execution options (such as gas limits or fallback configurations) and enforce them to guarantee that sufficient resources are provided for the transfer on the destination chain. * **Administrative controls:**\ Robust access controls—through admin and delegate roles—ensure that only authorized parties can update configurations (such as peers, fee settings, security settings, and execution parameters), maintaining a high security standard for all crosschain operations. ## Seamless Developer Experience * **Abstraction over VM differences:**\ Although the underlying implementations may differ between environments (e.g., Solidity for EVM, Rust/Anchor for Solana, or Move for Aptos), the core concepts remain identical. Developers can rely on the same mental model: send a message that deducts tokens on the source chain and credits them on the destination, all while using a unified interface. * **Extensibility:**\ The design allows developers to extend or customize the token logic. Whether you need to add custom fee mechanisms, block certain addresses, or trigger additional events on token receipt, the standard’s modular approach makes it easy to integrate advanced features. ## Further Reading By abstracting the complexities of crosschain communication into a common framework, LayerZero enables the creation of omnichain fungible and non-fungible tokens that work seamlessly across different blockchains. This unified approach ensures that regardless of your target chain, you can maintain a consistent, secure, and developer-friendly token experience. For VM-specific guides, developers can refer to: * [EVM OFT Overview](../../developers/evm/oft/quickstart) * [EVM ONFT Overview](../../developers/evm/onft/quickstart) * [Solana OFT Overview](../../developers/solana/oft/overview) * [Aptos Move OFT Overview](../../developers/aptos-move/contract-modules/oft) You can refer to the specific documentation for each environment, but the core concepts—generic message passing, unified supply, configurable execution, and composable design—remain the same across all platforms. # Omnichain Vaults (OVault) Source: https://docs.layerzero.network/v2/concepts/applications/ovault-standard Omnichain Vaults extend the ERC-4626 tokenized vault standard with LayerZero's omnichain messaging, enabling users to deposit assets from any chain and... **Omnichain Vaults** extend the [ERC-4626 tokenized vault standard](https://ethereum.org/en/developers/docs/standards/tokens/erc-4626/) with LayerZero's omnichain messaging, enabling users to **deposit assets from any chain** and **receive yield-bearing vault shares on their preferred network** in a single transaction. ## What Are Omnichain Vaults? * **Beyond single-chain vaults:**\ Traditional ERC-4626 vaults restrict users to depositing and withdrawing on a single blockchain. OVault removes this limitation by making vault shares **omnichain fungible tokens (OFTs)** that can move seamlessly between any LayerZero-connected chain. * **Hub-and-spoke architecture:**\ The vault itself lives on one "hub" chain, while users can interact with it from any "spoke" chain. This design maintains the security and simplicity of a single vault while providing universal access across the entire omnichain ecosystem. Architecture diagram showing OVault hub-and-spoke design: users on spoke chains interact with a central vault on the hub chain through two OFT meshes (asset and share) connected by an ERC-4626 vault and composer contract Architecture diagram showing OVault hub-and-spoke design: users on spoke chains interact with a central vault on the hub chain through two OFT meshes (asset and share) connected by an ERC-4626 vault and composer contract ## Mental Model: Two OFT Meshes + Vault To understand OVault architecture, think of it as **two separate OFT meshes** (`asset` + `share`) connected by an ERC-4626 vault and composer contract on a hub chain: * **Asset OFT Mesh**: Enables the vault's underlying assets (e.g., `USDT`) to move across chains using standard OFT implementation * **Share OFT Mesh**: Enables vault shares to move across chains, using OFTAdapter (lockbox model) on the hub chain and standard OFT elsewhere * **ERC-4626 Vault**: Lives on the hub chain, implements standard `deposit`/`redeem` operations * **OVault Composer**: Orchestrates crosschain vault operations by receiving assets or shares with special instructions and coordinating the vault interactions with OFT transfers **Key insight**: Users never interact directly with the vault - they send assets or shares crosschain to the composer with encoded instructions, and the composer handles all vault operations and transfers out to the target destination. This design leverages existing LayerZero standards ([OFT](./oft-standard) + [Composer](./composer-standard)) to make asset movement seamless between multiple blockchains. ## Why Omnichain Vaults Matter * **Unified liquidity across chains:**\ Instead of fragmenting liquidity across multiple single-chain vaults, OVault aggregates all deposits into one vault. This creates deeper liquidity, more efficient yield generation, and simpler management for vault operators. * **Seamless user experience:**\ Users no longer need to bridge assets manually, switch networks, or manage multiple transactions. A single transaction handles the entire flow—from depositing assets on one chain to receiving shares on another. * **Crosschain DeFi composability:**\ Vault shares as OFTs can be used as collateral, traded on DEXs, or integrated into any DeFi protocol on any chain. This unlocks new possibilities for yield-bearing assets in the omnichain ecosystem. ## How Omnichain Vaults Work 1. **Asset deposit flow:**\ When a user deposits `assets` from a source chain, the OVault system: * Transfers the `assets` to the hub chain via **LayerZero's OFT standard** * Executes the deposit workflow via **LayerZero's Composer standard** which: * Deposits `assets` into the `ERC-4626` vault * Mints vault `shares` * Sends the `shares` to the user's desired destination chain address via the OFT standard 2. **Share redemption flow:**\ When redeeming shares for underlying `assets`: * `shares` are sent from the user's current chain back to the hub * The vault redeems `shares` for the underlying `assets` * `assets` are then sent to the user's chosen destination chain address 3. **Automatic error recovery:**\ If any step fails (due to slippage, gas issues, or configuration errors), the OVault Composer provides permissionless recovery mechanisms to refund or retry the operation, ensuring users never lose funds. ## Core Design Principles * **Full ERC-4626 compatibility:**\ OVault maintains complete compatibility with the ERC-4626 standard. The vault contract itself is a standard implementation—the omnichain functionality is added through LayerZero's OFT and Composer patterns. * **Deterministic pricing:**\ Unlike AMM-based systems, ERC-4626 vaults use deterministic share pricing based on `totalAssets / totalSupply`. This eliminates the need for oracles and reduces crosschain complexity. * **Permissionless recovery:**\ All error recovery functions are permissionless—anyone can trigger refunds or retries for failed operations. This ensures that users always have a path to recover their assets without relying on admin intervention. * **Configurable security:**\ Vault operators can configure their security settings, including DVN selection, executor parameters, and rate limits, to match their risk tolerance and use case requirements. ## Common Use Cases * **Yield-bearing stablecoins:**\ Issue stablecoins backed by yield-generating vaults where users can mint and redeem from any chain while the underlying yield accrues to all holders. * **Real World Asset (RWA) tokenization:**\ Deploy RWA vaults on regulated chains while providing global access. Users worldwide can gain exposure to real-world yields without jurisdictional limitations. * **Crosschain lending collateral:**\ Use vault shares as collateral on any chain. As the shares appreciate from yield accrual, borrowing power automatically increases. * **Omnichain yield aggregation:**\ Aggregate yield strategies from multiple chains into a single vault, giving users exposure to the best opportunities across the entire ecosystem. ## Integration with LayerZero Standards * **Built on OFT Standard:**\ Both the asset and share tokens use LayerZero's OFT standard for crosschain transfers, ensuring consistent supply accounting and seamless movement between chains. * **Leverages Composer Pattern:**\ The OVault Composer handles complex multi-step operations (receive assets → deposit → send shares) in a single atomic transaction with automatic error handling. * **Protocol-level security:**\ Inherits LayerZero's security model with configurable DVNs, executors, and rate limiting to protect crosschain operations. ## Further Reading For implementation guides and technical details: * [EVM OVault Implementation](../../developers/evm/ovault/overview) * [OFT Standard](./oft-standard) * [Composer Standard](./composer-standard) # Omnichain Queries (lzRead) Source: https://docs.layerzero.network/v2/concepts/applications/read-standard Learn about Omnichain Queries (lzRead) in LayerZero V2. Understand the architecture, core concepts, and how it enables omnichain interoperability. Essential ... **Omnichain Queries** extend LayerZero’s crosschain messaging protocol to enable smart contracts to **request** and **retrieve** onchain state from other blockchains. With lzRead, developers aren’t limited to simply sending messages — they can now pull data from external sources, bridging the gap between disparate networks in a fast, secure, and cost-efficient manner. ## What Is LayerZero Read? * **Beyond messaging:**\ Traditional crosschain messaging allows a contract to push state changes to another chain. Omnichain Queries, by contrast, let a contract *pull* information from other chains, acting like a universal query interface across multiple networks. * **Universal query language:**\ lzRead is built around the idea of a Blockchain Query Language (BQL) — a standardized way to construct, retrieve, and process data requests across various chains and even off-chain sources. Whether you need real-time data, historical state, or aggregated information, lzRead provides the framework to ask for and receive exactly what you need. ## Why Omnichain Queries Are Valuable * **Access crosschain data securely:**\ In a fragmented blockchain ecosystem, a smart contract on one chain can’t natively read data from another. lzRead fills that gap by using Decentralized Verifier Networks (DVNs) that securely fetch and verify data from target chains, ensuring trustless access to global state. * **Instant and cost-efficient data retrieval:**\ By optimizing the request–response flow, lzRead minimizes onchain gas costs and latency. Instead of incurring multiple round-trips and paying gas on several chains, lzRead’s design reduces the process to a single round of messaging on the source chain—leading to near-instant, final responses. * **Enhanced developer flexibility:**\ Whether you’re building decentralized finance (DeFi) protocols that need real-time price feeds, crosschain yield strategies, or decentralized identity solutions, lzRead’s framework gives you a flexible tool to integrate smart contract data from any blockchain without heavy infrastructure overhead. ## How Omnichain Queries (lzRead) Work Diagram showing lzRead workflow: Application sends a query through LayerZero endpoint, DVNs fetch and verify data from target chain archival nodes, then deliver the response back to the original chain Diagram showing lzRead workflow: Application sends a query through LayerZero endpoint, DVNs fetch and verify data from target chain archival nodes, then deliver the response back to the original chain 1. **Request definition:**\ An application initiates a read request by constructing a query that defines what data to fetch, from which target chain, and at which block or time. This query is encoded into a standardized command using BQL semantics. 2. **Sending the request:**\ The read request is dispatched through the LayerZero endpoint using a specialized message channel. Instead of sending an ordinary crosschain message, the command specifies that it’s a query—indicating that a response (and not just a state change) is expected. 3. **DVN data fetch and verification:**\ Decentralized Verifier Networks (DVNs) pick up the query, retrieve the requested data from an archival node on the target chain, and—if needed—apply off-chain compute logic (such as mapping or reducing responses) to process the data. Each DVN then generates a cryptographic hash of the result, ensuring data integrity. 4. **Response handling:**\ Once the data is fetched and verified by the required number of DVNs, the LayerZero endpoint delivers the final response back to the original chain using the standard messaging workflow. The receiving contract processes the response in its \_lzReceive() function, extracting and using the queried data as needed. 5. **Custom processing and compute settings:**\ If additional processing is required, the framework supports compute logic to transform or aggregate the data before it reaches your contract—allowing you to customize exactly how the data is formatted and used. ## Broad Impact Across Environments * **Chain-agnostic data access:**\ Although the internal implementations might differ, the core principle remains the same across all supported blockchains. lzRead provides a universal method for querying any chain’s data, making crosschain applications more integrated and interoperable. * **Flexible, low-latency, and secure:**\ By reducing the interaction to a single round of messaging (often called an “AA” message pattern), lzRead offers both low latency and cost savings compared to traditional multi-step query processes. And because the verification of data is handled by DVNs and enforced through cryptographic hashing, the system maintains high security with minimal additional trust assumptions. ## Conclusion Omnichain Queries (lzRead) improve how smart contracts access external state. Rather than being limited to local data or relying on cumbersome multi-step processes, developers can now issue a simple query to retrieve verified data from any supported blockchain. ## Further Reading For VM-specific guides, developers can refer to: * [EVM lzRead Overview](../../developers/evm/lzread/overview) # Stargate Finance Source: https://docs.layerzero.network/v2/concepts/applications/stargate-finance Stargate is a composable crosschain liquidity protocol built on LayerZero V2 as its transport layer. It provides unified liquidity pools for native... **Looking for the Stargate docs?** Stargate documentation now lives here as part of LayerZero. Stargate is a core application built on LayerZero - read through this page and the [Stargate Integration Guide](/v2/developers/evm/stargate/overview) for everything you need. [Stargate](https://stargate.finance) is a composable crosschain liquidity protocol built on **LayerZero V2** as its transport layer. It provides unified liquidity pools for native assets (USDC, USDT, ETH) across multiple blockchains, enabling seamless asset transfers without fragmenting liquidity. Stargate uses LayerZero V2 messaging infrastructure to coordinate liquidity between **StargatePool** contracts on chains with native assets and **StargateOFT** contracts on emerging chains needing access to deep liquidity, creating a unified network where any protocol can access native assets instead of bootstrapping their own crosschain infrastructure. ## Stargate's Dual Role Stargate plays two key roles in the LayerZero ecosystem: (1) a set of onchain smart contracts providing deep crosschain liquidity infrastructure, and (2) a unified frontend bridge that routes omnichain transfers for both Stargate assets and all LayerZero [Omnichain Fungible Tokens (OFTs)](./oft-standard). ### 1. Liquidity Protocol (EVM Smart Contracts) Stargate operates smart contracts that manage coordinated liquidity pools for native assets: * **StargatePool**: Holds native assets (e.g., USDC, ETH) on core chains with deep liquidity * **StargateOFT**: Mints backed representations (e.g., USDC.e) on emerging chains * **TokenMessaging**: Every transfer uses LayerZero messaging, DVNs, and Executors * **EVM Only**: Protocol contracts deployed on EVM chains. **For Developers**: * **StargatePool** contracts hold native ERC20 tokens (e.g., USDC) or native assets (e.g., ETH) and implement the IOFT interface for crosschain transfers * **StargateOFT** contracts have mint/burn authority on their corresponding ERC20 OFT tokens and implement the IOFT interface Both contract types are drop-in compatible with [LayerZero composability](./composer-standard) and other LayerZero contract standards (e.g., [Omnichain Vaults](./ovault-standard)). #### How Stargate V2 Works Stargate V2 uses two types of contracts to represent assets on multiple chains: ##### StargatePool (Native Chains) * Holds native assets (e.g., USDC, ETH) in credit-allocated liquidity pools * Deployed on major chains with existing deep liquidity in specific assets * Pools can transfer directly to other pools on native chains or to Hydra chains ##### StargateOFT (Hydra Chains) * Minted OFT representations backed by pool liquidity * Deployed on emerging chains where deep native liquidity doesn't yet exist * Can transfer point-to-point to other Hydra chains or redeem to native pools #### The Hydra Mechanism Stargate Hydra connects native liquidity pools with Hydra-enabled chains: Diagram showing Stargate Hydra mechanism connecting native liquidity pools on core chains with StargateOFT contracts on emerging Hydra chains, enabling instant access to deep pool liquidity without bootstrapping Diagram showing Stargate Hydra mechanism connecting native liquidity pools on core chains with StargateOFT contracts on emerging Hydra chains, enabling instant access to deep pool liquidity without bootstrapping **Key Benefits**: * **Liquidity Extension**: Emerging chains get instant access to deep pool liquidity * **No Bootstrapping**: No need to create new liquidity on every chain * **Full Redeemability**: Any Hydra OFT can be redeemed for native pool assets * **Unified Network**: All chains share the same liquidity base #### Credit Allocation System Stargate uses a credit allocation system to manage liquidity across **all Stargate contracts** (both StargatePool and StargateOFT) and ensure reliable transfers. **What are Credits?** Credits track token inflows and outflows in the protocol. Each pathway between chains (LayerZero [endpoint IDs](../glossary#endpoint-id)) has allocated credits that determine how much liquidity can move through that route. **Instant Guaranteed Finality** Thanks to credit allocation, Stargate provides **Instant Guaranteed Finality**: swaps are settled locally and immediately on the source chain, without risk of revert, rollback, or double spending. While you still wait for LayerZero to deliver tokens on the destination chain, Stargate guarantees the destination transaction will succeed. This is possible because Stargate maintains these invariants: * **For each pool**: Pool balance ≥ local unallocated credits + sum of allocated credits in remote paths * **For the system**: Sum of pool balances ≥ sum of OFT supplies + sum of total values locked **AI Planning Module (AIPM)** Credits in Stargate V2 are managed by the AI Planning Module, which conducts automated credit rebalancing via LayerZero messages. The AIPM: * Monitors transfer volume across all pathways * Dynamically reallocates credits to high-demand routes * Ensures optimal capital efficiency * Prevents credit shortages on active pathways **Stargate V1 vs V2**: Stargate V1 had static credits on pathways. Stargate V2 has dynamic credits, providing much greater capital efficiency through automated rebalancing. ### Credit Operations Credit increases/decreases are coordinated through LayerZero messages between chains using the `CreditMessaging` contract. All OFTs are backed by pool liquidity through credit guarantees verified by LayerZero DVNs. #### Transfer Types Stargate supports four transfer patterns, all coordinated through LayerZero V2 messaging and the credit allocation system: ##### Native Pool Transfers Diagram showing Stargate pool-to-pool transfer: direct transfers between core chains where native assets are locked/unlocked between StargatePool contracts Diagram showing Stargate pool-to-pool transfer: direct transfers between core chains where native assets are locked/unlocked between StargatePool contracts **Pool-to-Pool**: Direct transfers between core chains (e.g., Ethereum ↔ Arbitrum) where native assets are locked/unlocked between StargatePool contracts. This provides the most efficient path for moving established assets between chains with deep liquidity. ##### Hydra OFT Transfers Diagram showing Stargate Hydra OFT transfers: pool-to-OFT (core chain to Hydra), OFT-to-OFT (between Hydra chains), and OFT-to-pool (redeeming back to native assets) Diagram showing Stargate Hydra OFT transfers: pool-to-OFT (core chain to Hydra), OFT-to-OFT (between Hydra chains), and OFT-to-pool (redeeming back to native assets) **Pool-to-OFT**: Extending liquidity from core chains to Hydra chains (e.g., Ethereum → Bera) where native assets are locked in pools and minted as OFTs on emerging chains. **OFT-to-OFT**: Point-to-point transfers between Hydra chains (e.g., Bera ↔ Scroll) where OFTs are burned and minted without touching pool liquidity. **OFT-to-Pool**: Redeeming back to native assets (e.g., Bera → Ethereum) where OFTs are burned and native assets are unlocked from pools, providing global redeemability. All transfers use LayerZero V2 for secure crosschain communication, with DVNs verifying state changes and Executors handling automatic execution. ### 2. Universal Bridge Hub (Frontend Application) Because of its critical role in moving liquidity and proven LayerZero integration, Stargate also operates the **de facto bridge interface** for the broader omnichain ecosystem: **The [stargate.finance](https://stargate.finance) frontend supports**: * Stargate protocol assets (StargatePool and StargateOFT) * All LayerZero OFTs across any chain (EVM and non-EVMs) * Circle's CCTP and other common interoperability standards Diagram showing Stargate as Universal Bridge Hub: the stargate.finance frontend supporting Stargate protocol assets, all LayerZero OFTs across EVM and non-EVM chains, and Circle CCTP Diagram showing Stargate as Universal Bridge Hub: the stargate.finance frontend supporting Stargate protocol assets, all LayerZero OFTs across EVM and non-EVM chains, and Circle CCTP **Important:** The [stargate.finance](https://stargate.finance) frontend bridges LayerZero OFT transfers across EVM and non-EVM chains, but Stargate protocol contracts are EVM-only and live solely on LayerZero V2. Frontend support for non-EVMs does **not** mean the Stargate protocol itself covers non-EVM chains. ## Why Stargate Matters Stargate holds a unique position in the LayerZero ecosystem, serving as both critical infrastructure and the primary user gateway for omnichain assets. **As Protocol Infrastructure**: Stargate provides the foundational liquidity layer that other protocols build on. Instead of every DeFi protocol bootstrapping their own crosschain USDC or ETH infrastructure, they can simply integrate with Stargate's existing deep pools. This creates a network effect - as more protocols use Stargate liquidity, it becomes the standard liquidity layer for the ecosystem. **As Application Gateway**: Because Stargate already solved the hard problems of liquidity management and crosschain coordination at scale, the Stargate team built the most comprehensive bridge interface for the broader OFT ecosystem. The [stargate.finance](https://stargate.finance) frontend serves as the de facto bridge UI not just for Stargate assets, but for **all** LayerZero OFTs across any chain. **As Proof of Concept**: Stargate demonstrates that LayerZero V2 can support production-grade financial protocols with real economic value. It's not just a messaging layer - it's secure and reliable enough to manage hundreds of millions in liquidity across dozens of chains, with sophisticated credit allocation and automated rebalancing happening entirely through LayerZero messages. This dual role - providing both the liquidity infrastructure and the user interface - makes Stargate the central hub for omnichain asset movement in the LayerZero ecosystem. ## When to Use Stargate vs Custom OFT | Scenario | Recommendation | | ------------------------------------------ | ---------------------------------- | | Need USDC/USDT/ETH liquidity | ✅ Use Stargate protocol contracts | | Building vaults/lending with native assets | ✅ Use Stargate as underlying asset | | Launching a new token | ⚠️ Deploy your own OFT | | Need custom token economics | ⚠️ Deploy your own OFT | | Making existing ERC20 omnichain | ⚠️ Deploy OFT Adapter | ## Finding Stargate Contracts **LayerZero Deployments Page**: 1. Visit [OFT Ecosystem & Stargate Assets ](/v2/deployments/oft-ecosystem-stargate-assets) 2. Search for your chain or asset (e.g., "USDC") 3. Stargate contracts appear at the top of each chain's contract list **Stargate Resources**: * [Contract Addresses](https://docs.stargate.finance/resources/contracts/mainnet-contracts) * [Stargate API](https://mainnet.stargate-api.com/v1/metadata?version=v2) ## Transfer Modes & Composability Stargate supports two transfer modes with different composability capabilities: ### Taxi Mode * **Single transfer** per Stargate message * **Supports composability** - `composeMsg` can trigger additional actions on destination (e.g., vault deposits, swaps) * Automatically enabled when `composeMsg` is non-empty and `oftCmd` is empty * **Required for OVaults** and other composable strategies ### Bus Mode * **Multiple transfers** bundled in one Stargate message for gas efficiency * **Does NOT support composability** - no `lzCompose()` execution on destination * Optimized for simple transfers without additional logic ### Composability Requirement Composable strategies **require Taxi mode**. Bus mode will not trigger `lzCompose()` calls. All implementation guides in this documentation cover Taxi mode only. ## Next Steps **Learn More**: * [Stargate Protocol Docs](https://docs.stargate.finance) - Full protocol documentation and architecture * [OFT Standard](/v2/concepts/applications/oft-standard) - Understand the IOFT interface * [Value Transfer Patterns](/v2/concepts/value-transfer-implementations) - Compare approaches **Build**: * [Stargate Integration Guide](/v2/developers/evm/stargate/overview) - Integrate Stargate contracts * [OVault Overview](/v2/developers/evm/ovault/overview) - Build vaults with Stargate assets * [Find Contracts](/v2/deployments/deployed-contracts) - Get deployed addresses **Explore**: * [Stargate App](https://stargate.finance) - Use the bridge interface * [LayerZero Scan](https://layerzeroscan.com) - Track transfers * [Stargate Analytics](https://stargate.finance/overview) - View pool liquidity ## Summary **Stargate Protocol**: EVM smart contracts providing unified liquidity for native assets, built entirely on LayerZero V2 **Stargate Frontend**: Bridge UI serving as the universal gateway for all OFT transfers and common interoperability standards **For LayerZero Developers**: Use Stargate's deep liquidity pools instead of bootstrapping your own, or integrate via the frontend for multi-chain OFT support. Stargate demonstrates the power of building sophisticated financial protocols on LayerZero's messaging infrastructure. # What is LayerZero? Source: https://docs.layerzero.network/v2/concepts/getting-started/what-is-layerzero LayerZero is an omnichain messaging protocol — a permissionless, open framework designed to securely move information between blockchains. It empowers any... LayerZero is an omnichain messaging protocol — a permissionless, open framework designed to securely move information between blockchains. It empowers any application to bring its own security, execution, and crosschain interaction, providing a predictable and adaptable foundation for decentralized applications living on multiple networks. ## Before LayerZero Diagram illustrating attack vectors in traditional crosschain bridges: centralized verifiers and fixed signers creating single points of failure that put all connected applications at risk Before LayerZero, crosschain communication was a patchwork of monolithic bridges and isolated solutions. Achieving true crosschain communication was a complex and often fragile endeavor. Traditional methods relied on monolithic bridges with centralized verifiers or a fixed set of signers — approaches that imposed rigid structures and created single points of failure. When any component of these systems faltered, every connected application was put at risk, stifling innovation and leaving developers scrambling for secure solutions. ## The LayerZero Framework LayerZero redefines crosschain interactions by combining several key architectural elements: * **Immutable Smart Contracts:**\ Non-upgradeable endpoint contracts are deployed on each blockchain. These immutable contracts serve as secure entry and exit points for messages, ensuring consistency and trust across all networks. * **Configurable Message Libraries:**\ LayerZero offers flexible libraries that developers can select to tailor the way messages are emitted off-chain. This adaptability means applications can optimize message formatting and handling according to specific needs without being tied to a one-size-fits-all solution. * **Modular Security Owned by the Application:**\ Instead of relying on a centralized verifier network, LayerZero enables each application to configure its own security stack. Developers can choose from various decentralized verifier networks (DVNs) and set parameters like finality and execution rules. This modular approach shifts control to the application, allowing for tailored security that evolves with emerging technologies. * **Permissionless Execution:**\ By making the execution of crosschain messages available to anyone, LayerZero ensures that once a message is verified, it can be executed without gatekeepers. This open design removes bottlenecks and facilitates seamless interaction across the blockchain mesh. Together, these elements create a robust foundation that makes the following primitives possible. ## Getting Started with LayerZero Concepts New to blockchain interoperability? Start with our foundational concept modules that progressively build from basic principles to LayerZero's specific approach: ### Foundational Concepts * [Module 1: Interoperability Foundations](../interoperability-foundations)\ Explore the fundamental approaches to blockchain interoperability—from trusted intermediaries to cryptographic proofs—and understand the trade-offs that shape crosschain architecture decisions. * [Module 2: Interface Coupling Problems](../interface-coupling-problems)\ Learn why traditional bridges create tight coupling between applications and infrastructure, and how this architectural limitation constrains innovation and security. * [Module 3: LayerZero Protocol Architecture](../layerzero-protocol-architecture)\ Discover LayerZero's five-layer separation of concerns—from immutable endpoints to configurable libraries—that enables composable crosschain architectures without compromising security. ### Implementation Concepts * [Module 4: Verification & Execution Services](../verification-execution-services)\ Understand how LayerZero's worker services—Decentralized Verifier Networks (DVNs) and Executors—provide configurable security models and permissionless message delivery. * [Module 5: Application Design Patterns](../application-design-patterns)\ Master the core patterns for building Omnichain Applications (OApps), from basic messaging to advanced coordination patterns across multiple blockchains. * [Module 6: Value Transfer Implementations](../value-transfer-implementations)\ Learn how LayerZero's token standards (OFT, ONFT) implement secure crosschain asset movement through specialized messaging with token-specific invariants. These modules provide a structured learning path from theoretical foundations to practical implementation, equipping you with the knowledge to build secure, scalable omnichain solutions. ### Developer Documentation Ready to start building? Explore our platform-specific implementation guides and deployment resources: #### Platform-Specific Development * [EVM Development](../../developers/evm/overview)\ Complete guide to building LayerZero applications on Ethereum Virtual Machine chains using Solidity contract standards. * [Solana Development](../../developers/solana/overview)\ Build omnichain applications on Solana using Rust and Anchor framework with LayerZero's Solana programs. * [Aptos Move Development](../../developers/aptos-move/overview)\ Develop secure omnichain applications using the Move programming language on Aptos and other Move-based chains. * [Hyperliquid Development](../../developers/hyperliquid/hyperliquid-concepts)\ Learn to integrate with Hyperliquid's unique dual-network architecture using LayerZero Composer patterns. #### Network Support & Deployment * [Deployed Contracts](../../deployments/deployed-contracts)\ View all supported blockchain networks with their LayerZero V2 contract addresses, including Endpoints, Message Libraries, and Executors. * [DVN Addresses](../../deployments/dvn-addresses)\ Browse the complete list of Decentralized Verifier Networks (DVNs) that provide verification services for LayerZero applications across all supported chains. ## Further Reading Ready for deeper technical understanding? Explore LayerZero's core architecture components and key primitives: ### Core Architecture Components For detailed technical understanding of LayerZero's architectural elements: * [Protocol Overview](../protocol/protocol-overview)\ Learn how LayerZero defines secure messaging channels between sender and receiver contracts through immutable Endpoints and standardized Message Packets. * [Message Library Overview](../protocol/message-library)\ Understand the modular, immutable libraries that handle message encoding, verification, and processing across different blockchain environments. * [Workers Overview](../workers)\ Discover how Decentralized Verifier Networks (DVNs) and Executors provide verification and execution services through the unified Worker interface. * [Omnichain Applications Overview](../applications/oapp-standard)\ Explore the generic messaging interface that enables applications to send and receive arbitrary data across multiple blockchain networks. ### Key Primitives Built into LayerZero LayerZero's architecture provides a robust set of core primitives that redefine crosschain interaction: * [Omnichain Message Passing (Generic Messaging)](../applications/oapp-standard)\ This primitive enables applications to send and receive arbitrary data across a fully-connected mesh of blockchains. Applications can push state transitions to any network in the LayerZero mesh. * [Omnichain Tokens (OFT & ONFT)](../applications/oft-standard)\ Unified token standards that empower the crosschain transfer of both fungible and non-fungible tokens. These standards ensure a consistent global supply through mechanisms like burn/mint or lock/unlock—abstracting away the differences across blockchain environments and providing a seamless token experience. * [Omnichain State Queries (lzRead)](../applications/read-standard)\ Go beyond simple messaging—this primitive allows smart contracts to request and retrieve onchain state from other blockchains securely. It empowers your applications to "pull" data across chains efficiently. * [Omnichain Composability](../applications/composer-standard)\ By decoupling security from execution, this design enables developers to build complex, multi-step workflows across chains. It breaks down crosschain operations into discrete, manageable messages that achieve instant finality, facilitating advanced use cases and improved user experiences. # LayerZero V2 Glossary Source: https://docs.layerzero.network/v2/concepts/glossary Learn about LayerZero V2 Glossary in LayerZero V2. Understand the architecture, core concepts, and how it enables omnichain interoperability. Essential infor... This glossary defines and explains key LayerZero concepts and terminology. ## Chain ID The native blockchain identifier assigned by the network itself (for example, `1` for Ethereum Mainnet, `42161` for Arbitrum Mainnet). This is distinct from LayerZero's [Endpoint ID (EID)](#endpoint-id), which is the protocol's internal identifier used to route messages between chains. When interacting with the LayerZero protocol, you'll primarily work with EIDs rather than chain IDs. See [Endpoint](#endpoint) for more details. ## Channel / Lossless Channel A dedicated message pathway in LayerZero defined by four specific components: the sender OApp (source application contract), the source endpoint ID, the destination endpoint ID, and the receiver OApp (destination application contract). The channel maintains message ordering through nonce tracking, ensuring messages are delivered exactly once and in the correct sequence. For example, if a token bridge on Ethereum (sender OApp) is communicating with its counterpart on Arbitrum (receiver OApp), their messages flow through a unique channel distinct from all other application pathways between these chains. Each channel maintains its own independent message sequence, allowing multiple applications to communicate across the same chain pairs without interference. ## Compose / Composition The ability to combine multiple crosschain operations into a single transaction. Composition allows for complex crosschain interactions while maintaining transaction integrity across multiple chains. ## Composer A Composer is the smart contract that is responsible for executing a compose message. See also: [Compose / Composition](#compose-composition), [Executor](#executor), and [Message Options](#message-options). ## Escrow Account An escrow account is a financial arrangement where a third party, holds funds or assets on behalf of another until specific conditions are met. **Vertical Composability** The traditional form of smart contract composability, where multiple function calls are stacked within a single transaction. In vertical composability, all operations must succeed together or the entire transaction reverts, providing atomic execution. For example, when a crosschain token bridge receives tokens, it might atomically update balances, emit events, and trigger other contract functions. All these operations either complete successfully or fail together. **Horizontal Composability** LayerZero's unique approach to crosschain composability using `endpoint.sendCompose` and `ILayerZeroComposer`. Unlike vertical composability, horizontal composability allows a receiving contract to split its execution into separate atomic pieces. Each piece can succeed or fail independently, removing the requirement for all-or-nothing execution. This enables more flexible crosschain operations, as applications can handle partial successes and continue execution even if some components fail. For example, a crosschain DEX might receive tokens in one atomic transaction, then initiate a separate composed transaction for performing the swap, allowing the token receipt to succeed even if the swap fails. ## CPI (Cross Program Invocation) A CPI in Solana is when one program calls the instruction of another program. For more, refer to the [official Solana documentation](https://solana.com/en/docs/core/cpi) ## Destination Chain The blockchain network that receives and processes a LayerZero message. The destination chain hosts the contract that will execute the received message's instructions through its `lzReceive` function. ## DVN (Decentralized Verifier Network) A network of independent verifiers that validate message integrity between chains. DVNs are part of LayerZero's modular security model, allowing applications to configure multiple verification schemes for their messages. ### Dead DVN A placeholder DVN used when the default LayerZero configuration is inactive for a specific pathway. Dead DVNs appear when new chains are added before default providers (e.g., Google Cloud, Polyhedra) support every pathway. They function as null addresses - no verification will match, and messages will be blocked until the Dead DVN is replaced with a functional DVN. ## Endpoint The core, immutable smart contract deployed on each blockchain that serves as the entry and exit point for LayerZero messages. The Endpoint provides standardized interfaces for sending, receiving, and configuring messages. It's the primary interface through which applications interact with LayerZero. ### Endpoint Alt A variant of the LayerZero Endpoint (`EndpointV2Alt`) designed for blockchains where an ERC20 token serves as the de facto native currency for fee payments. While the standard Endpoint processes fees via the chain's native token sent as `msg.value`, Endpoint Alt accepts fees exclusively through ERC20 token transfers. This enables LayerZero to support chains where the native token has no economic value. See [LayerZero Endpoint Alt](/v2/concepts/protocol/layerzero-endpoint-alt) for details. ## Endpoint ID Endpoint ID (EID) is LayerZero's internal identifier used to route messages between chains. Each Endpoint contract has a unique EID for determining which chain's endpoint to send to or receive messages from. EID values have no relation to Chain ID values - since LayerZero spans both EVM and non-EVM chains, EIDs provide a unified addressing system across all supported blockchains. When using LayerZero contract methods, you'll work with EIDs rather than native chain IDs. The EID numbering convention follows a structured pattern: * **30xxx**: Mainnet chains * **40xxx**: Testnet chains To check if a LayerZero contract supports communication with another chain, use the `isSupportedEid()` method with the target chain's EID. See also: [FAQ — Endpoint ID vs Chain ID](/v2/faq#whats-the-difference-between-endpoint-id-eid-and-chain-id) ### Committer A committer is an off-chain process that monitors a crosschain message and, once it receives the required confirmations from the configured DVNs, submits a commit transaction to the destination chain. This action validates the message, making it ready for execution by the Executor. ## Executor Ensures the seamless execution of messages on the destination chain by following instructions set by the OApp owner on how to automatically deliver omnichain messages to the destination chain. An off-chain service that monitors message verification status and executes verified messages on destination chains when all required DVNs have verified the message. Executors handle gas payments and message delivery. It's a permissionless service that can be run by any party. ## GUID (Global Unique ID) A unique identifier generated for each LayerZero message that combines the message's nonce, source chain, destination chain, and participating contracts. GUIDs ensure messages can be tracked across the network and prevent replay attacks. ## Lazy nonce (lazy inbound nonce) A mechanism that tracks the highest consecutively delivered message number for a channel. Messages can be verified out of order, but they can only be executed sequentially starting from the lazy nonce. All messages before the message with lazyNonce have been verified. This ensures lossless message delivery while allowing parallel verification. ## LZ Config The file that declares the configuration for the OApp. Configuration refers to things such as the pathways (connections), DVN (Security Stack), and more. In our examples, this file has the default name of `layerzero.config.ts` but its name can be arbitrary. When needed, the LayerZero CLI expects the LZ config file via the `--oapp-config` flag. Check out the [LZ config in the OFT example](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft/layerzero.config.ts). ## `lzCompose` *First, see [Compose](#compose-composition) to understand what composition is.* A function that enables horizontal composition by allowing a received message to trigger additional crosschain messages. These composed messages are processed sequentially, creating chains of crosschain operations. ## `lzRead` Allows an OApp to request, receive and compute data from another blockchain by specifying the target chain and the block from which the state needs to be retrieved (including all historical data). ## `lzReceive` The standard function implemented by LayerZero-compatible contracts to process incoming messages. When a message is delivered, the destination chain's Endpoint calls `lzReceive` on the target contract with the decoded message data. ## `lzSend` The primary function used by the sender OApp to send messages through LayerZero. OApps call `endpoint.send()` on their local Endpoint, providing the destination details and message payload. The function initiates the crosschain messaging process. ## Mesh Network LayerZero's network topology where every supported blockchain can directly communicate with every other supported blockchain. This creates a fully connected network without requiring intermediate chains or bridges. ## Message Library (MessageLib) Smart contracts that handle message payload packing on the source chain and verification on the destination chain. MessageLibs are immutable and append-only, allowing protocols to add new verification methods while preserving existing ones. The Ultra Light Node (ULN) is the default MessageLib. [Ultra-Light Node](#uln-ultra-light-node) is an implementation of a Message Library. ## Message Options A required parameter in LayerZero transactions that specifies how messages should be handled on the destination chain. Message options must be provided either through enforced options configured at the application level or as explicit parameters in the transaction. These options control critical execution parameters like gas limits for `lzReceive` calls, composed message handling, and native token drops on the destination chain. When calling functions like `quote()` or `send()`, the protocol will revert if no valid message options are present. This is a safety mechanism to ensure every crosschain message has explicit instructions for its execution. Applications can enforce minimum gas requirements using `OAppOptionsType3`, which combines any user-provided options with the application's required settings. For example, an OFT contract might enforce minimum gas limits for token transfers while allowing users to specify additional gas for composed operations. ### Enforced Options Enforced options are OApp-level options that, when configured, must be included on every message to ensure minimum gas limits for a pathway. Enforced options can be set by the owner via `setEnforcedOptions`, typically populated during wiring from `layerzero.config.ts`. For each send call, the caller’s options are combined with the enforced options and any extra per-transaction options. See: [Enforcing Options](./message-options#enforcing-options) and [Simple Config](../tools/simple-config). ### Extra Options Per‑transaction options passed in `send`. Merged with enforced options to adjust gas/behavior or supply `msg.value`. See: [Extra Options](./message-options#extra-options). ## Nonce A unique identifier for the message *within specific messaging channel*. Prevents replay attacks and censorship by defining a strong gapless ordering between all nonces in each channel. Each channel maintains its own independent nonce counter. Difference between nonce and GUID: * Nonce is unique within a channel (between two endpoints) and sequential. * GUID is unique across all channels and is not sequential, allowing for tracking messages across the entire LayerZero network. ## OApp (Omnichain Application) A smart contract that implements LayerZero's messaging interface for crosschain communication. The base contract type for building omnichain applications. ## OFT (Omnichain Fungible Token) **Omnichain Fungible Token** - A token standard that extends fungible token standards such as the EVM's [ERC20](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/), [Solana's SPL / Token-2022](https://solana.com/ja/docs/core/tokens), and [Aptos' Fungible Asset](https://aptos.dev/en/build/smart-contracts/fungible-asset), with LayerZero's messaging capabilities, enabling seamless token transfers across different blockchains. OFTs maintain a unified total supply across all chains while allowing tokens to be transferred between networks. This standard works by debiting (burn / lock) tokens on the source chain whenever an omnichain transfer is initiated, sending a message via the protocol, and delivering a function call to the destination contract to credit (mint / unlock) the same number of tokens debited. This creates a unified supply across all networks LayerZero supports that the OFT is deployed on. Vanilla OFTs will utilize burn and mint: Diagram showing vanilla OFT burn-and-mint mechanism: tokens are burned on the source chain and minted on the destination chain via LayerZero messaging Diagram showing vanilla OFT burn-and-mint mechanism: tokens are burned on the source chain and minted on the destination chain via LayerZero messaging ### OFT Adapter An OFT Adapter enables an existing token (e.g. ERC-20, SPL token) to function as an OFT. The OFT Adapter contract serves as a lockbox for the original token. OFT Adapters will utilize lock and mint: Diagram showing OFT Adapter lock-and-mint mechanism: existing tokens are locked in an adapter lockbox on the source chain and minted on the destination chain Diagram showing OFT Adapter lock-and-mint mechanism: existing tokens are locked in an adapter lockbox on the source chain and minted on the destination chain ## OMP (Omnichain Messaging Protocol) The core protocol that enables secure crosschain communication. An OMP provides the fundamental messaging capabilities that higher-level applications build upon. ## ONFT (Omnichain Non-Fungible Token) Omnichain Non-Fungible Token - A token standard that extends ERC721 with LayerZero's messaging capabilities, enabling NFT transfers across different blockchains while maintaining their unique properties and ownership history. ## Packet The standardized formatted data structure for messages in LayerZero, containing the message payload along with routing and verification information. Packets include fields like nonce, source chain, destination chain and the actual message data. ## Payload The actual data being sent in a crosschain LayerZero message. This could be token transfer information, function calls, or any other data the application needs to transmit between chains. ## Peer A trusted OApp address on another blockchain configured for a specific crosschain pathway. Peers define which contracts are authorized to send messages to your OApp from each source chain. When your OApp receives a message, it validates that the sender matches the configured peer for that source endpoint. **Configuration**: Set via `setPeer(uint32 _eid, bytes32 _peer)` on EVM, or equivalent methods on other chains. **Validation**: In `lzReceive`, the OApp checks that `_origin.sender == peers[_origin.srcEid]` to ensure messages only come from trusted sources. **Address format**: * **EVM chains**: 20-byte address left-padded to 32 bytes * **Solana**: 32-byte public key * **Sui**: 32-byte package ID (where code is deployed, not object ID) * **Aptos**: 32-byte module address **Security**: Only messages from configured peers are accepted. Empty or misconfigured peer addresses will cause message delivery to fail. ## PDA (Program Derived Address) A PDA is a Solana account owned by a program and derived using "seeds". Refer to the official [Solana documentation](https://solana.com/docs/core/pda). ## Security Stack The combination of MessageLib, DVNs, and other security parameters that an application configures for its crosschain messages. Each application can (and should) customize its security stack to balance security, cost, and performance. ## Source Chain The blockchain from which a crosschain message is being sent. ## ULN (Ultra Light Node) The default MessageLib in LayerZero that implements a flexible verification system using configurable DVN sets. ULN allows applications to specify required and optional verifiers along with confirmation thresholds. `Ultra Light Node 302` is a MessageLib for Endpoint V2 applications. `Ultra Light Node 301` is a MessageLib for existing Endpoint V1 applications wanting to utilize the new Security Stack and Executor. ## Wire / Wiring "Wiring" in LayerZero refers to the process of connecting [OApps](#oapp-omnichain-application) across different blockchains to enable crosschain communication. The process involves setting peer addresses between OApps, configuring [DVNs](#dvn-decentralized-verifier-network), and message execution settings. All these actions are done via submitting transactions to the relevant contracts (e.g. OApp, [Endpoint](#endpoint)) on each chain. Once wired, contracts can send and receive messages between specific source and destination contracts. ## Worker A general term for offchain or onchain components that perform specific tasks in the LayerZero network, including executors and DVNs. ## X of Y of N A configurable security model pattern where: * **X**: This is the number of **required DVNs** — each one is a specific, non-fungible verifier network that must always verify a message. * **Y**: This is the total number of DVNs needed for a message to be considered verified. It includes the required DVNs (X) plus a set **threshold of optional DVNs**. Any of the optional DVNs can contribute toward this threshold since they are fungible; it doesn't matter which optional DVNs verify, as long as the required number is met. * **N**: This is the **total pool of DVNs** available for verification. It includes both the specific required DVNs (X) and all optional DVNs from which verification could be collected. For example, consider a "1 of 3 of 5" setup: * **X = 1**: One specific DVN must always sign (non-fungible). * **Y = 3**: A total of three DVNs are required. Since one is the required DVN, you need 2 additional verifier networks from the optional group (which are fungible). * **N = 5**: The application has configured five DVNs in total available for verification (1 required, plus a threshold of 2 out of a pool of 4 optional, which totals to 5 DVNs in the stack). In summary, "X of Y of N" means that out of a total pool (N) of DVNs, you must always have some specific DVN(s) (X) verify, and then you need additional verifications from the remaining pool (with any optional DVN counting) until you hit the overall threshold (Y). In pratice, this is done by setting an array of required DVN contract addresses, an array of optional DVN addresses, and a threshold for the optional DVNs. ## Delegate An address that an Omnichain Application (OApp) authorizes to act on its behalf within LayerZero's protocol. Specifically: * **Authorization:** The OApp calls `setDelegate(address _delegate)`, registering a delegate that can perform configuration changes. * **Permissions:** Once set, both the OApp itself **and** its Delegate are the **only** parties allowed to update LayerZero settings (e.g., security thresholds, channel configurations). Any unauthorized caller will revert with `LZ_Unauthorized`. * **Not the same as Owner:** The Delegate configures the Endpoint (libraries, DVN/security config, message-channel controls). The OApp **Owner** (`Ownable`) controls OApp policy — peers and enforced options — and is the only role that can change the Delegate. See the [Delegate vs Owner FAQ](/v2/faq#whats-the-difference-between-delegate-and-owner). * **RBAC variant:** OApps that use role-based access control instead of `Ownable` (such as the [Stablecoin OFT](/v2/developers/evm/stablecoin-oft/rbac-reference)) have no single owner. `DEFAULT_ADMIN_ROLE` replaces the owner, and `setDelegate` is disabled — the Delegate is permanently synced to the `DEFAULT_ADMIN_ROLE` holder. This ensures that each application can securely delegate configuration rights. ## Shared Decimals The "lowest common denominator" of decimal precision across all chains in the OFT system. It limits how many decimal places can be reliably represented when moving tokens cross‑chain. * **Default:** 6 (optimal for most use cases, since it still allows up to 2⁶⁴–1 units) * **Override:** If your total supply exceeds `(2⁶⁴–1) / 10^6`, you can override `sharedDecimals()` to a smaller value (e.g. 4), trading precision for a higher max supply. ## Stargate Finance A composable crosschain liquidity transport protocol built on LayerZero V2 that enables seamless asset transfers through unified liquidity pools. The Stargate **protocol** (EVM-only) consists of StargatePool and StargateOFT contracts using LayerZero messaging and a credit allocation system. The Stargate **frontend** is a multi-chain bridge UI that supports all LayerZero OFTs (EVM, Solana, Aptos, TON) and Circle's CCTP. See [Stargate Finance](/v2/concepts/applications/stargate-finance) for more details. ```solidity wrap theme={null} /// @dev Lowest common decimal denominator between chains. /// Defaults to 6, allowing up to 18,446,744,073,709.551615 units. function sharedDecimals() public view virtual returns (uint8) { return 6; } ``` ## Local Decimals The number of decimal places a token natively supports on the source chain. * **Example (EVM):** Most ERC‑20s use 18 local decimals. * **Example (Solana):** Many SPL tokens use 9 local decimals. * **Example (Aptos):** Many Fungible Asset tokens use 9 local decimals. > Tokens on different VMs may use different integer sizes (e.g. `uint256` vs `uint64`), so local decimals capture each chain's native precision. ## Decimal Conversion Rate The scaling factor used to "clean" a local‑decimal token amount down to the shared‑decimal precision before cross‑chain transfer, and to scale it back on the destination chain. ```solidity wrap theme={null} decimalConversionRate = 10^(localDecimals – sharedDecimals) ``` When you bridge a token, you **scale down** on the source chain to fit the shared precision, then **scale up** on the destination chain to restore your original decimals. 1. **Compute the rate** * For a typical ERC‑20: `localDecimals = 18`, `sharedDecimals = 6` → `rate = 10^12` 2. **Scale Down (remove "dust")** ```solidity wrap theme={null} // integer division drops any extra decimals uint256 sharedUnits = originalAmount / rate; ``` **Example:** * Original amount: 1.234567890123456789 tokens\ (that's `1_234_567_890_123_456_789` wei) * `sharedUnits = 1_234_567_890_123_456_789 / 10^12 = 1_234_567.890123456789` → **1 234 567** 3. **Bridge the "sharedUnits"** * Now you have a safe `uint64`‑friendly number: **1 234 567** 4. **Scale Up (restore local decimals)** ```solidity wrap theme={null} uint256 restored = sharedUnits * rate; ``` * `restored = 1_234_567 * 10^12 = 1_234_567_000_000_000_000` wei * Which is **1.234567000000000000 tokens** on the destination chain. Always do the "scale down" after subtracting any fees, so you don't accidentally round away more than intended. ## Dust The tiny remainder that gets dropped when you scale a token amount down to the shared‑decimal precision. In other words, any fractional units smaller than `1 / rate` (where `rate = 10^(localDecimals – sharedDecimals)`) become "dust." * **Precision safety:** By removing dust, you guarantee that every bridged amount fits within the shared-decimal limits of all chains. * **Rounding loss:** That leftover dust is returned to the sender, so you want to remove it *after* fees and before bridging to avoid accidentally rounding away more than intended. # Crosschain Verification & Interface Issues Source: https://docs.layerzero.network/v2/concepts/interface-coupling-problems Traditional bridges bundle interface, verification, and execution into monolithic systems, creating vendor lock-in and preventing composable crosschain... Traditional bridges bundle interface, verification, and execution into monolithic systems, creating vendor lock-in and preventing composable crosschain architectures. Interface coupling occurs when verification methods are inseparable from application interfaces, forcing developers to rewrite applications when changing security models or adding new chains. ## The Core Verification Problem **"How can Chain B verify an event from Chain A?"** Blockchains cannot directly read each other's state. Every crosschain system must solve this fundamental problem by choosing a verification approach (multisig, ZK proofs, light clients, optimistic verification). The chosen verification method determines: * **Security model**: Trust assumptions and failure modes * **Interface design**: Message formats and configuration options * **Execution semantics**: Gas delivery and retry mechanisms * **Chain coverage**: Which blockchains are supported ## How Traditional Bridges Create Coupling Traditional bridges bundle three concerns into one monolithic system: ```mermaid wrap theme={null} graph LR subgraph BRIDGE ["TYPICAL BRIDGE STACK"] direction TB I["Interface Layer
Contracts, encodings, calls"] V["Verification Layer
Opinionated trust model (validators, ZK, optimistic)"] E["Execution Layer
Opinionated Relayers, fee & retry semantics"] I -.-> V V -.-> E end CONSEQ["Implications
Vendor lock-in
Tight coupling to trust & gas semantics
Finite chain coverage"] BRIDGE -.-> CONSEQ ```
**The coupling problem emerges when**: * Applications need chains the bridge doesn't support * Security requirements change (e.g., upgrade from multisig to ZK proofs) * Multiple bridges become necessary for full chain coverage **Result**: N bridges = N different integrations with incompatible interfaces, making dynamic security selection and bridge aggregation difficult. ## Operational Complexity Interface coupling creates significant operational overhead beyond development complexity: **Monitoring & Observability**: Each bridge requires separate monitoring systems, different error formats, and distinct failure modes. Tracking a crosschain transaction across multiple bridges becomes a complex correlation problem. **Support & Debugging**: Issues must be diagnosed across N different systems with N different APIs, logs, and debugging tools. Root cause analysis becomes exponentially more difficult as bridge count increases. **Maintenance & Updates**: Each bridge integration requires separate maintenance cycles, different upgrade procedures, and independent security reviews. Operational teams must maintain expertise across multiple systems. ## The Solution: Separation of Concerns Separated architecture solves these problems by decoupling each layer: ```mermaid wrap theme={null} graph LR subgraph SEPARATED ["DECOUPLED ARCHITECTURE"] direction TB A["Application Layer
Portable business logic"] I["Interface Layer
Universal send/receive"] V["Verification Layer
Configurable per route"] E["Execution Layer
Permissionless delivery"] A -.-> I I -.-> V V -.-> E end BENEFITS["Benefits
Mix security models
Upgrade without redeployment
Compose protocols"] SEPARATED -.-> BENEFITS ```
**Separation enables**: * **Universal interface**: Single API for all crosschain interactions * **Unified observability**: Common monitoring, logging, and tracing across all pathways * **Configurable verification**: Choose security model per pathway * **Simplified operations**: Single system to maintain, monitor, and debug * **Upgrade paths**: Change verification without code changes ## Architectural Trade-offs **Coupled bridge architectures** provide simplicity at the cost of flexibility. Applications inherit a single verification model and cannot adapt security requirements without complete rewrites. **Separated architectures** enable configurability and composability but require developers to understand and configure multiple components independently. The coupled approach locks applications into specific bridge implementations, while separated architecture enables verification flexibility without code changes. ## See Also * Module 1: [Interoperability Foundations](./interoperability-foundations) - Types of crosschain interactions * Module 3: [LayerZero as Master Interface](./layerzero-protocol-architecture) - How LayerZero solves coupling * [Glossary](./glossary) - Interface, verification, coupling definitions **Note**: LayerZero-specific implementation details begin in Module 3. # Blockchain Interoperability Source: https://docs.layerzero.network/v2/concepts/interoperability-foundations Interoperability lets independent blockchains exchange data and value safely so applications can span multiple. LayerZero enables secure crosschain messaging. **Interoperability** lets independent blockchains exchange data and value safely so applications can span multiple networks. ## The Basic Model ```mermaid wrap theme={null} graph LR subgraph "Chain A" A[Source Contract] end subgraph "Chain B" B[Destination Contract] end A -->|Crosschain Message| B ```
* **Source** — A smart contract on Chain A that encodes (data or value) and emits a message. * **Crosschain Message** — A structured container that includes metadata about the source/destination chain and structured data for the destination contract's business logic. * **Destination** — A smart contract on Chain B that receives the message, decodes, and executes some requested business logic. ## Why It Matters Without interoperability, smart contract state, liquidity, and users remain **siloed per chain**, increasing fragmentation, friction, and operational overhead. Interoperability enables better UX, capital efficiency, and scalable architectures by coordinating logic and assets across chains. ## What Crosschain Smart Contracts Do Interoperable smart contracts can share data, update state, trigger actions, and transfer value between different blockchain networks seamlessly, as if these contracts were deployed on a single network. ### 1) Data Messaging Move structured data and/or trigger logic across smart contracts on different blockchains. #### **Push** A sends a message that B receives. ```mermaid wrap theme={null} graph LR subgraph "Chain A" U[User] A[Source Contract] end subgraph "Chain B" B[Destination Contract] end U -->|"Calls method"| A A -->|"Push crosschain message"| B ``` #### **Pull** A requests from B; B replies; A processes on arrival. ```mermaid wrap theme={null} graph LR subgraph "Chain A" U[User] A[Source Contract] end subgraph "Chain B" B[Destination Contract] end U -->|"Calls method"| A A -->|"Crosschain data request"| B B -->|"Return data"| A ``` *** **Data Messaging** is typically used for state synchronization, remote function invocation, orchestration, and governance. ### 2) Value Transfer Move assets between chains while preserving global supply/ownership invariants. Value transfer in practice can take various forms, depending on the type of asset being moved (native chain currency or fungible token smart contract) and whether the bridge is the asset issuer, but the underlying pattern is the exact same: the contract **debits** funds on one chain, and **credits** an equivalent amount on another chain. ```mermaid wrap theme={null} graph LR subgraph "Chain A" USER[User
-100 tokens] A[Source Contract] end subgraph "Chain B" B[Destination Contract] RECEIVER[Receiver
+100 tokens] end USER -->|"Call debits funds"| A A -->|Crosschain Message| B B -->|"Call credits funds"| RECEIVER ``` #### Canonical Burn/Mint Chain A **burns** tokens and Chain B **mints** the same amount, unifying supply across chains. ```mermaid wrap theme={null} graph LR subgraph "Chain A" U[User] A[Source Contract] BURN[Burn Address] end subgraph "Chain B" B[Destination Contract] R[Receiver] end U -->|"Calls transfer"| A A -->|"Burn tokens"| BURN A -->|"Crosschain message"| B B -->|"Mint tokens"| R ``` #### Lock/Unlock Chain A **locks** tokens on a home chain and represents elsewhere (typically Chain B **mints**). When B **burns**, A **unlocks** the original escrowed tokens. **Forward flow (A → B):** ```mermaid wrap theme={null} graph LR subgraph "Chain A" U[User] A[Source Contract] VAULT[Escrow Address] end subgraph "Chain B" B[Destination Contract] R[Receiver] end U -->|"Calls transfer"| A A -->|"Lock tokens"| VAULT A -->|"Crosschain message"| B B -->|"Mint representation"| R ``` *** **Reverse flow (B → A):** ```mermaid wrap theme={null} graph LR subgraph "Chain B" U2[User] B[Source Contract] BURN[Burn Address] end subgraph "Chain A" A[Destination Contract] VAULT[Escrow Address] R2[Receiver] end U2 -->|"Calls transfer"| B B -->|"Burn representation"| BURN B -->|"Crosschain message"| A A -->|"Unlock from escrow"| VAULT VAULT -->|"Send original tokens"| R2 ``` #### Liquidity Pools Chain A transfers assets to pool A destination pays from pool B (fees/slippage possible). ```mermaid wrap theme={null} graph LR subgraph "Chain A" U[User] A[Source Contract] POOL_A[Pool A] end subgraph "Chain B" B[Destination Contract] POOL_B[Pool B] R[Receiver] end U -->|"Calls transfer"| A A -->|"Add tokens to pool"| POOL_A A -->|"Crosschain message"| B B -->|"Pay from pool"| POOL_B POOL_B -->|"Send tokens"| R ``` *** **Value Transfer** is typically used for crosschain payments, asset bridging, liquidity provision, and treasury management. Value transfer is simply data messaging with token‑specific invariants/settlement logic. ## How Crosschain Messages Can Be Trusted and Delivered This concept applies to **both** Data Messaging and Value Transfer. ### A) Verification (state correctness) Each blockchain is a network of nodes reaching consensus about the state of that specific chain. In principle, this means that nodes from one blockchain network and the consensus they reach is entirely independent of another blockchain network. Each network is effectively an isolated island without any knowledge of external state outside its own domain. **The Core Challenge:** How can Chain B trust a statement about Chain A when Chain B's validators have no direct way to verify what happened on Chain A? Because of this limitation, **every crosschain message requires verification**. Different approaches involve different trust assumptions and trade-offs: #### Verifier Networks ```mermaid wrap theme={null} graph LR subgraph "Chain A" A[Smart Contract A] end subgraph "Chain B" B[Smart Contract B] end subgraph "Off-Chain Verification" VN[Verifier Network] end A -->|"Event: Message sent"| VN VN -->|"Proof: Message verified"| B ```
Verifier Networks act as **independent observers** that watch blockchain events and provide proof to the destination chain that a state change occurred on the source network. They are opinionated about how crosschain messages are routed, what chains they support, and what trust assumptions are used to confirm the state of a message from another chain. > **Note**: The following is a simplified mental model. In practice, security depends on implementation details, economic incentives, validator sets, and attack vectors specific to each system. | Approach | Core Assumption | Typical Characteristics | | ------------------------ | --------------------------------------------- | ------------------------------------------------ | | **Light / zk client** | Chain consensus or validity proofs | High assurance; higher cost/latency | | **Committee / multisig** | Honest majority of a known validator set | Fast; social/economic security; validator risks | | **Optimistic** | Fraud proofs during a challenge window | Delayed finality; capital‑efficient | | **Middlechain** | Shared validator set and middlechain liveness | Aggregated security; inherits middle layer risks | **Key insight**: There's no universal "best" approach - the optimal choice depends on your application's value-at-risk, latency requirements, and acceptable trust assumptions. As this trilemma between verification security, cost, and latency evolves, new verifier networks may emerge with better properties that serve applications better than existing solutions. ### B) Transport & Message Delivery Transport layers handle the routing and delivery of crosschain messages, managing the flow from source to destination contracts. **Question:** How are verified messages actually delivered on the destination chain? ```mermaid wrap theme={null} graph LR subgraph "Chain A" U[User] SENDER["Sender Contract"] TL_A["Transport Layer
Contract"] end subgraph "Chain B" TL_B["Transport Layer
Contract"] RECEIVER["Receiver Contract"] end U -->|"Initiates"| SENDER SENDER -->|"Message"| TL_A TL_A -->|"Crosschain Delivery"| TL_B TL_B -->|"Delivers Message"| RECEIVER ```
Crosschain systems must solve fundamental delivery challenges: ensuring messages arrive reliably, handling ordering requirements, and preventing replay attacks. Different applications have varying needs - some require strict ordering, while others can tolerate unordered delivery for better performance. **Key considerations:** * **Message ordering**: Whether messages must arrive in sequence or can be processed independently * **Delivery guarantees**: Handling network failures, ensuring messages aren't lost or duplicated * **Replay protection**: Preventing the same message from being executed multiple times ### C) Execution & Gas Management Executors are external services that monitor for verified messages and pay the gas costs to execute them on destination chains.\ **Question:** Who pays for destination chain execution and how is it funded? ```mermaid wrap theme={null} graph LR subgraph "Chain A" U[User] A[Smart Contract A] end subgraph "Chain B" TL_B[Transport Layer] B[Smart Contract B] end subgraph "Off-Chain Services" EX[Executor
Gas Provider] end U -->|"Initiates + Pays Gas"| A A -->|"Message Ready"| TL_B EX -->|"Monitors and Executes
Uses own gas"| TL_B TL_B -->|"Delivers Message"| B ```
Every crosschain message requires someone to submit a transaction on the destination chain and pay gas fees. This creates an economic coordination problem: **the message sender is on a different chain from where execution costs are incurred**. **Key considerations:** * **Execution responsibility**: Whether delivery is permissionless, automated, or manual * **Gas funding**: How destination chain fees are paid and by whom * **Economic incentives**: Ensuring reliable delivery through proper fee markets and incentive alignment ## Complete Crosschain Message Flow Now combining all three components, here's how a complete crosschain message flows through verification, transport, and execution: ```mermaid wrap theme={null} graph LR subgraph "Chain A" U[User] SENDER["Sender Contract"] TL_A["Transport Layer
Contract"] end subgraph "Chain B" TL_B["Transport Layer
Contract"] RECEIVER["Receiver Contract"] end subgraph "Off-Chain Services" VN[Verifier Network] EX[Executor
Gas Provider] end U -->|Initiates + Pays Gas| SENDER SENDER -->|Message| TL_A TL_A -->|Event: Message sent| VN VN -->|Proof: Message verified| TL_B TL_B -->|Message ready| EX EX -->|Monitors and Executes
Uses own gas| TL_B TL_B -->|Delivers Message| RECEIVER ```
1. **Send** on Chain A: User calls method, contract updates state and notifies transport layer 2. **Notify**: The transport layer notifies the verifier network to produce proof/attestation of what happened on A 3. **Mark Verified**: Verifier network marks message as verified in Chain B's transport layer 4. **Detect**: Executor detects verified message ready for delivery 5. **Deliver & Execute**: Executor calls transport layer to deliver message to destination contract ## See Also * [Glossary](./glossary) — canonical terms used throughout * Module 2: [Crosschain Verification & Interface Issues](./interface-coupling-problems) * Module 3+: [Protocol‑specific implementation details](./layerzero-protocol-architecture) # LayerZero Protocol Architecture Source: https://docs.layerzero.network/v2/concepts/layerzero-protocol-architecture LayerZero is an omnichain interoperability protocol that provides a stable, immutable interface for crosschain. LayerZero enables secure crosschain messaging. LayerZero is an omnichain interoperability protocol that provides a stable, immutable interface for crosschain messaging. By separating interface, verification, and execution into independent layers, LayerZero enables composable crosschain architectures without compromising security. ## Architecture: Five Independent Layers ```mermaid wrap theme={null} graph LR APP["Application
Business logic only"] --> ENDPOINT["Protocol Interface
Immutable endpoints"] ENDPOINT --> LIBRARIES["Configurable Libraries
Modular per pathway"] LIBRARIES --> VERIFICATION["Verification
N+1 verifier networks"] LIBRARIES --> EXECUTION["Execution
Permissionless delivery"] ```
Each layer has one job. They work together but remain independent. ## 1. Business Logic Interface (OApp) **Omnichain Applications (OApps)** are LayerZero's definition for smart contracts that use LayerZero to send and receive crosschain messages. Building on [Module 1's](./interoperability-foundations) data messaging concepts, the LayerZero protocol allows applications to define any custom data as bytes and send them as crosschain **messages**. The OApp standard provides a consistent application interface for invoking the LayerZero protocol with this custom business logic: ```solidity wrap theme={null} // SPDX-License-Identifier: MIT import {OApp, MessagingFee, Origin} from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import {OptionsBuilder} from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; contract MyOmnichainApp is OApp { using OptionsBuilder for bytes; constructor(address endpoint) OApp(endpoint, msg.sender) {} function quoteMessage(uint32 dstEid, bytes memory message, bool payInLzToken) external view returns (MessagingFee memory fee) { bytes memory options = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(200_000, 0); // Returns the cost of sending a message in either native gas token or ZRO return _quote(dstEid, message, options, payInLzToken); } function sendMessage(uint32 dstEid, bytes memory message) external payable { // Unordered by default; requests destination chain gas units for message delivery bytes memory options = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(200_000, 0); // If you need ordering: // options = options.addExecutorOrderedExecutionOption(); _lzSend( dstEid, // highlight-next-line message, combineOptions(dstEid, SEND, options), MessagingFee(msg.value, 0), payable(msg.sender) ); } function _lzReceive( Origin calldata origin, bytes32 guid, // highlight-next-line bytes calldata message, address /*executor*/, bytes calldata /*extraData*/ ) internal override { // your logic } } ``` The OApp standard acts as a facade that wraps the raw LayerZero protocol interface with developer-friendly methods. Instead of calling `endpoint.send()` and `endpoint.lzReceive()` directly, OApps use `_lzSend()` and `_lzReceive()` which handle common patterns like fee quoting, message validation, and error handling. Your application code focuses on business logic (encoding/decoding custom data, state transitions, etc.) while the OApp facade manages protocol interaction. ## 2. Protocol Interface (Endpoint) The LayerZero Endpoint serves as the **single entrypoint and exitpoint** for all crosschain messaging on a blockchain. Each chain has one LayerZero Endpoint contract that can send and receive messages between any other LayerZero Endpoint contract on any supported chain. The Endpoint provides the universal, immutable protocol interface: ```solidity wrap theme={null} // Excerpt of ILayerZeroEndpointV2 - core messaging functions // Core data structures struct MessagingParams { uint32 dstEid; // Destination endpoint ID bytes32 receiver; // Receiver address (bytes32 for crosschain compatibility) bytes message; // Message containing application-specific business logic bytes options; // Execution options (gas, native drops, etc.) bool payInLzToken; // Payment method (native chain token vs LZ token) } struct MessagingReceipt { bytes32 guid; // Globally unique identifier for tracking uint64 nonce; // Message sequence number for ordering MessagingFee fee; // Actual fee charged } struct MessagingFee { uint256 nativeFee; // Fee in native gas token uint256 lzTokenFee; // Fee in LZ token (alternative payment) } struct Origin { uint32 srcEid; // Source endpoint ID bytes32 sender; // Sender address (bytes32 for crosschain) uint64 nonce; // Message nonce for ordering } interface ILayerZeroEndpointV2 { // Events for tracking message lifecycle event PacketSent(bytes encodedPayload, bytes options, address sendLibrary); event PacketVerified(Origin origin, address receiver, bytes32 payloadHash); event PacketDelivered(Origin origin, address receiver); // Quote fees before sending function quote(MessagingParams calldata _params, address _sender) external view returns (MessagingFee memory); // Core send primitive - emits message and returns receipt function send( MessagingParams calldata _params, address _refundAddress ) external payable returns (MessagingReceipt memory); // Mark message as verified function verify(Origin calldata _origin, address _receiver, bytes32 _payloadHash) external; // Check if message can be verified function verifiable(Origin calldata _origin, address _receiver) external view returns (bool); // Check if message passes the target receiver's checks function initializable(Origin calldata _origin, address _receiver) external view returns (bool); // Core receive primitive - delivers message to destination contract function lzReceive( Origin calldata _origin, address _receiver, bytes32 _guid, bytes calldata _message, bytes calldata _extraData ) external payable; // ... additional configuration and management functions } ``` Because every LayerZero Endpoint is **immutable** and **permissionless** to interact with, anyone can use a LayerZero Endpoint for crosschain messaging without requiring authorization, integration approval, or dependency on external bridge operators. This creates a fully independent transport layer that, as we'll expand on in later sections, serves as the ideal crosschain messaging interface for nearly all crosschain applications. The immutable nature ensures that the interface will never change, providing permanent compatibility for applications built on LayerZero. The permissionless design means **no gatekeepers can restrict access or censor transactions**, creating a truly open crosschain infrastructure. **Key Properties**: * **Universal**: Same interface on every supported chain * **Immutable**: Cannot be upgraded or changed * **Permissionless**: Anyone can call (with proper fees) * **Chain-agnostic**: Works across EVM, Solana, Aptos, etc. (see non-EVM sections for specific sister implementations) ### Channels: OApp-to-OApp Communication A **channel** in LayerZero is uniquely defined by four components: * **Sender OApp**: The contract initiating the message * **Source Endpoint**: The LayerZero endpoint on the source chain * **Destination Endpoint**: The LayerZero endpoint on the destination chain * **Receiver OApp**: The contract receiving the message Each unique combination creates an independent channel with its own configuration: ```mermaid wrap theme={null} graph LR subgraph "Chain A" SENDER["Sender OApp"] ENDPOINT_A["LayerZero
Endpoint"] end subgraph "Chain B" RECEIVER_B["Receiver OApp B"] ENDPOINT_B["LayerZero
Endpoint"] end subgraph "Chain C" RECEIVER_C["Receiver OApp C"] ENDPOINT_C["LayerZero
Endpoint"] end subgraph "Chain D" TARGET_D["Target Contract"] end SENDER --> ENDPOINT_A ENDPOINT_A -->|"Channel 1: Push Messaging"| ENDPOINT_B ENDPOINT_A -->|"Channel 2: Push Messaging"| ENDPOINT_C ENDPOINT_A -.->|"Channel 3: Pull Messaging"| TARGET_D ENDPOINT_B --> RECEIVER_B ENDPOINT_C --> RECEIVER_C TARGET_D -.->|"Return data"| ENDPOINT_A ```
> **Note**: Channel 3 shows the pull messaging pattern where data is queried directly from target contracts without involving the destination chain's LayerZero endpoint. We'll expand on this [lzRead](../concepts/applications/read-standard) functionality in future modules.
### Channel Identification In practice, each channel can be identified from a source chain using only the destination **Endpoint ID (EID)** and **receiver** contract address. The source EID is already known (stored in the local Endpoint) and the caller is the OApp making the request, simplifying pathway management: ```solidity wrap theme={null} // Standard EIDs for endpoint-to-endpoint channels uint32 constant ETHEREUM_EID = 30101; // Ethereum mainnet endpoint uint32 constant ARBITRUM_EID = 30110; // Arbitrum mainnet endpoint uint32 constant POLYGON_EID = 30109; // Polygon mainnet endpoint // Channel identification from source chain: // - Source EID: Known (local endpoint) // - Sender OApp: Known (msg.sender) // - Destination EID: Provided as argument // - Receiver OApp: Provided as argument // Example: From Ethereum, only need (30110, PeerOApp) to identify Ethereum → Arbitrum channel ``` **Custom Channel IDs**: For lzRead pull-messaging, the sender and receiver are the same OApp (request/response pattern). Since we need to distinguish read requests from standard push messages to the same contract, we use arbitrary channel IDs: ```solidity wrap theme={null} // Custom channel ID for lzRead workflows uint32 constant CUSTOM_READ_CHANNEL = 4294967295; // Arbitrary identifier // lzRead pattern: // - Sender OApp: The contract requesting data (e.g., on Ethereum) // - Receiver OApp: Same contract receiving the response (same Ethereum contract) // - Custom EID: Distinguishes this as a read request, not a push message // - Target: The contract being queried (e.g., on Arbitrum) ``` ### Channel-Specific Nonce Tracking Each channel maintains its own independent nonce sequence, enabling parallel messaging without ordering conflicts: **Benefits of Per-Channel Nonces**: * **Parallel messaging**: No ordering dependencies between different channels * **Independent scaling**: High-traffic channels don't block low-traffic channels * **Replay protection**: Each channel has its own sequence for security * **Channel isolation**: Message ordering only matters within the same communication pathway ```mermaid wrap theme={null} graph LR subgraph "Chain A" SENDER["Sender OApp"] ENDPOINT_A["LayerZero
Endpoint"] end subgraph "Chain B" RECEIVER_B["Receiver OApp B"] ENDPOINT_B["LayerZero
Endpoint"] end subgraph "Chain C" RECEIVER_C["Receiver OApp C"] ENDPOINT_C["LayerZero
Endpoint"] end subgraph "Chain D" TARGET_D["Target Contract"] end SENDER --> ENDPOINT_A ENDPOINT_A -->|"Channel 1: nonce 104,105,106..."| ENDPOINT_B ENDPOINT_A -->|"Channel 2: nonce 10,11,12..."| ENDPOINT_C ENDPOINT_A -.->|"Channel 3: nonce 1,2,3..."| TARGET_D ENDPOINT_B --> RECEIVER_B ENDPOINT_C --> RECEIVER_C TARGET_D -.->|"Return data"| ENDPOINT_A ``` ### Message Packet Generation Per Channel When applications send messages, the Endpoint wraps the raw message data in a standardized packet container with channel-specific metadata. Based on the [EndpointV2 implementation](https://github.com/LayerZero-Labs/LayerZero-v2/blob/4645b5795185a196713263311f76a497a3267dcc/packages/layerzero-v2/evm/protocol/contracts/EndpointV2.sol#L108C5-L144C6): ```solidity wrap theme={null} // Endpoint constructs packet with unique identifiers per channel Packet memory packet = Packet({ nonce: latestNonce, // Sequential message number per channel srcEid: eid, // Source endpoint ID (this chain) sender: _sender, // Sender contract address (the OApp) dstEid: _params.dstEid, // Destination endpoint ID receiver: _params.receiver, // Receiver contract address guid: GUID.generate(latestNonce, eid, _sender, _params.dstEid, _params.receiver), // Globally unique ID message: _params.message // Raw application data (bytes) }); ``` **Key Properties**: * **Nonce**: Sequential per channel for ordering and replay protection - each channel maintains its own nonce sequence * **GUID**: Globally unique identifier generated from channel components + nonce * **Channel identification**: `(sender, srcEid, dstEid, receiver)` uniquely identifies the communication pathway * **Message isolation**: Raw application data separated from protocol metadata ### Managing Channel Configuration The Endpoint provides the interface for managing channel configurations via modular **Message Libraries**: ```solidity wrap theme={null} struct SetConfigParam { uint32 eid; // Target endpoint ID uint32 configType; // Configuration type identifier bytes config; // Encoded configuration data } interface IMessageLibManager { // Library selection per pathway function setSendLibrary(address _oapp, uint32 _eid, address _newLib) external; function setReceiveLibrary(address _oapp, uint32 _eid, address _newLib, uint256 _gracePeriod) external; // Library configuration per pathway function setConfig(address _oapp, address _lib, SetConfigParam[] calldata _params) external; function getConfig(address _oapp, address _lib, uint32 _eid, uint32 _configType) external view returns (bytes memory config); } ``` Each OApp can configure libraries to define the messaging behavior for its specific channels. These libraries determine how messages are verified, executed, and delivered for that OApp's communication needs. ## 3. Configurable Protocol Libraries (Message Libraries) Message Libraries are onchain rulesets that handle how messages are sent off-chain and arrive onchain between Endpoints. They define the complete workflow: message processing, verification requirements, and delivery coordination while maintaining the universal Endpoint interface. ```solidity wrap theme={null} // Excerpt of IMessageLib, ISendLib - core library functions interface IMessageLib { // Configure verification and execution rules per pathway function setConfig(address _oapp, SetConfigParam[] calldata _config) external; // Send path: Transform endpoint calls into off-chain instructions function send( Packet calldata _packet, bytes calldata _options ) external returns (MessagingFee memory, bytes memory); // Receive path: Verifier networks call this to mark messages verified function verify( Origin calldata _origin, address _receiver, bytes32 _payloadHash ) external; // Check compatibility and versioning function version() external view returns (uint64 major, uint8 minor, uint8 endpointVersion); } struct Packet { uint64 nonce; // Message sequence number uint32 srcEid; // Source endpoint ID address sender; // Sender contract address uint32 dstEid; // Destination endpoint ID bytes32 receiver; // Receiver address (bytes32 for crosschain) bytes32 guid; // Globally unique identifier bytes message; // Message business logic } ``` ### Message Library Types & Workflows Message Libraries can define different workflows and have specialized roles, such as **Sending**, **Receiving**, or **Reading** crosschain state: #### Push Messaging Workflow (Send/Receive ULN) ```mermaid wrap theme={null} graph LR ENDPOINT_A["Chain A
Endpoint"] --> SEND_ULN["SendUln302
Send Library"] SEND_ULN --> CHANNEL["Crosschain
Channel"] CHANNEL --> RECEIVE_ULN["ReceiveUln302
Receive Library"] RECEIVE_ULN --> ENDPOINT_B["Chain B
Endpoint"] ``` #### Pull Messaging Workflow (Request/Response lzRead) ```mermaid wrap theme={null} graph LR ENDPOINT_A["Chain A
Endpoint"] --> READ_LIB["ReadLib1002"
Read Library] READ_LIB --> CHANNEL["Crosschain
Channel"] CHANNEL --> TARGET["Chain B
Target Contract"] TARGET --> CHANNEL CHANNEL --> READ_LIB READ_LIB --> ENDPOINT_A ```
Different libraries enable OApps to handle different channel types with distinct behaviour, security, and delivery characteristics. Each channel represents a unique communication pathway between specific OApps with configurable message processing rules. LayerZero Labs may deploy new libraries with different workflows, features, or versions to enable developers to opt into new changes per channel. ### Channel-Specific Library Configuration Each channel gets unique settings through a two-step process from the Endpoint: 1. **Library Selection**: Use `setSendLibrary`/`setReceiveLibrary` to assign specific libraries per channel 2. **Library Configuration**: Use `setConfig` to customize each library's settings per channel This creates truly independent channel configurations - the same library type (e.g., SendUln302) can have completely different verification, finality, and execution settings for different channels. Each unique combination creates an independent channel setting with its own library configuration: ```mermaid wrap theme={null} graph LR subgraph "Chain A" SENDER["Sender OApp"] ENDPOINT_A["LayerZero
Endpoint"] LIB_A1["SendUln302
Library"] LIB_A2["SendUln302
Library"] LIB_A3["ReadLib1002
Library"] end subgraph "Chain B" RECEIVER_B["Receiver OApp B"] ENDPOINT_B["LayerZero
Endpoint"] LIB_B["ReceiveUln302
Library"] end subgraph "Chain C" RECEIVER_C["Receiver OApp C"] ENDPOINT_C["LayerZero
Endpoint"] LIB_C["ReceiveUln302
Library"] end subgraph "Chain D" TARGET_D["Target Contract"] end SENDER --> ENDPOINT_A ENDPOINT_A --> LIB_A1 ENDPOINT_A --> LIB_A2 ENDPOINT_A --> LIB_A3 LIB_A1 -->|"Channel 1: Push Messaging"| LIB_B LIB_A2 -->|"Channel 2: Push Messaging"| LIB_C LIB_A3 -.->|"Channel 3: Pull Messaging"| TARGET_D LIB_B --> ENDPOINT_B LIB_C --> ENDPOINT_C ENDPOINT_B --> RECEIVER_B ENDPOINT_C --> RECEIVER_C TARGET_D -.->|"Return data"| LIB_A3 LIB_A3 --> ENDPOINT_A ``` ### What Can Be Configured Per Channel Message Libraries enable three main types of configuration for each channel: * **Finality**: How many block confirmations are required before verification begins * **Verification**: Which verifier networks must verify messages and in what combinations * **Execution**: Which execution services deliver messages and with what parameters ### X-of-Y-of-N Verification Coordination LayerZero libraries today use an **X-of-Y-of-N** configuration pattern for verification, where: * **X**: Required verifier networks that MUST always verify (non-fungible) * **Y**: Total verifications needed (required + threshold of optional) * **N**: Total pool of available verifier networks **Example**: **2-of-4-of-6** configuration * **2 specific networks** must always verify (X = required) * **4 total verifications** needed (Y = required + optional threshold) * **6 networks available** in the pool (N = total options) * **Any 2 of the remaining 4** optional networks can provide the additional verifications Each "verifier network" is an independent verification system that implements one of the verification approaches from [Module 1](./interoperability-foundations#verifier-networks) (ZK proofs, committee consensus, light clients, etc.). This creates a **quorum of quorums** - each network must reach its own internal verification criteria before contributing to the overall X-of-Y-of-N requirement. ### Example: 2-of-2-of-2 Verifier Networks ```mermaid wrap theme={null} graph LR subgraph "Chain A" SENDER["Sender OApp"] ENDPOINT_A["LayerZero
Endpoint"] LIB_A["SendLib"] end subgraph "Verification Layer" VN1["Verifier Network 1
ZK Proofs"] VN2["Verifier Network 2
Committee"] end subgraph "Chain B" LIB_B["ReceiveLib"] ENDPOINT_B["LayerZero
Endpoint"] RECEIVER_B["Receiver OApp"] end subgraph "Config" CONFIG["2-of-2-of-2
Required: Verifier Network 1, Verifier Network 2
Optional: None
Simple Configuration"] end SENDER --> ENDPOINT_A ENDPOINT_A --> LIB_A LIB_A --> VN1 LIB_A --> VN2 VN1 --> LIB_B VN2 --> LIB_B LIB_B --> ENDPOINT_B ENDPOINT_B --> RECEIVER_B CONFIG -.-> LIB_A CONFIG -.-> LIB_B ```
This simple **2-of-2-of-2** configuration shows two required verifier networks with no optional networks. #### 1-of-2-of-3 Verifier Networks ```mermaid wrap theme={null} graph LR subgraph "Chain A" SENDER["Sender OApp"] ENDPOINT_A["LayerZero
Endpoint"] LIB_A["SendLib"] end subgraph "Verification Layer" VN1["Verifier Network 1
ZK Proofs"] VN2["Verifier Network 2
Committee A"] VN3["Verifier Network 3
Committee B"] end subgraph "Chain B" LIB_B["ReceiveLib"] ENDPOINT_B["LayerZero
Endpoint"] RECEIVER_B["Receiver OApp"] end subgraph "Config" CONFIG["1-of-2-of-3
Required: Network 1
Optional: Choose 1 of {Network 2, Network 3}
Flexible Configuration"] end SENDER --> ENDPOINT_A ENDPOINT_A --> LIB_A LIB_A --> VN1 LIB_A --> VN2 LIB_A --> VN3 VN1 --> LIB_B VN2 --> LIB_B VN3 --> LIB_B LIB_B --> ENDPOINT_B ENDPOINT_B --> RECEIVER_B CONFIG -.-> LIB_A CONFIG -.-> LIB_B ```
This **1-of-2-of-3** configuration shows 1 required verifier network (solid border) and 2 optional verifier networks (dashed borders). Only 1 of the 2 optional networks needs to verify, providing flexibility while maintaining the required verification. #### Configuration Per Channel Message Libraries enable each channel to specify different X-of-Y-of-N configurations based on security requirements: ```mermaid wrap theme={null} graph LR subgraph "Chain A" SENDER["Sender OApp"] ENDPOINT_A["LayerZero
Endpoint"] LIB_A1["SendLib
3-of-3-of-3"] LIB_A2["SendLib
1-of-2-of-5"] LIB_A3["ReadLib
2-of-4-of-7"] end subgraph "Chain B" RECEIVER_B["Receiver OApp B"] ENDPOINT_B["LayerZero
Endpoint"] LIB_B["ReceiveLib
3-of-3-of-3"] end subgraph "Chain C" RECEIVER_C["Receiver OApp C"] ENDPOINT_C["LayerZero
Endpoint"] LIB_C["ReceiveLib
1-of-2-of-5"] end subgraph "Chain D" TARGET_D["Target Contract"] end SENDER --> ENDPOINT_A ENDPOINT_A --> LIB_A1 ENDPOINT_A --> LIB_A2 ENDPOINT_A --> LIB_A3 LIB_A1 -->|"Channel 1: All Required"| LIB_B LIB_A2 -->|"Channel 2: 1 Required, 1 of 4 Optional"| LIB_C LIB_A3 -.->|"Channel 3: 2 Required, 2 of 5 Optional"| TARGET_D LIB_B --> ENDPOINT_B LIB_C --> ENDPOINT_C ENDPOINT_B --> RECEIVER_B ENDPOINT_C --> RECEIVER_C TARGET_D -.->|"Return data"| LIB_A3 LIB_A3 --> ENDPOINT_A ```
Each channel's Message Library enforces these X-of-Y-of-N requirements onchain, ensuring that the specified verification pattern is met before message execution. ### Configuration Structures Message Libraries define specific configuration structures for the three configurable aspects: **Note**: The configuration structures reference "DVNs" (Decentralized Verifier Networks) and "Executors". You don't need to understand their implementation details yet - just know that DVNs help determine which verifier networks to select, and Executors handle message delivery. Both will be explained in detail in [Module 4: Verification & Execution Services](./verification-execution-services). #### Push Messaging Configuration (ULN) ```solidity wrap theme={null} struct UlnConfig { uint64 confirmations; // Block confirmations required for finality uint8 requiredDVNCount; // Number of required DVNs (0 = DEFAULT, NIL_DVN_COUNT = NONE) uint8 optionalDVNCount; // Number of optional DVNs available uint8 optionalDVNThreshold; // How many optional DVNs needed: (0, optionalDVNCount] address[] requiredDVNs; // Required DVN addresses (sorted, no duplicates) address[] optionalDVNs; // Optional DVN addresses (sorted, no duplicates) } struct ExecutorConfig { uint32 maxMessageSize; // Maximum message size in bytes address executor; // Executor contract address } ``` #### Pull Messaging Configuration (lzRead) ```solidity wrap theme={null} struct ReadLibConfig { address executor; // Executor for read operations uint8 requiredDVNCount; // Number of required DVNs uint8 optionalDVNCount; // Number of optional DVNs available uint8 optionalDVNThreshold; // How many optional DVNs needed address[] requiredDVNs; // Required DVN addresses (sorted, no duplicates) address[] optionalDVNs; // Optional DVN addresses (sorted, no duplicates) } ``` **Key Insight**: Message Libraries are the onchain glue that makes verifier networks and delivery services pluggable while maintaining security guarantees. Different libraries can implement completely different messaging paradigms and verification requirements. ## Crosschain Services (Workers) Message Libraries coordinate two types of off-chain worker services that handle verification and execution: ### 4. DVNs (Decentralized Verifier Networks) **DVNs** are LayerZero's official term for the "verifier networks" discussed in previous sections. Each DVN is an independent verification service that implements one of the verification approaches from [Module 1](./interoperability-foundations) (ZK proofs, committee consensus, light clients, middlechains, etc.). **Key Properties**: * **Independent operation**: Each DVN runs its own verification logic * **Configurable per pathway**: Different DVN sets per channel * **X-of-Y-of-N coordination**: Multiple DVNs work together via Message Library rules * **Verification diversity**: Different DVNs use different trust models and verification methods ### 5. Executors **Executors** are permissionless services that deliver verified messages to destination chains. Anyone can run an executor, making message delivery competitive and censorship-resistant. **Key Properties**: * **Permissionless**: Anyone can operate an executor * **Competitive**: Multiple executors compete on speed and cost * **Optional**: Applications can opt out and execute manually * **Separation from verification**: Executors deliver, DVNs verify ### Worker Service Architecture ```solidity wrap theme={null} // Message Libraries coordinate both worker types: // 1. DVNs verify messages according to X-of-Y-of-N rules // 2. Executors deliver verified messages to destination contracts // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; /// @dev should be implemented by the ReceiveUln302 contract and future ReceiveUln contracts on EndpointV2 interface IReceiveUlnE2 { /// @notice for each dvn to verify the payload /// @dev this function signature 0x0223536e function verify(bytes calldata _packetHeader, bytes32 _payloadHash, uint64 _confirmations) external; /// @notice verify the payload at endpoint, will check if all DVNs verified function commitVerification(bytes calldata _packetHeader, bytes32 _payloadHash) external; } interface ILayerZeroEndpointV2 { function lzReceive( Origin calldata _origin, address _receiver, bytes32 _guid, bytes calldata _message, bytes calldata _extraData ) external payable; } ``` **Coordination**: Message Libraries define the rules, DVNs verify according to X-of-Y-of-N configurations, and Executors deliver once verification requirements are met. This separation enables independent scaling and competitive markets for both verification and execution services. For detailed implementation, configuration examples, and provider information, see [Module 4: Verification & Execution Services](./verification-execution-services). ## Exit Criteria Before proceeding to Module 4, you should be able to: 1. Describe the four layers (application, endpoint, libraries, workers) 2. Explain what DVNs do and how to configure them 3. Show how to set different verification per route ## See Also * Module 2: [Verification & Interface Coupling](./interface-coupling-problems) - The problem LayerZero solves * Module 5: [Application Design Patterns](./application-design-patterns) - Building on the interface * [Security Stack & DVNs](./modular-security/security-stack-dvns) - Deep dive on DVN configuration * [Message Libraries](./protocol/message-library) - Technical library details # Message Options Source: https://docs.layerzero.network/v2/concepts/message-options In the LayerZero protocol, message options are a way for applications to describe how they want their messages to be handled by off-chain infrastructure.... In the LayerZero protocol, **message options** are a way for applications to describe how they want their messages to be handled by off-chain infrastructure. These options are passed along with every message sent through LayerZero and are formatted as serialized `bytes`; a universal language that both the protocol and workers (like [DVNs](./modular-security/security-stack-dvns) and [Executors](./permissionless-execution/executors)) can understand. Each option acts like an instruction or a setting for a specific worker. For example, you might request that a certain amount of gas / compute units are allocated to execute your message on the destination chain, or that some native tokens be delivered along with the message. **Options are how applications communicate verification and execution preferences to the off-chain workers that carry out crosschain messages.** ## How Does LayerZero Route Options? When an application sends a message through LayerZero, it includes a field called `options`. This field is a compact, structured byte array that can contain multiple worker-specific instructions. LayerZero doesn’t interpret these options directly; instead, it forwards them to the appropriate service providers (called **workers**) that know how to read and act on the instructions. The workers typically fall into two categories: * **Decentralized Verifier Networks (DVNs)**: These provide verification to ensure the message is valid and has not been tampered with. * **Executors**: These are responsible for delivering and executing the message on the destination chain. The LayerZero messaging library understands how to break apart the `options` and route them to the correct workers. Since applications can configure message libraries, this design is modular, as new types of workers and options can be added over time without changing the core protocol. See the [OptionsBuilder library and SDK](../tools/sdks/options) to learn more about the specific encoding of options. ## Enforcing Options Some applications may require strict guarantees on how their messages are handled. Without this enforcement, users could accidentally (or maliciously) send messages that fail to execute, leading to a poor user experience or even stuck tokens. To prevent this, applications can enforce options. Enforcement means the application itself verifies and guarantees that a specific set of options is always present and correctly formatted before the message is allowed to be sent. Enforced options helps by: * Preventing underfunded executions that would otherwise fail on the destination chain. * Protecting users who omit critical options for a specific application use case. * Providing a consistent baseline experience regardless of the sender’s intent. This concept is especially important in applications like token bridges, composable smart contracts, or stateful protocols where execution must be predictable and reliable. Enforcing options means your application checks that users provide the correct `options` when calling the Endpoint's `send()` method. However, this does **NOT** guarantee that the specified instructions (e.g., gas limits or native drops) will be executed as intended by the worker or respected by permissionless callers on the destination chain. If your application requires strict guarantees, such as an exact gas amount or mandatory native gas drops, you must also validate those conditions **onchain** at the destination, or use a worker you trust. See the [**Integration Checklist**](../tools/integration-checklist#enforce-msgvalue-in-_lzreceive-and-lzcompose) for guidance on how to enforce execution requirements inside your `_lzReceive()` or `lzCompose()` logic. ## Extra Options While enforced options protect the base behavior of an application, users often have additional use cases that require more flexibility. To support this, LayerZero applications can also allow users to supply extra options. These are user-defined additions to the enforced baseline, offering more granular control over the message’s behavior on the destination chain. ### Why would a user want to add extra options? Take the example of an **Omnichain Token (OFT)** that supports **Omnichain Composability**; allowing the token to trigger additional logic after being received. This logic might involve calling another contract, performing swaps, or interacting with a dApp on the destination chain. In this case, the user might want to pay for: * A required amount of **gas** to ensure `lzReceive()` succeeds (enforced by the app). * Extra gas to support additional post-processing via `lzCompose()` (added by the user). By adding these extra options, users pay to extend the functionality without modifying the underlying application logic. ### Another example: Token + Native Gas Drop Suppose a user is bridging USDT0 (an OFT) to a new chain and wants to start interacting with dApps right away. Normally, they'd receive the token, but they wouldn’t have any native gas on the destination chain to pay for further transactions. With extra options, the user can: * Ensure `lzReceive()` executes successfully to receive the USDT0. * Add a **native token drop** option, funding their wallet with native gas on arrival. From the user's perspective, they complete a single crosschain action and arrive on the new chain with both: * The token they sent (USDT0) * Enough native gas to immediately start interacting This separation of concerns makes the system both secure by default and flexible by design; a core benefit of LayerZero's modular architecture. ## Why Do Options Matter? When sending a crosschain message, the source chain has no direct knowledge of the destination chain’s state: things like how much gas is needed, what the native currency is, or how the contract should be called. **Options solve this by letting the sender provide detailed instructions about how the message should be processed once it arrives.** Some common examples include: * **Execution Gas**: Telling the Executor how much gas or native token the destination contract will need during `lzReceive()`. * **Composer Gas**: Adding gas or native tokens for the composer contract when calling calling `lzCompose()`. * **Native Token Drops**: Sending native tokens (like ETH or APT) separately from the message. These instructions are interpreted by the off-chain workers, so that the message is handled as expected. ## Key Takeaways * `options` are serialized instructions that help off-chain workers understand how to process a message. * Each type of worker (DVN, Executor, etc.) looks for specific options relevant to their task. * Applications can enforce options to require correct behavior on source. * Users can extend options for extra functionality on destination. * The LayerZero protocol’s modular design means it can support new worker types without breaking existing behavior. # Message Ordering Source: https://docs.layerzero.network/v2/concepts/message-ordering LayerZero offers both unordered delivery and ordered delivery, providing developers with the flexibility to choose the most appropriate transaction... LayerZero offers both **unordered delivery** and **ordered delivery**, providing developers with the flexibility to choose the most appropriate transaction ordering mechanism based on the specific requirements of their application. ## Unordered Delivery By default, the LayerZero protocol uses **unordered delivery**, where transactions can be executed out of order if all transactions prior have been verified. If transactions `1` and `2` have not been verified, then transaction `3` cannot be executed until the previous nonces have been verified. Once nonces `1`, `2`, `3` have been verified: * If nonce `2` failed to execute (due to some gas or user logic related issue), nonce `3` can still proceed and execute. Diagram showing unordered (lazy) nonce enforcement: even if nonce 2 fails to execute, nonce 3 can still proceed after verification, demonstrating flexible out-of-order execution Diagram showing unordered (lazy) nonce enforcement: even if nonce 2 fails to execute, nonce 3 can still proceed after verification, demonstrating flexible out-of-order execution This is particularly useful in scenarios where transactions are not critically dependent on the execution of previous transactions. ## Ordered Delivery Developers can configure the OApp contract to use **ordered delivery**. Diagram showing ordered (strict) nonce enforcement: packets with nonces 1, 2, 3 must be executed in exact sequential order for system consistency Diagram showing ordered (strict) nonce enforcement: packets with nonces 1, 2, 3 must be executed in exact sequential order for system consistency In this configuration, if you have a sequence of packets with nonces `1`, `2`, `3`, and so on, each packet must be executed in that exact, sequential order: * If nonce `2` fails for any reason, it will block all subsequent transactions with higher nonces from being executed until nonce `2` is resolved. Diagram showing strict nonce enforcement failure scenario: if nonce 2 fails, all subsequent transactions with higher nonces are blocked until nonce 2 is resolved Diagram showing strict nonce enforcement failure scenario: if nonce 2 fails, all subsequent transactions with higher nonces are blocked until nonce 2 is resolved Strict nonce enforcement can be important in scenarios where the order of transactions is critical to the integrity of the system, such as any multi-step process that needs to occur in a specific sequence to maintain consistency. In these cases, strict nonce enforcement can be used to provide consistency, fairness, and censorship-resistance to maintain system integrity. ## Enabling Ordered Delivery To implement strict nonce enforcement, you need to implement the following: * a mapping to track the maximum received nonce. * override `_acceptNonce` and `nextNonce`. * add `ExecutorOrderedExecutionOption` in `_options` when calling `_lzSend`. * a governance function to keep the nonce mapping between the protocol and application in sync when skipping nonces. If you do not pass an `ExecutorOrderedExecutionOption` in your `_lzSend` call, the Executor will attempt to execute the message despite your application-level nonce enforcement, leading to a message revert. Append to your [Message Options](../developers/evm/configuration/options) an `ExecutorOrderedExecutionOption` in your `_lzSend` call: ```solidity wrap theme={null} // appends "01000104", the ExecutorOrderedExecutionOption, to your options bytes array _options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0).addExecutorOrderedExecutionOption(); ``` ## Keeping Nonces In Sync When skipping nonces at the protocol level, such as calling `endpoint.skip`, your OApp's local mapping must be incremented as well. If the local `receivedNonce` mapping falls behind the protocol's stored nonce, subsequent messages will revert with an invalid nonce error. A governance helper could look like: ```solidity wrap theme={null} /** * @notice skips exactly the next‐in‐line message, and keeps our mapping in perfect sync * @param _srcEid the LayerZero source chain ID * @param _sender the address of the remote sender (packed as bytes32) * @param _nonce the nonce to skip — must equal nextNonce(_srcEid,_sender) */ function skipInboundNonce( uint32 _srcEid, bytes32 _sender, uint64 _nonce ) public onlyOwner { // 1) sanity‐check that you're skipping exactly the next message uint64 expected = nextNonce(); require(_nonce == expected, "OApp: invalid skip nonce"); // 2) fire the skip on the endpoint IMessagingChannel(address(endpoint)).skip( address(this), _srcEid, _sender, _nonce ); // 3) sync our mapping receivedNonce[_srcEid][_sender] = _nonce; } ``` Keeping these values aligned ensures `nextNonce` returns the correct value and prevents ordered messages from being blocked. Implement strict nonce enforcement via function override: ```solidity wrap theme={null} pragma solidity ^0.8.20; import { OApp } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; // Import OApp and other necessary contracts/interfaces /** * @title OmniChain Nonce Ordered Enforcement Example * @dev Implements nonce ordered enforcement for your OApp. */ contract MyNonceEnforcementExample is OApp { // Mapping to track the maximum received nonce for each source endpoint and sender mapping(uint32 eid => mapping(bytes32 sender => uint64 nonce)) private receivedNonce; /** * @dev Constructor to initialize the omnichain contract. * @param _endpoint Address of the LayerZero endpoint. * @param _owner Address of the contract owner. */ constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) {} /** * @dev Public function to get the next expected nonce for a given source endpoint and sender. * @param _srcEid Source endpoint ID. * @param _sender Sender's address in bytes32 format. * @return uint64 Next expected nonce. */ function nextNonce(uint32 _srcEid, bytes32 _sender) public view virtual override returns (uint64) { return receivedNonce[_srcEid][_sender] + 1; } /** * @dev Internal function to accept nonce from the specified source endpoint and sender. * @param _srcEid Source endpoint ID. * @param _sender Sender's address in bytes32 format. * @param _nonce The nonce to be accepted. */ function _acceptNonce(uint32 _srcEid, bytes32 _sender, uint64 _nonce) internal virtual override { receivedNonce[_srcEid][_sender] += 1; require(_nonce == receivedNonce[_srcEid][_sender], "OApp: invalid nonce"); } // @dev Override receive function to enforce strict nonce enforcement. function _lzReceive( Origin calldata _origin, bytes32 _guid, bytes calldata _message, address _executor, bytes calldata _extraData ) public payable virtual override { _acceptNonce(_origin.srcEid, _origin.sender, _origin.nonce); // your _lzReceive(...) logic continues here } } ``` # Production DVN Configuration Source: https://docs.layerzero.network/v2/concepts/modular-security/production-dvn-configuration Threat model and target DVN configurations for OApps preparing a mainnet deployment, including risk-tier guidance and provider diversity recommendations. This page is the canonical reference for choosing a production DVN configuration. The shorter [Integration Checklist](/v2/tools/integration-checklist) tells you *what* to do; this page explains *why*, and gives you the risk-tier guidance to choose between a 2-of-2, a 2-of-3, a 3-of-N, and so on. ## Threat model: what a DVN compromise means A DVN attests that a specific `payloadHash` was emitted by a specific source-chain message. The library on the destination chain accepts a message for delivery only if every required DVN has attested. A 1-of-1 configuration means a single operator can: * **Forge messages** that were never sent on the source chain. Any operation reachable by a forged message — minting tokens on the destination, releasing collateral from a lockbox, granting permissions — can be executed without consent of the OApp owner or the source chain. * **Suppress messages** that were sent on the source chain (by simply refusing to attest), creating a censorship channel. The compromise vector can be: * The DVN operator's signing keys. * The DVN operator's RPC infrastructure (an attacker who compromises every RPC the DVN reads can feed it false source-chain data, causing the DVN to attest forged events with its real keys). * The DVN operator's deployment artifacts (binaries, S3-hosted config, etc.). * A social engineering or insider attack on the operator. Multi-DVN configurations are not immune to all of these, but they require the attacker to compromise *every* required operator's pipeline simultaneously. Provider diversity (different operators, different infrastructure, different verification methods) raises that bar substantially. ## Choosing providers (the diversity rules) The point of multi-DVN is to reduce correlated failure. A configuration with two DVNs run by the same operator, on the same infrastructure, with the same verification method, has correlated failure — a single compromise still breaks the pathway. Diversity dimensions, in order of priority: 1. **Operator diversity.** Two different legal entities. Two different on-call teams. Two different deployment pipelines. 2. **Infrastructure diversity.** Different cloud providers. Different RPC providers. Different signing key custody. 3. **Verification method diversity.** Examples: a node-based committee DVN paired with a zk-proof DVN; a native-bridge DVN paired with an oracle DVN. An attacker who exploits a bug in one method does not automatically compromise the other. 4. **Geographic / jurisdictional diversity.** Less critical for most threat models, but relevant if your pathway is exposed to government-level censorship risk. Available providers are listed in [DVN Addresses](/v2/deployments/dvn-addresses). Not every provider is deployed on every chain; check coverage before committing to a target configuration. ## X-of-Y-of-N strategies [X-of-Y-of-N](/v2/concepts/protocol/message-security#configurable-channellevel-security-xofyofn) lets you mix mandatory and pluggable verifiers: * **X (required)** — DVNs that *must* attest every message. Increase X to require more independent witnesses. * **Y (threshold of optional)** — minimum number of optional DVN attestations. Increase Y to require redundancy beyond the required set. * **N (total optional)** — pool of optional DVNs available. Increase N to widen the pool. Common patterns: * **2-of-2 strict:** `X=2, optional=[]`. Both required DVNs must attest. Simplest. Single point of *liveness* failure for either DVN — if one DVN goes offline, all messages stall. * **2-of-3:** `X=2 with one optional, threshold 1` (where one of three operators is in the "optional" slot). Two named operators are required; the third operator can sub for one of the named two if needed. Better liveness. * **2-of-3 redundant:** `X=2, optional=[N=2, threshold=1]`. Two required + at least one of two optional. Tolerates one optional DVN failing. Recommended for medium-value pathways. * **3-of-3 + 1-of-2 optional:** Three required + one of two optional. Maximum security floor; widely used for critical assets. For OFTs and bridges with composable downstreams (e.g., LRTs that flow into restaking, LSTs that collateralize lending), favor the higher tiers. The downstream blast radius of a forged mint is not bounded to your own protocol. If the Config Checker reports **fewer than 2 effective DVNs** (`less-than-2-dvns`), a single DVN compromise can forge messages on this pathway. Resolve it by calling `setConfig` on **both** the send and receive sides to raise `requiredDVNCount` to at least 2 with independent, diverse `requiredDVNs` — or add optional DVNs with an `optionalDVNThreshold` that brings the effective count to 2 or more. Use the tiers above as your target. ## Confirmation depth `confirmations` is the number of source-chain blocks the DVN waits before attesting. Low confirmations are fast but allow reorg-vulnerable messages to be confirmed. High confirmations are slow but resilient to source-chain reorgs. Recommended floors (tune to the source chain's reorg profile): * Ethereum mainnet: 15 minimum, 32 (1 epoch) preferred for high-value pathways. * Optimistic L2s: typically 15–30; consult the L2's reorg history before going below 15. * ZK L2s: typically lower (1–5) is acceptable once finality is committed to L1, but check the specific rollup's finality model. * Solana: 32 minimum (consult the [Solana DVN config docs](/v2/developers/solana/configuration/dvn-executor-config)). * Move chains (Aptos, Sui, IOTA): consult the per-VM config docs. The CLI examples in this documentation use `[1, 1]` for fast iteration during development. **Replace with production values before mainnet.** ### Asymmetric Confirmations Pin `confirmations` explicitly on **both** sides of every pathway. If one side calls `setConfig` with an explicit value and the mirror leaves `confirmations: 0` (implicit), the implicit side inherits the chain's default — and that default can change without notice. The pathway can shift from a 32/32 stance to whatever LayerZero Labs publishes next, in either direction. Reliable delivery requires `sendConfirmations >= receiveConfirmations`. A drifting default can therefore convert a healthy pathway into either form of the same failure mode — the receive side demanding more confirmations than the send side waits for: * Default lowers the send side below the receiver's pinned floor → messages arrive but the receiver rejects them as not having enough confirmations (`receive-confirmations-higher`). * Default raises the receive side above the sender's pinned ceiling → the sender's attestations look fine on chain A, but chain B blocks them as under-confirmed (`receive-confirmations-higher`). **Do:** * Call `setConfig` for the ULN configuration on both the send and receive libraries for every pathway, setting `confirmations` to your chosen production floor. * Choose a value at or above the [recommended floor](#confirmation-depth) for the source chain on **both** sides. * Re-run the check below after any LayerZero default migration that touches confirmation depth. **Don't:** * Leave `confirmations: 0` on one side because the merged view (`getUlnConfig`) looks healthy — the merged view hides which side is on the default. * Assume two sides matching today implies they will match tomorrow if only one is explicit. Asymmetric confirmations are a latent failure: messages keep delivering until the default rotates. When it rotates, every in-flight message on that pathway can stall mid-channel until you re-`setConfig` the side that drifted. Prefer pinning both sides to the same value. #### How to check ```bash wrap theme={null} # `confirmations` is the first field of UlnConfig. SEND_CONF=$(cast call "$SEND_LIB_A" \ "getAppUlnConfig(address,uint32)((uint64,uint8,uint8,uint8,address[],address[]))" \ "$OAPP_A" "$EID_B" --rpc-url "$RPC_A" | awk -F'[(,)]' '{print $2}') RECV_CONF=$(cast call "$RECV_LIB_B" \ "getAppUlnConfig(address,uint32)((uint64,uint8,uint8,uint8,address[],address[]))" \ "$OAPP_B" "$EID_A" --rpc-url "$RPC_B" | awk -F'[(,)]' '{print $2}') echo "Send confirmations: $SEND_CONF (0 = using default)" echo "Receive confirmations: $RECV_CONF (0 = using default)" # If exactly one is 0, the config is asymmetric — pin both sides. ``` See [Self-Validation with `cast`](/v2/developers/evm/configuration/dvn-executor-config#self-validation-with-cast) for the shared `$SEND_LIB_A` / `$RECV_LIB_B` environment. ## Executor concentration DVNs support a multi-attestor security stack (X-of-Y-of-N); the Executor does not — there is exactly one Executor per pathway. For high-value pathways, evaluate: * Running your own Executor (see [Build Executors](/v2/workers/off-chain/build-executors)). * Using a different third-party Executor. * Operating without an Executor and using LayerZero Scan or a custom relayer to call `lzReceive` / `lzCompose` directly. See [Executors](/v2/concepts/permissionless-execution/executors) for the full discussion. ## Pre-launch checks Before mainnet, confirm: 1. **Resolved config matches intent.** Run `getConfig` (or the per-VM equivalent) for every pathway. Compare against your target configuration in this document. Do not rely on defaults — see [Default Config Checker](https://layerzeroscan.com/tools/defaults). 2. **DVNs are sorted in ascending address order** on every `requiredDVNs` and `optionalDVNs` array. The contract reverts on unsorted arrays. 3. **Send and receive sides match.** A common bug is updating one side and forgetting the other. Both directions of every pathway need symmetric configurations. 4. **No Dead DVN in your resolved config.** A Dead DVN means the message will never be deliverable; this is a configuration error, not a security feature. 5. **Confirmations are production-grade**, not the `[1, 1]` development value. 6. **Executor is set explicitly**, not inherited from defaults. 7. **You have a tested rollback procedure** for each pathway. The full set of pre-launch items is enumerated in the [Integration Checklist](/v2/tools/integration-checklist). ## See also * [Security Stack (DVNs)](./security-stack-dvns) — concept reference * [Migrating from a Single-DVN Configuration](/v2/get-started/migrating-from-single-dvn) — operational migration guide * [Integration Checklist](/v2/tools/integration-checklist) — pre-launch gate * [DVN Addresses](/v2/deployments/dvn-addresses) — available providers per chain * [LayerZero Bug Bounty Scope](/community/bug-bounty-support) — what's in/out of scope (note: OApp DVN configuration is the OApp owner's responsibility) # Security Stack (DVNs) Source: https://docs.layerzero.network/v2/concepts/modular-security/security-stack-dvns As mentioned in previous sections, every application built on top of the LayerZero protocol can configure a unique messaging. LayerZero enables secure... As mentioned in previous sections, every application built on top of the LayerZero protocol can configure a unique [messaging channel](../protocol/message-security). This stack of multiple DVNs allows each application to configure a unique security threshold for each source and destination, known as [X-of-Y-of-N](../protocol/message-security#configurable-channellevel-security-xofyofn). In this stack, each DVN independently verifies the `payloadHash` of each message to ensure integrity. Once the designated DVN threshold has been reached, the message nonce can be marked as verified and inserted into the destination Endpoint for execution. Diagram showing DVN verification flow: multiple DVNs independently verify the payloadHash of each message, and once the designated threshold is reached, the message can be committed to the destination Endpoint Diagram showing DVN verification flow: multiple DVNs independently verify the payloadHash of each message, and once the designated threshold is reached, the message can be committed to the destination Endpoint Each DVN applies its own verification method to check that the `payloadHash` is correct. Once the required DVNs and optionally a sufficient number of optional DVNs have confirmed the `payloadHash`, any authorized caller (for example, an [Executor](../permissionless-execution/executors)) can commit the message nonce into the destination [Endpoint’s](../protocol/layerzero-endpoint) messaging channel for execution. The following image and table describe how messages can be inserted into the Endpoint's messaging channel post-verification: Table diagram showing message nonce verification states: illustrating how required DVNs (DVNᴬ, DVNᴮ) and optional DVNs verify payloadHash before nonces can be committed to the Endpoint's messaging channel Table diagram showing message nonce verification states: illustrating how required DVNs (DVNᴬ, DVNᴮ) and optional DVNs verify payloadHash before nonces can be committed to the Endpoint's messaging channel | Message Nonce | Description | | :-----------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | The Security Stack has verified the `payloadHash` and the nonce has been committed to the Endpoint’s messaging channel. | | 2 | All configured DVNs have verified the `payloadHash`, but no caller has yet committed the nonce to the Endpoint’s messaging channel. | | 3 | Two required and one optional DVN have verified the `payloadHash`, meeting the security threshold, but the nonce has not yet been committed. | | 4 | Even though the optional DVN threshold is met, the Security Stack requires that every **required DVN** (e.g. `DVNᴬ`) must verify the `payloadHash` before the nonce can be committed. | | 5 | Only the required DVNs (e.g. `DVNᴬ`, `DVNᴮ`) have verified the `payloadHash`; none of the optional verifiers have submitted their proof. | | 6 | Both the required DVNs and the optional threshold have verified the `payloadHash`, but no caller has committed the nonce to the Endpoint’s messaging channel yet. | ## Verification Model Each DVN can use its own verification method to confirm that the `payloadHash` correctly represents the message contents. This design allows application owners to tailor their Security Stack based on the desired security level and cost–efficiency tradeoffs. For an extensive list of DVNs available for integration, see [DVN Addresses](../../deployments/dvn-addresses). ### DVN Adapters **DVN Adapters** enable the integration of third-party generic message passing networks, such as native asset bridges, middlechains, or other specialized verification systems. With DVN Adapters, applications can incorporate diverse security models into their Security Stack, broadening the spectrum of available configurations while still ensuring a consistent verification interface via the `payloadHash`. Diagram showing DVN Adapters: enabling integration of third-party verification systems like native bridges, middlechains, or specialized networks into the Security Stack while maintaining consistent payloadHash verification Diagram showing DVN Adapters: enabling integration of third-party verification systems like native bridges, middlechains, or specialized networks into the Security Stack while maintaining consistent payloadHash verification Since “DVN” broadly describes any verification mechanism that securely delivers a message’s `payloadHash` to the destination [Message Library](../protocol/message-send-library), application owners have the flexibility to integrate with virtually any infrastructure that meets their security requirements. ## Configuring the Security Stack Every LayerZero Endpoint can be used to send and receive messages. Because of that, **each Endpoint has a separate Send and Receive Configuration**, which an OApp can configure per remote Endpoint (i.e., the messaging channel, sending to that remote chain, receiving from that remote chain). For a configuration to be considered valid, **the Send Library configurations on Chain A must match the Receive Library configurations on Chain B.** ## Default Configuration For each new channel, LayerZero provides a placeholder configutation known as the **default**. If you provide no configuration settings, the protocol will fallback to the default configuration. This default configuration can vary per channel, changing the placeholder block confirmations, the [X‑of‑Y‑of‑N](../glossary#x-of-y-of-n) thresholds for verification, the Executor, and the message libraries. A default pathway configuration will typically have one of the following preset Security Stack configurations within `SendULN302` and `ReceiveUlN302`: | | Security Stack | Executor | | ------------------------------ | ------------------------------------------------------------------- | -------------- | | **Default Send and Receive A** | requiredDVNs: \[ Google Cloud, LayerZero Labs ] | LayerZero Labs | | **Default Send and Receive B** | requiredDVNs: \[ Polyhedra, LayerZero Labs ] | LayerZero Labs | | **Default Send and Receive C** | requiredDVNs: \[ [Dead DVN](../glossary#dead-dvn), LayerZero Labs ] | LayerZero Labs | | **Default Send and Receive D** | requiredDVNs: \[ [Dead DVN](../glossary#dead-dvn) ] (count: 1) | LayerZero Labs | You can view all of the current default pathway configurations on [LayerZero Scan's Default Configs by Chain](https://layerzeroscan.com/tools/defaults).
Defaults A, B, and C list **LayerZero Labs** as a required DVN, and every default uses **LayerZero Labs** as the Executor. A single operator controls both verification and execution on every pathway whose default has a working DVN. Production deployments should explicitly configure their security stack with at least one required DVN that is not operated by LayerZero Labs, and consider the Executor accordingly. See the [Integration Checklist](../../tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway). **Some chains have only one DVN provider currently deployed.** On those chains, an OApp's options are: 1. Run your own DVN (see [Build a DVN](../../workers/off-chain/build-dvns)). 2. Wait until a third-party provider deploys (track on [DVN Addresses](../../deployments/dvn-addresses)). 3. Defer the chain until multi-DVN coverage exists. You should not assume a second DVN will appear in time for a launch. Confirm available DVNs on every chain in your mesh via the [Default Config Checker](https://layerzeroscan.com/tools/defaults) before committing. What is a **[Dead DVN](../glossary#dead-dvn)**? Since LayerZero allows for anyone to permissionlessly run DVNs, the network may occassionally add new chain Endpoints before the default providers (Google Cloud or Polyhedra) support every possible pathway to and from that chain. A default configuration with a **Dead DVN** will require you to either configure an available DVN provider for that Send or Receive pathway, or run your own DVN if no other security providers exist, before messages can safely be delivered to and from that chain. Some pathways currently use **Default D** above — a single Dead DVN with no co-DVN. On these pathways, the network default is fully non-functional: no message can be verified until the OApp explicitly configures its own DVNs. Verify the current state of any pathway you depend on via the [Default Config Checker](https://layerzeroscan.com/tools/defaults). You should always **set your DVN configuration** explicitly. Defaults are placeholder configurations — they may be Dead DVNs that prevent message delivery, may include only a single DVN, and may change without notice. ## Further Reading To query and set your application's configuration, you can review these VM-specific guides: * [EVM DVN and Executor Configuration](../../developers/evm/configuration/dvn-executor-config) * [Solana DVN and Executor Configuration](../../developers/solana/configuration/dvn-executor-config) * [Aptos DVN and Executor Configuration](../../developers/aptos-move/configuration/dvn-executor-config) # Executors Source: https://docs.layerzero.network/v2/concepts/permissionless-execution/executors Executors provide Execution as a Service for omnichain messages, automatically delivering and executing calls on the destination chain according to... Executors provide **Execution as a Service** for omnichain messages, automatically delivering and executing calls on the destination chain according to specific resource settings provided by your OApp directly or via call parameters. Automatic execution abstract away the complexity of managing gas tokens on different networks and invoking contract methods manually, enabling a more seamless crosschain experience. ## What "Execution" Means In the LayerZero protocol, **execution** refers to the invocation of the [LayerZero Endpoint](../protocol/layerzero-endpoint) methods on the destination chain after a message has been verified: 1. **`lzReceive(...)`**: Delivers a verified message to the destination OApp, triggering its logic. 2. **`lzCompose(...)`**: Delivers a composed message (e.g., nested calls) after the initial receive logic has triggered. Both methods are **permissionless** on the endpoint contract, meaning anyone can call them once the message has been marked as verified. ## Executors: Execution as a Service While you could manually call `lzReceive(...)` or `lzCompose(... )` and pay gas on the destination chain directly, Executors automate this process: * **Quote in Source Token**: Executors accept payment in the source chain's native token and calculate the cost to deliver the destination chain's gas token based on the instructions provided and a pricefeed formula. * **Automatic Delivery**: After verification, the Executor invokes the appropriate endpoint method (`lzReceive(...)` or `lzCompose(...)`) with the specified resources and message. * **Native Token Supplier**: Executors are responsible for sourcing the native gas token on the destination chain, making them a resource for users needing to convert chain-specific resources. * **Fee for Service**: Executors charge a fee for relaying and executing messages. ### Permissionless Functions Because the endpoint methods are open, your application remains **decentralized and trust-minimized**, as any party can run an Executor or call the endpoint directly. ## Message Options Use **Message Options** to pass execution instructions along with your payload. Available options include: * [`lzReceiveOption`](../../developers/evm/configuration/options#lzreceive-option): Specify `gas` and `msg.value` when calling `lzReceive(...)`. * [`lzComposeOption`](../../developers/evm/configuration/options#lzcompose-option): Specify `gas` and `msg.value` when calling `lzCompose(...)`. * [`lzNativeDropOption`](../../developers/evm/configuration/options#lznativedrop-option): Drop a specified `amount` of native tokens to a `receiver` on the destination. * [`lzOrderedExecutionOption`](../../developers/evm/configuration/options#orderedexecution-option): Enforce nonce-ordered execution of messages. These options let you fine-tune gas usage and value transfers for each message type. More information can be found under [Message Options](../message-options). ## Default vs. Custom Executors Choose the executor strategy that fits your application: 1. **Default Executor**: Use the out-of-the-box implementation maintained by LayerZero Labs. 2. **Custom Executor**: Select from third-party Executors or deploy your own variant. 3. **Build Your Own**: Follow [Build Executors](../../workers/off-chain/build-executors) to implement a bespoke message Executor. 4. **No Executor**: Opt out of automated execution entirely; users can manually call `lzReceive(...)` or `lzCompose(...)` via [LayerZero Scan](../../developers/evm/tooling/layerzeroscan) or a block explorer. See [Executor Configuration](../../developers/evm/configuration/dvn-executor-config#setting-custom-send-config-dvn-executor) for details on wiring up a non-default Executor in your OApp. ## Executor concentration: a single point of failure for liveness Unlike DVNs (which support multi-operator X-of-Y-of-N configurations), an OApp configures **exactly one Executor per pathway**. Today, every default Executor across every pathway in every preset network configuration is operated by LayerZero Labs. This is a structural concentration. It is **not** equivalent to the DVN concentration discussed in [Security Stack (DVNs)](../modular-security/security-stack-dvns) — a compromised Executor cannot forge messages, because verification is independent. But a degraded or compromised Executor can: * **Delay or fail message delivery** on every OApp using it as the default. * **Censor specific messages** by selectively refusing to call `lzReceive` / `lzCompose`. * **Mis-account gas / value delivery**, breaking application invariants that depend on `msg.value` or `gasleft()` at the destination. For high-value pathways and pathways with strict liveness requirements, evaluate the alternatives in the list above. The available options: * **Run your own Executor** (option 3 above). Strongest liveness guarantee; highest operational overhead. * **Use a third-party Executor** (option 2). Reduces LayerZero Labs concentration without requiring you to operate infrastructure. * **Run without an Executor** (option 4). Trade automation for trust-minimization. Combine with a self-operated relayer that calls `lzReceive` directly. The same considerations that apply to a single-DVN configuration — pre-launch monitoring, governance pause, value caps — also apply to a single-Executor configuration. # LayerZero Endpoint Source: https://docs.layerzero.network/v2/concepts/protocol/layerzero-endpoint The LayerZero Endpoint is the immutable, permissionless protocol entrypoint for sending and receiving omnichain. LayerZero enables secure crosschain messaging. The LayerZero Endpoint is the immutable, permissionless protocol entrypoint for sending and receiving omnichain messages. Every LayerZero message passes through the Endpoint. It not only ensures secure and exactly-once message processing, but also will be your home for managing messaging channels, configurations, and fees. Below is an overview of the five core modules that comprise the Endpoint and the role each plays: ## Endpoint Interface The core interface defines the essential data structures and key functions used for transmitting messages between blockchains. It establishes: | **Functionality** | **Description** | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | | **Messaging Parameters** | Defines the destination endpoint identifier, receiver address, message payload, and worker options. | | **Messaging Receipts** | Returns a unique global identifier (GUID) and a nonce with each send call to track messages. | | **Key Methods** | Implements the core methods `quote`, `send`, `verify`, and `lzReceive` that all applications and workers routinely use. | *This interface guarantees every message is uniquely identified, correctly routed, and has its fees and security checks properly handled.* ## Message Channel Management This module tracks and manages messages along each distinct communication pathway. | **Functionality** | **Description** | | -------------------------- | -------------------------------------------------------------------------------------------------------------------- | | **Nonce Tracking** | Maintains gapless, monotonically increasing nonces per sender, receiver, and chain to enforce exactly‑once delivery. | | **Payload Hash Recording** | Stores the verified hash of each message payload to ensure message integrity before execution. | | **State Management** | Manages transitions (delivered, skipped, or burned) to maintain the channel’s integrity. | *Together, these functions create a lossless communication pathway essential for reliable cross‑chain messaging.* ## Message Library Management This module enables applications (OApps) to tailor the security threshold, finality, executor, and more. | **Functionality** | **Description** | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Custom Library Selection** | Allows an application to choose a specific messaging library for different operations (e.g., [send](../applications/oapp-standard#generic-message-passing) versus [read](../applications/read-standard#how-omnichain-queries-lzread-work)); defaults to the standard library if not set. | | **Worker Configuration** | Configures off‑chain workers (e.g, DVNs [X-of-Y-of-N](../protocol/message-security#configurable-channellevel-security-xofyofn) and Executor address) and finality settings on a per‑channel basis. | This flexibility enables each application to customize its security and fee management settings rather than relying on a fixed validator set and standard. ## Send Context and Reentrancy Protection The Messaging Context module ensures: | **Functionality** | **Description** | | ----------------------- | -------------------------------------------------------------------------------------------------------------------- | | **Unique Send Context** | Tags each outbound message with a combination of the destination endpoint and sender address, preventing reentrancy. | | **Reentrancy Guard** | Implements a dedicated modifier to prevent overlapping message processing. | *These features maintain the integrity of the messaging process, ensuring that each message is processed in isolation.* ## Message Composition "Arbitrary runtime dispatch" refers to the ability of a virtual machine (like the EVM) to decide dynamically at runtime which function to call based on input data. Not every blockchain virtual machine supports this, which limits how dynamically contracts can interact. The Messaging Composer provides a standardized way to compose and send follow‑up messages within multistep cross‑chain workflows. | **Feature** | **Description** | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | **Standardized Composition** | Stores a composed message payload onchain, which can later be retrieved and passed to a callback via `lzCompose`. | | **Lossless, Exactly‑Once Delivery** | Inherits the same guarantees as the core messaging functions, ensuring that each composed message maintains integrity and finality. | | **Fault Isolation** | Decouples composed messages from primary transactions so that errors remain isolated, simplifying troubleshooting. | *This module enables advanced cross‑chain interactions without compromising security or finality.* ## Summary The LayerZero Endpoint is the single, immutable entry and exit point for cross‑chain messaging, built on five core modules: | **Module** | **Primary Role** | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | **Core Interface** | Defines foundational messaging structures and methods to ensure unique identification and proper routing. | | **Messaging Channel** | Tracks nonces and payload hashes between senders and receivers, enforcing exactly‑once, lossless delivery. | | **Message Library Manager** | Provides flexibility for applications to configure custom messaging libraries and worker settings. | | **Messaging Context** | Supplies execution context and reentrancy protection to safeguard message processing. | | **Messaging Composer** | Standardizes the composition and dispatch of follow‑up messages, enabling advanced cross‑chain workflows without compromising security. | Together, these modules guarantee that every message sent and received via LayerZero is processed securely, efficiently, and reliably; no matter which blockchain the message originates from or is delivered to. ### Endpoint Alt For blockchains where an ERC20 token serves as the native currency for fee payments (instead of native ETH/gas token), LayerZero deploys a specialized [Endpoint Alt](./layerzero-endpoint-alt) variant. See [LayerZero Endpoint Alt](./layerzero-endpoint-alt) for details on how fee payments differ on these chains. # LayerZero Endpoint Alt Source: https://docs.layerzero.network/v2/concepts/protocol/layerzero-endpoint-alt The LayerZero Endpoint Alt is a variant of the LayerZero Endpoint designed for chains where a fungible token standard (rather than the chain's native gas... The LayerZero Endpoint Alt is a variant of the [LayerZero Endpoint](./layerzero-endpoint) designed for chains where a fungible token standard (rather than the chain's native gas token) serves as the currency for LayerZero fee payments. While the standard Endpoint processes fees via the chain's native token, Endpoint Alt accepts fees through token transfers using the chain's fungible token standard. This enables LayerZero to support chains where the native gas token has no economic value or where a different token is used as the primary currency. ### EVM Implementation On EVM chains, the Endpoint Alt implementation (`EndpointV2Alt`) uses ERC20 tokens for fee payments instead of native ETH/gas tokens sent via `msg.value`. ## Key Differences from Standard Endpoint Endpoint Alt inherits all core functionality from the standard [LayerZero Endpoint](./layerzero-endpoint) - including message channel management, library configuration, reentrancy protection, and message composition - but overrides the fee payment mechanism. | Aspect | Standard Endpoint | Endpoint Alt | | ----------------------- | ------------------------ | ------------------------------------------- | | **Fee Payment** | Chain's native gas token | Fungible token standard (e.g., ERC20) | | **Fee Token Discovery** | Implicit (native token) | Query endpoint for configured token address | | **Prerequisites** | None | Must approve Endpoint to spend fee tokens | ## Fee Payment Flow The following diagrams illustrate the fee payment flow differences on EVM chains. ### Standard Endpoint Flow ```mermaid wrap theme={null} sequenceDiagram participant OApp participant Endpoint participant MessageLib as Message Library OApp->>Endpoint: send{value: fee}(params, refundAddress) Endpoint->>Endpoint: Calculate required fees Endpoint->>MessageLib: Transfer native token fee Endpoint->>OApp: Refund excess native token ``` ### Endpoint Alt Flow ```mermaid wrap theme={null} sequenceDiagram participant OApp participant Endpoint as Endpoint Alt participant MessageLib as Message Library OApp->>Endpoint: approve(endpoint, amount) OApp->>Endpoint: Transfer ERC20 tokens OApp->>Endpoint: send(params, refundAddress) Note right of OApp: No msg.value allowed Endpoint->>Endpoint: Calculate required fees Endpoint->>MessageLib: Transfer ERC20 fee Endpoint->>OApp: Refund excess ERC20 ``` ## Modified Functions (EVM) On EVM chains, `EndpointV2Alt` overrides the following functions from the base `EndpointV2` contract: | Function | Description | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`constructor`** | Accepts an additional `_altToken` parameter specifying the ERC20 fee token address, stored as an immutable variable for gas optimization. | | **`_payNative`** | Reverts if `msg.value > 0` (with `LZ_OnlyAltToken`), then delegates to `_payToken()` for ERC20 transfers instead of native token transfers. | | **`_suppliedNative`** | Returns `IERC20(nativeErc20).balanceOf(address(this))` instead of `msg.value`, checking the contract's ERC20 balance. | | **`setLzToken`** | Adds validation to prevent setting `lzToken` to the same address as `nativeErc20`, avoiding accounting conflicts between the two fee payment systems. | | **`nativeToken`** | Returns the configured `nativeErc20` address instead of `address(0)`, allowing callers to discover the required fee token. | ## Integration Notes When building on a chain that uses Endpoint Alt: | Consideration | Description | | -------------------------- | -------------------------------------------------------------------------------------------------------- | | **Discover the fee token** | Query the Endpoint to retrieve the required fee token address. | | **Approve before sending** | Your contract must authorize the Endpoint to spend fee tokens before sending messages. | | **Quote fees correctly** | The `quote()` function returns the required fee amount in the configured fee token, not native currency. | ### EVM-Specific Notes | Consideration | Description | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | **Call `nativeToken()`** | Returns the ERC20 address used for fees; returns `address(0)` on standard endpoints. | | **Never send msg.value** | Any transaction with `msg.value > 0` will revert with `LZ_OnlyAltToken`. | | **Use OFT Alt for tokens** | When deploying OFTs on Endpoint Alt chains, use the [OFT Alt](/v2/developers/evm/oft/quickstart#oft-alt) contract variants. | ## Checking Endpoint Type (EVM) To determine whether an EVM chain uses the standard Endpoint or Endpoint Alt: ```solidity wrap theme={null} address feeToken = ILayerZeroEndpointV2(endpoint).nativeToken(); if (feeToken == address(0)) { // Standard Endpoint: pay fees with native token (msg.value) } else { // Endpoint Alt: pay fees with ERC20 at feeToken address } ``` ## Summary Endpoint Alt extends the standard LayerZero Endpoint to support chains where a fungible token standard replaces the native gas token for fee payments. On EVM chains, this means using ERC20 tokens instead of native ETH/gas tokens. All other protocol guarantees - immutability, permissionless messaging, [channel security](./message-security), and exactly-once delivery - remain unchanged. ## Further Reading * [LayerZero Endpoint](./layerzero-endpoint) - Core Endpoint architecture and modules * [OFT Alt](/v2/developers/evm/oft/quickstart#oft-alt) - Building OFTs on chains with Endpoint Alt # Omnichain Mesh Network Source: https://docs.layerzero.network/v2/concepts/protocol/mesh-network Learn about Omnichain Mesh Network in LayerZero V2. Understand the architecture, core concepts, and how it enables omnichain interoperability. Essential info... LayerZero’s Omnichain Mesh is the idea that every application’s smart contract—deployed on its respective blockchain—forms part of a single, fully interconnected system. Rather than limiting an application to communicating only with a select group of chains, the protocol enables any deployed [LayerZero Endpoint](./layerzero-endpoint) (the contract interface on each chain) to interact directly with any other Endpoint across all supported blockchains. ## What Is the LayerZero Mesh? Diagram showing LayerZero's Omnichain Mesh: multiple blockchain endpoints connected in a fully interconnected network where each endpoint can communicate with any other endpoint Diagram showing LayerZero's Omnichain Mesh: multiple blockchain endpoints connected in a fully interconnected network where each endpoint can communicate with any other endpoint * **Points on the Mesh:**\ Every blockchain LayerZero supports has one canonical LayerZero Endpoint deployed per protocol version. This means that on each chain, there is a single, unique smart contract, the LayerZero Endpoint, that provides a consistent interface for sending and receiving messages for all applications. As a result, each Endpoint acts as a distinct “point” in the mesh, ensuring that all crosschain communication adheres to the same standards and is easily identifiable. * **Pathways on the Mesh:**\ When two smart contracts on different chains communicate, they create a pathway between their respective Endpoints. Think of a pathway as a direct communication [channel](../glossary#channel--lossless-channel) between one Endpoint (point A) and another (point B). * **A Fully Connected Network:**\ The mesh is “omnichain” because it allows every Endpoint to set up a communication pathway with any other Endpoint using a common interface. In other words, an application is not limited to interacting with only a subset of chains. Any Endpoint can reach out and communicate with any other Endpoint using consistent data structures and handling, ensuring seamless interoperability across the entire network. ## Omnichain Features * **Universal Network Semantics:**\ The network enforces uniform standards for message delivery regardless of the blockchain pair involved. This guarantees that data packets are reliably transferred and delivered exactly once, while preserving censorship resistance. * **Modular Security Model:**\ LayerZero enables configurable security tailored per application for each pathway: * [Decentralized Verifier Networks (DVNs)](../modular-security/security-stack-dvns) validate messages according to application–specific requirements. * [Configurable Block Confirmations](../../developers/evm/configuration/dvn-executor-config#send-config-type-executor) protect against chain reorganizations by waiting a specified number of blocks before verification. * The Endpoint’s immutable core ensures that essential security features—like protection against censorship, replay attacks, and unauthorized code changes—are consistently maintained across the entire network. * **Channel Security:**\ Each communication channel, defined by the source blockchain, source application, destination blockchain, and destination application, can be individually configured to match the security and cost–efficiency requirements of that particular connection between endpoint and applications. * **Chain Agnostic Applications:**\ With these universal standards in place, developers can build [Omnichain Applications (OApps)](../applications/oapp-standard) that seamlessly operate across all supported blockchains, making it easy to transfer data and value across different networks. In summary, the Omnichain Mesh Network in LayerZero is a fully connected system where every Endpoint on every supported blockchain can directly interact with any other. This design empowers developers to create applications with truly universal crosschain capabilities—ensuring seamless, secure, and reliable messaging regardless of the underlying blockchain. # Message Library Overview Source: https://docs.layerzero.network/v2/concepts/protocol/message-library Learn about Message Library Overview in LayerZero V2. Understand the architecture, core concepts, and how it enables omnichain interoperability. Architecture... The **Message Library** is a fundamental concept in the LayerZero protocol that encompasses how the protocol can both send and receive messages. These libraries are responsible for processing, encoding / decoding, and verifying messages as they traverse between blockchains. ## Why Do Message Libraries Exist? While specific implementations may vary to accommodate different use cases (e.g., push-based messaging versus pull-based queries), several common themes form the backbone of all Message Libraries. ### Modularity & Separation of Concerns Message Libraries are designed to abstract and isolate the core functions of crosschain messaging. By separating tasks (e.g., encoding / decoding packets, fee calculation and management, configuration enforcement) from higher-level application logic and the LayerZero Endpoint, each library can be independently developed, optimized, and updated. This modularity enables: * **Independent Optimization:** Specialized libraries (like the Ultra Light Node) can be created without affecting how other parts of the protocol operate. * **Easier Maintenance:** The well-defined boundaries between components result in a cleaner, more maintainable architecture. ### Immutable and Append-Only Design Once deployed, Message Libraries are immutable and act as append-only components. This means that: * **Predictability:** The behavior of a library remains consistent over time, ensuring that applications can rely on its functionality. * **Backward Compatibility:** New libraries can be added to the ecosystem without affecting existing applications. This allows the protocol to evolve; integrating innovations and optimizations, while preserving the performance and security of the deployed components. ### Customizability and Flexibility Each Message Library supports a range of configurations, which applications set via the LayerZero Endpoint. These configurations determine critical aspects of message processing: * **Send Libraries:** Custom configurations define how packets are encoded and how fees are computed for routing messages outbound from a source chain. * **Receive Libraries:** Configurations specify the required verification parameters that must be met before a message is accepted and routed inbound to the destination receiver. This flexibility allows the system to support various messaging paradigms, such as push-based messaging (e.g., **Ultra Light Node**) or pull-based queries (e.g., **Read Library**). ### Security and Integrity Security is embedded at every layer of the message lifecycle: * **Encoding Integrity:** On the send side, messages are wrapped in a standardized Packet that includes unique identifiers, nonces, and routing metadata to prevent replay attacks and misrouting. * **Rigorous Verification:** On the receive side, libraries perform stringent checks to ensure the message has not been tampered with. * **Configuration Enforcement:** Receive libraries enforce that only the preconfigured, authorized workers can validate and process the incoming message, adding an extra layer of security. ### Efficiency and Decoupling Efficiency is achieved by: * **Streamlined Processing:** Specialized libraries focus on only transmitting and processing the essential data needed for a specific messaging workflow, reducing overhead. * **Decoupled Logic:** By decoupling message processing from the Endpoint and application code, the protocol supports rapid processing and efficient scaling without compromising on security or flexibility. ## Benefits for Developers and Users * **Reliability:** Immutable, well-defined libraries ensure that crosschain messaging remains consistent and dependable. * **Security:** Robust verification and configuration enforcement guard against unauthorized access or tampering. * **Flexibility:** Developers can choose from different library implementations that best match their application's needs, with the assurance that new capabilities will be seamlessly added. * **Scalability:** The append-only nature of these libraries enables the protocol to integrate new innovations without disrupting existing deployments. In summary, the Message Library is a key building block in the LayerZero protocol that unifies the processes of message encoding, transmission, decoding, and verification. Its modular, immutable, and flexible design ensures that the protocol can adapt over time while delivering secure, efficient, and reliable crosschain communication. ## Further Reading * For details on how messages are processed on the sending side, see the [Message Send Library](./message-send-library) page. * For details on how inbound messages are decoded and verified on the receiving side, see the [Message Receive Library](./message-receive-library) page. # Message Properties Source: https://docs.layerzero.network/v2/concepts/protocol/message-properties LayerZero is purpose built for lightweight message passing across multiple blockchains. To accomplish this, the protocol provides authentic and guaranteed... LayerZero is purpose built for lightweight message passing across multiple blockchains. To accomplish this, the protocol provides authentic and guaranteed message delivery with a configurable level of trustlessness. ## Message State Messages are sent from the User Application (UA) at source `srcUA` to the UA at the destination `dstUA`. Once the message is received by the `dstUA`, the message is considered delivered (transitioning from `INFLIGHT` to either `SUCCESS` or `STORED`) | Message State | Cases | | :------------ | :------------------------------------------------------------------------ | | **INFLIGHT** | After a message is sent | | **SUCCESS** | A1: `dstUA` success OK()
A2: `dstUA` fails with uncaught exception | | **STORED** | B1: `dstUA` fails with uncaught error / exception | ```solidity wrap theme={null} // message handling at destination chain try ILayerZeroReceiver(_dstAddress).lzReceive{gas: _gasLimit}(_srcChainId, _srcAddress, _nonce, _payload) { // message state becomes SUCCESS } catch { // message state becomes STORED emit PayloadStored(_srcChainId, _srcAddress, _dstAddress, _payload); } ``` **Case A2: `dstUA`** is expected to store the message in their contract to be retried (LayerZero will not store any successfully delivered messages). dstUA is expected to monitor and retry STORED messages on behalf of its users. **Case B1: `dstUA`** is expected to gracefully handle all errors/exceptions when receiving a message, and any uncaught errors/exceptions (including out-of-gas) will cause the message to transition into STORED. A STORED message will block the delivery of any future message from srcUA to all dstUA on the same destination chain and can be retried until the message becomes SUCCESS. dstUA should implement a handler to transition the stored message from STORED to SUCCESS. If a bug in dstUA contract results in an unrecoverable error/exception, LayerZero provides a last-resort interface to force resume message delivery, only by the dstUA contract. ### Message Ordering LayerZero provides ordered delivery of messages from a given sender to a destination chain, i.e. `srcUA -> dstChain`. In other words, the message order nonce is shared by all `dstUA` on the same `dstChain`. That's why a `STORED` message blocks the message pathway from `srcUA` to all `dstUA` on the same destination chain. If it isn't necessary to preserve the sequential nonce property for a particular `dstUA` the sender must add the nonce into the payload and handle it end-to-end within the UA. UAs can implement a non-blocking pattern in their contract code. ### Extensibility #### Message Adapter Parameters LayerZero allows UAs to add arbitrary transaction params in the `send()` function, providing a high level of flexibility and opening up opportunities for a diverse set of 3rd party plugins This is implemented as an unreserved byte array parameter to the send() function, with UAs allowed to write any additional data necessary into that parameter. We recommend that UAs leave some degree of configurability for the extra parameters to allow for feature extensions. One great feature of `_adapterParams` is performing an Airdrop. ### Patterns #### Non-Reentrancy LayerZero Endpoint has a non-reentrancy guard for both the `send()` and `receive()`, respectively. In other words, both `send()` and `receive()` can not call themselves on the same chain. UAs **should not** rely on LayerZero to perform the non-reentrancy check. However, UAs can query the endpoint to see if the endpoint **`isSendingPayload()`** or **`isReceivingPayload()`** for finer-grained reentrancy control. #### Message Chaining UAs can call `send()` in the `receive()` calls on the same chain. Example applications for calling `send()` in the `receive()` include (e.g. Ping Pong): * the UA at the source chain wants a message receipt (Chain A -> Chain B -> Chain A) * the UA at the destination reroutes the message (Chain A -> Chain B -> Chain C) ```solidity wrap theme={null} function lzReceive(uint16 _srcChainId, bytes memory _fromAddress, uint64, /*_nonce*/ bytes memory _payload) external override { ... // message chaining endpoint.send{value: messageFee}( ... ); } ``` However, the fee for sending messages on another chain is not observable onchain. UAs would need to create some fee estimate heuristics. Optionally, user apps can store the chained message and then resend them with another transaction. #### Multi-Send UAs can send multiple messages in one transaction at the source chain. The endpoint non-reentrancy will not block this pattern. ```solidity wrap theme={null} function sendFirstMessage( uint gasAmountForDst, uint16[] calldata chainIds, bytes[] calldata dstAddresses) external payable { ... for(uint i = 0; i < chainIds.length; i++){ endpoint.send{value: fee}(chainIds[i], dstAddresses[i], messageString, msg.sender, address(0x0), _relayerParams); } } ``` # Message Read Library Source: https://docs.layerzero.network/v2/concepts/protocol/message-read-library The Read Library is a specialized Message Library designed for Omnichain Queries. It combines both send and receive capabilities to process read requests... The **Read Library** is a specialized Message Library designed for [Omnichain Queries](/v2/concepts/applications/read-standard). It combines both send and receive capabilities to process read requests and deliver verified responses across chains. ## What Makes the Read Library Unique? Unlike the standard [Message Send Library](./message-send-library) and [Message Receive Library](./message-receive-library), the Read Library handles a full request-and-response workflow: * **Send Side:** It serializes a read command and directs it to the appropriate chain using the application's configured Decentralized Verifier Networks (DVNs). * **Receive Side:** It verifies DVN attestations for the returned data and routes the final response back to the endpoint and ultimately the requesting application. This dual nature allows a single library to manage both outbound queries and inbound responses, ensuring the correct workers are used for each step. ## How It Fits Into lzRead When an application issues a query via `EndpointV2.send()`, the Read Library (`ReadLib1002`) encodes the request and forwards it to the configured DVNs. Each DVN reads from an archival node on the target chain, optionally performs off-chain compute (mapping or reducing data), and submits a hash of the result. Once the required number of DVNs confirm the same payload hash, the Read Library finalizes the response and the endpoint delivers the data to `OApp.lzReceive()`. This process transforms normal crosschain messaging into a request/response pattern: **Application → Endpoint → Read Library → DVNs → Read Library → Endpoint → Application** ## Configuration and Security Applications must configure the Read Library just like any other Message Library, specifying DVN thresholds and executor addresses. Because it enforces the DVN verification on the receive side, both the send and receive pathways must use the same `ReadLib1002` instance to ensure correct processing. ## Reference Implementation The reference contract for the Read Library can be found in the LayerZero V2 repository: `LayerZero-v2/packages/layerzero-v2/evm/messagelib/contracts/uln/readlib/ReadLib1002.sol` This file details how queries are encoded, how DVN submissions are validated, and how fees are handled for workers and the treasury. ## Summary * **Purpose:** Manage omnichain query requests and responses using the LayerZero Read workflow. * **Function:** Acts as both send and receive library, serializing requests, verifying DVN responses, and routing the final data to the application. * **Learn More:** For an overview of the read workflow and query language, see [Omnichain Queries (lzRead)](/v2/concepts/applications/read-standard). # Message Receive Library Source: https://docs.layerzero.network/v2/concepts/protocol/message-receive-library The Message Receive Library is a core component of the LayerZero protocol that manages the reception and verification of messages on the destination... The **Message Receive Library** is a core component of the LayerZero protocol that manages the reception and verification of messages on the destination chain. It functions as a dedicated message handler on the receive side by decoding incoming encoded packets, verifying their integrity through specialized processes, and routing valid messages to the endpoint. ## What Is a Message Receive Library? The Message Receive Library is responsible for several crucial tasks that enable secure and reliable processing of inbound messages: * **Decoding Messages:**\ It parses the incoming data, ensuring the received packet information can be accurately reconstructed. * **Verifying Integrity:**\ The library performs validation steps verifying that the packet is intended for the local endpoint and meets requirements set by the receiving application. * **Managing and Enforcing Configuration:**\ Applications set configuration parameters via the LayerZero Endpoint, which are then applied to the library’s internal worker logic. This configuration determines the expected verification requirements. Unlike the send side where fees are simply processed and workers selected, the receive library uses these settings to enforce that the workers verifying the packet match the predefined configuration. * **Routing to the Endpoint:**\ After verification, the decoded packet is passed from the library to the endpoint for further processing. In sum, the Message Receive Library encapsulates the core logic for safely accepting and processing incoming packets. ## How It Fits Into the Protocol LayerZero’s architecture separates the receive process into clear, sequential steps: **Receive Model Flow:**\ **Workers → Message Receive Library → Endpoint → Application** 1. **Workers:** These off-chain service providers receive the raw packet data and forward the `encodedPacket` to the destination chain. 2. **Message Receive Library:** The library decodes the incoming packet, verifies its integrity using both header information and payload data, and ensures that the `encodedPacket` meets library requirements and application configurations. 3. **Endpoint:** Once verified, the endpoint receives the validated packet and passes it to the appropriate application. 4. **Application:** The final recipient processes the application’s original message from the sender and executes business logic. ## Receive Ultra Light Node (ULN) A specialized variant of the Message Receive Library is the **Receive Ultra Light Node (ULN)**. Like its [sending counterpart](./message-send-library#send-ultra-light-node-uln), the Receive ULN is tailored for a streamlined process: it not only decodes and verifies inbound messages but also enforces that only the preconfigured DVNs (or workers) validate the message. ### Message Processing with Receive ULN 1. **Decoding the EncodedPacket:**\ The Receive ULN begins by decoding the received `encodedPacket`. This packet is composed of two parts: * **Packet Header:**\ Contains vital routing and identification details, such as version information, nonce, source and destination endpoint IDs, and sender and receiver contract addresses. * **Payload:**\ Includes the GUID and the actual application’s message. You can see how these data structures differ under [Message, Packet, and Payload](./packet). 2. **Verifying DVN Submissions:**\ The Receive ULN allows DVNs to call its verification function `verify()`, where each DVN submits a verification for a specific packet header eand payload hash. Thse attestations are stored in an internal mapping, ensuring that each DVN’s submission is recorded. 3. **Enforcing Configuration:**\ Before an inbound message is accepted, the Receive ULN retrieves the `UlnConfig` set by the application (via the Endpoint) and verifies that the DVN meet the required criteria, both in terms of identity and the number of block confirmations. This step ensures that only messages verified by the proper, preconfigured workers are processed. 4. **Commit Verification:**\ Once the DVN verifications have been checked against the configuration, the `commitVerification()` function is called. This function: * Asserts that the packet header is correctly formatted and that the destination endpoint matches the local configuration. * Retrieves the receive `UlnConfig` based on the source endpoint and receiver contract address. * Checks that the necessary verification conditions have been met using the stored DVN verifications. * Reclaims storage for the verification records and calls the destination Endpoint's `verify()` method, thereby adding the message to the inbound messaging channel. ## In Summary * **Purpose:** The Message Receive Library governs the decoding, verification, and routing of inbound messages in the LayerZero protocol. * **Function:** It deciphers the `encodedPacket` and validates the integrity through predefined checks, and then hands the message off to the endpoint for delivery. * **Design:** By isolating the inbound processing logic in a dedicated module, the protocol remains modular and adaptable. Specialized variants such as the Receive ULN demonstrate how the architecture can be tailored to meet different operational needs. * **User Benefit:** For developers and users, this clear separation ensures that crosschain communication is both secure and efficient, while also remaining flexible enough to integrate future enhancements. # Message Channel Security Source: https://docs.layerzero.network/v2/concepts/protocol/message-security Cross‑chain messaging introduces unique security challenges: the total value moved between chains often far exceeds what any single validator set can... Cross‑chain messaging introduces unique security challenges: the total value moved between chains often far exceeds what any single validator set can effectively protect. LayerZero's architecture isolates risk per [pathway](../glossary#channel--lossless-channel), ensuring security measures can scale directly with the value in the channel. ## Why Traditional Bridges Struggle Most cross‑chain bridges rely on a single, global validator set to secure all transfers between networks. This creates a concentration of risk: any attack compromises the entire pool of bridged assets rather than a specific transfer. | Asset Value Secured | Security Scope | Security Implication | | --------------------- | -------------------- | ------------------------------------- | | All cross‑chain value | Single validator set | Large aggregated target for attackers | Because their security isn't partitioned per application pathway, traditional bridges expose every asset moving across chains to the same risk; making them a high‑value target for adversaries. ## LayerZero's Channel Security Model LayerZero avoids this misalignment by decoupling security from aggregate network value. Instead of one monolithic bridge, it partitions trust into per‑channel security configurations. Each unique pathway (sender → source Endpoint → destination Endpoint → receiver) is secured by its own configuration of Decentralized Verifier Networks (DVNs). ### Configurable Channel‑Level Security (X‑of‑Y‑of‑N) Every application defines its own security parameters: | Parameter | Definition | Effect on Security & Cost | | --------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | **X** | Specific DVNs required to always witness a message | Higher X increases fault tolerance by controlling which DVNs must always agree | | **Y** | Total DVN threshold (required + optional) | Ensures specific DVNs always verify while the remainder come from any members of the broader pool, balancing specificity and decentralization | | **N** | Total DVNs available | Maximum pool of DVNs for the channel | #### Key Benefits * **Granular Risk Isolation:** Attackers can only target a specific channel's value, not the entire cross‑chain mesh. * **Economic Alignment:** Security scales with the channel's value, so higher‑value paths can require stronger DVN configurations. * **Configurable Trade‑Offs:** High‑value channels can opt for larger X/Y/N thresholds; low‑value channels can reduce them to minimize cost and latency. ## Why LayerZero's Approach Is More Secure | Feature | LayerZero Channel Security | Monolithic Bridges | | ----------------------- | ----------------------------------- | ----------------------------------------- | | Economic Attack Cost | Scoped to individual channel value | Covers every connected chain's value | | Attack Surface | Isolated per channel | Entire network mesh | | Security Cost Alignment | Matches collateral to channel value | Single validator set must cover all value | | Configurability | Adjustable per channel | Fixed, global configuration | | Immutability | Only adjustable by application | Core interfaces upgradeable via multisig | While no system can guarantee per‑pathway collateral that always exceeds transferred value, LayerZero's design dramatically raises the economic cost of a successful attack compared to existing bridges. ## Impact LayerZero is today the only modular cross‑chain messaging framework that is both fully permissionless and immutable. Once an application defines its channel's X‑of‑Y‑of‑N security settings, those parameters are enforced at the protocol level indefinitely. Only the application [delegate](../glossary#delegate) can update these configurations. There is no governance, upgrade mechanism, or external actor that can alter or disable a channel's configuration once set, guaranteeing that security guarantees persist without relying on LayerZero. By partitioning security and allowing each channel to calibrate its own verifier quorum, LayerZero achieves a practical balance between robust protection and efficient operation, delivering a more economically sound, scalable omnichain architecture. # Message Send Library Source: https://docs.layerzero.network/v2/concepts/protocol/message-send-library Learn about Message Send Library in LayerZero V2. Understand the architecture, core concepts, and how it enables omnichain interoperability. Essential inform... The **Message Send Library** is a core component of the LayerZero protocol that manages the internal mechanics of sending messages between blockchain networks. It functions as a dedicated message handler and routing contract that connects high-level application logic with the low-level workers responsible for crosschain communication. ## What Is a Message Send Library? The Message Send Library is responsible for several key tasks that enable reliable message delivery: * **Encoding Packets:** It packages outgoing message packets from the LayerZero Endpoint by encoding the unique identifiers, nonces (which help maintain the correct order), and other metadata. This process ensures that each message is uniquely identifiable and traceable across networks. * **Calculating Fees:** While processing a packet, the library computes and returns fee details back to the endpoint based on the worker settings defined by the application. This ensures that all cost-related aspects of message delivery are handled accurately. * **Managing Configuration:** Applications set configuration parameters via the LayerZero Endpoint, which are then applied to the library’s internal worker logic. This means that the library processes messages based on custom application settings for routing and fee management. The Message Send Library acts as a specialized routing contract to direct how packets are encoded, how fees are computed, and how configurations shape the overall message delivery process. ## How It Fits Into the Protocol LayerZero’s design splits the crosschain messaging process into clear, sequential steps: **Send Model Flow:**\ **Application → Endpoint → Message Send Library → Workers** 1. **Application:** The sender smart contract initiates a message for a fee. 2. **Endpoint:** Acting as the entrypoint, the endpoint moves the message inside a packet, and leverages the application’s settings to determine which Message Send Library to invoke. 3. **Message Send Library:** The library processes the packet by encoding it, calculating fees for the given configuration settings, and routing the encodedPacket to the appropriate workers. 4. **Workers:** These service providers handle the actual transmission and execution of the encodedPacket, ensuring it reaches its intended destination. ## Send Ultra Light Node (ULN) A specialized version of the Message Library is the **Ultra Light Node (ULN)**. A ULN focuses on efficiently streaming and encoding only the critical packet headers along with the application's message. In other words, while every Message Library can define its own outbound message encoding, the ULN variant is tailored for push-based messaging to a destination chain. The ULN concept borrows from the idea of a [Light Node](https://www.alchemy.com/overviews/light-node) in blockchain systems, which processes only block headers rather than entire blocks. Similarly, the ULN transmits a specific, optimized encoded format called the **encodedPacket**. This format is constructed in two key steps: ### Message Encoding with ULN 1. **Packet Header Encoding:**\ The ULN first creates a concise header containing vital routing and identification information. This includes: * **Version Information:** To ensure consistent interpretation of the packet. * **Nonce:** To maintain the correct order of messages. * **Source and Destination Information:** Such as endpoint identifiers and sender/receiver contract addresses. This header functions as a roadmap for subsequent processing by workers. 2. **Payload Encoding:**\ Next, the ULN encodes the remaining contents of the protocol packet. In this context: * **The Application's Message:** Represents the actual content sent by the application. * **GUID:** A global unique identifier that ties the message to its metadata. The ULN combines these two components (`packetHeader` and `payload`) to create the final **encodedPacket**. This composite packet includes both the serialized header (providing essential metadata) and the payload (containing the GUID and the actual message), enabling downstream workers to efficiently process and verify the message. You can see how these data structures differ under [Message, Packet, and Payload](./packet). ## Key Takeaways * **Adaptability:** The overall encoding process is flexible. Different Message Libraries can adopt their own strategies based on performance or security considerations. The ULN is just one example that emphasizes efficiency by transmitting minimal yet critical data. * **Future-Proofing:** This modular approach to encoding allows for technological advancements to be integrated into the protocol without disrupting existing application logic. ## In Summary * **Purpose:** The Message Send Library manages the processes of encoding, configuring, and fee-calculating messages within the LayerZero protocol. * **Function:** Acting as a dedicated handler and routing contract, it bridges the gap between applications and the underlying message workers, ensuring proper packaging and delivery. * **Design:** By clearly separating responsibilities, the protocol remains modular and adaptable. The ULN exemplifies how a specialized Message Library can optimize for specific functions, such as ultra-lightweight packet header transmission. * **User Benefit:** For developers and end-users, this robust, configurable routing mechanism simplifies crosschain communication while ensuring high efficiency and security. # Message, Packet, and Payload Source: https://docs.layerzero.network/v2/concepts/protocol/packet Because crosschain messaging enables a wide range of operations, such as transferring assets, relaying data, or executing external calls, the LayerZero... Because crosschain messaging enables a wide range of operations, such as transferring assets, relaying data, or executing external calls, the LayerZero protocol standardizes how information is passed from one chain to another. This standardization is achieved by breaking down the process into three interconnected components: ### Message (Application) The message is the raw, original content or instruction as defined by the application in `bytes`. It represents the core data that the sender intends to deliver to the recipient via the LayerZero Endpoint: ```solidity wrap theme={null} // packages/layerzero-v2/evm/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol struct MessagingParams { uint32 dstEid; bytes32 receiver; // highlight-next-line bytes message; bytes options; bool payInLzToken; } ``` ### Packet (Endpoint) The Packet is the protocol-level container that wraps the application’s message along with additional metadata necessary for secure and reliable crosschain communication. The standard Packet structure is defined as follows in the LayerZero Endpoint: ```solidity wrap theme={null} // packages/layerzero-v2/evm/protocol/contracts/interfaces/ISendLib.sol struct Packet { uint64 nonce; // The nonce of the message in the pathway, ensuring proper ordering and preventing replay attacks. uint32 srcEid; // The source endpoint ID. address sender; // The sender address. uint32 dstEid; // The destination endpoint ID. bytes32 receiver; // The receiving address. bytes32 guid; // A globally unique identifier for tracking the message. bytes message; // The application’s original message. } ``` This structure ensures that each message is uniquely identifiable and carries the necessary information (like routing, ordering, and traceability data) for the underlying protocols to process it accurately. ### Payload (Message Libraries) The payload is the encoded representation of the key components of the Packet that the messaging libraries operate on. In many library implementations (for example, in the Ultra Light Node), the payload is created by serializing specific elements of the Packet (typically the GUID followed by the actual application message) into a compact binary format: ```solidity wrap theme={null} // packages/layerzero-v2/evm/protocol/contracts/messagelib/libs/PacketV1Codec.sol function encodePayload(Packet memory _packet) internal pure returns (bytes memory) { return abi.encodePacked(_packet.guid, _packet.message); } ``` When combined with the encoded packet header (which contains routing and metadata information such as the nonce, endpoint IDs, and addresses), the payload forms the final **encodedPacket** that is transmitted between chains. ```solidity wrap theme={null} // packages/layerzero-v2/evm/protocol/contracts/messagelib/libs/PacketV1Codec.sol function encodePacketHeader(Packet memory _packet) internal pure returns (bytes memory) { return abi.encodePacked( PACKET_VERSION, _packet.nonce, _packet.srcEid, _packet.sender.toBytes32(), _packet.dstEid, _packet.receiver ); } ``` ```solidity wrap theme={null} // packages/layerzero-v2/evm/messagelib/contracts/uln/SendUlnBase.sol encodedPacket = abi.encodePacked(packetHeader, payload); ``` ## Packet Structure and Its Benefits Standardizing the Packet structure brings several advantages: * **Ordering and Routing:**\ Fields like `nonce`, `srcEid`, `dstEid`, and `receiver` ensure that messages are delivered in the correct order to the proper destination, while also mitigating replay attacks. * **Traceability:**\ The inclusion of a unique `guid` along with source and destination identifiers allows each message to be tracked across chains, providing a robust audit trail that enhances debugging and system trust. * **Payload Integrity:**\ The `message` field carries the actual application data, and when the Packet is processed by a messaging library, its contents are split into two parts: 1. **Packet Header:** Contains essential routing and identification metadata. 2. **Payload:** Comprises the encoded version of the GUID and the application’s message. This separation allows for efficient processing by downstream components while ensuring that the integrity of the message is maintained throughout transit. ## Summary * **Message:**\ The raw application data or instruction that needs to be communicated. * **Packet:**\ The complete protocol container that encapsulates the message along with metadata (nonce, endpoint IDs, sender, receiver, and a global identifier) required for secure and orderly crosschain communication. * **Payload:**\ The encoded portion (typically a serialization of the GUID and message) that is generated by the messaging library and used for efficient data transmission and processing. This layered approach ensures that messages are both adaptable to various blockchain environments and robust in terms of security and traceability. # Protocol Overview Source: https://docs.layerzero.network/v2/concepts/protocol/protocol-overview Learn about Protocol Overview in LayerZero V2. Understand the architecture, core concepts, and how it enables omnichain interoperability. Architecture and im... To send a crosschain message, a user must write a transaction on both the source and destination blockchains. At its core, the LayerZero protocol defines a **channel** between a `sender` and a `receiver` smart contract by leveraging two key components: * **Source and Destination Endpoints:**\ Each supported blockchain deploys an immutable, permissionless Endpoint contract. On the source chain, a smart contract calls the Endpoint’s entry function (`endpoint.send()`) to send a message. On the destination chain, a smart contract authorizes the Endpoint to act as an exit point to receive and process that same message (`endpoint.lzReceive()`). * **Channel Definition:**\ A unique messaging channel in LayerZero is defined by four specific components: 1. **Sender Contract (Source OApp):** The contract initiating the crosschain communication. 2. **Source Endpoint ID:** The identifier for the Endpoint on the source chain. 3. **Destination Endpoint ID:** The identifier for the Endpoint on the destination chain. 4. **Receiver Contract (Destination OApp):** The contract designated to receive and process the message on the destination chain. Within each channel, message ordering is maintained through nonce tracking. This ensures that messages are delivered exactly once. For example, if a token bridge on one chain sends a message to its counterpart on another chain, the messages flow through a dedicated channel — distinct from all other application pathways between those chains — preserving the integrity and sequence of communication. ## How the Protocol Works Diagram showing LayerZero V2 protocol flow: message dispatch through source Endpoint, Message Library generating standardized packets, DVN verification, and execution via lzReceive on the destination chain Diagram showing LayerZero V2 protocol flow: message dispatch through source Endpoint, Message Library generating standardized packets, DVN verification, and execution via lzReceive on the destination chain 1. **Message Dispatch on the Source Chain:**\ A smart contract on the source blockchain initiates the process by calling the Endpoint's entry function. This call includes an arbitrary message payload, details of the destination Endpoint, and the receiver's contract address. The Endpoint then uses a configurable Message Library to generate a standardized Message Packet based on the sender contract’s configuration. 2. **Establishing a Secure Channel:**\ The generated Message Packet is emitted as an event by the source Endpoint. This packet contains critical information—including source and destination Endpoint IDs, the sender's and receiver’s addresses, and the message payload—which collectively define a unique messaging channel. 3. **Verification and Nonce Management:**\ On the destination chain, the configured Security Stack (Decentralized Verifier Networks) deliver the corresponding payload hash to the receiver contract's configured Message Library. Once the threshold of DVN verifications satisfies the [X of Y of N](../glossary#x-of-y-of-n) configuration, the Message Packet can be marked as verified and committed to the destination channel, ensuring exactly-once delivery. 4. **Message Execution on the Destination Chain:**\ Finally, a caller (typically an authorized smart contract like the Executor) calls the Endpoint’s exit function `lzReceive` to trigger the execution of the verified message. This call delivers the message payload to the receiver contract, which can then execute its defined logic based on the incoming data. ## Security and Flexibility * **Immutable and Permissionless Design:**\ The core Endpoint contracts are immutable and permissionless. This ensures that the protocol remains secure and resistant to unauthorized changes, regardless of which virtual machine (VM) or blockchain environment is used. * **VM-Agnostic Integration:**\ The LayerZero protocol itself is designed to be VM agnostic. The same fundamental principles apply whether you’re working with Solidity on Ethereum, Rust on Solana, Move on Aptos, or any other supported environment. * **Independent Channel Management:**\ Each channel between a given pair of endpoints maintains its own independent message sequence. This means that multiple applications can communicate across the same chain pairs without interference, providing scalability and flexibility in designing crosschain solutions. ## Further Reading For more detailed technical insights into the protocol contracts for each specific virtual machine, please refer to the following overviews: * **EVM Technical Overview:**\ Learn how LayerZero’s protocol contracts are implemented for EVM-based chains, covering the Endpoint architecture, Message Libraries, and Workers.\ [Read the EVM Protocol Overview](../../developers/evm/protocol-contracts-overview) * **Solana Technical Overview:**\ Discover the adaptations made for Solana’s runtime, including crosschain messaging through the LayerZero Endpoint and integrations with Solana’s unique architecture.\ [Read the Solana Protocol Overview](../../developers/solana/technical-overview) * **Aptos Technical Overview:**\ Explore how LayerZero leverages the Aptos Move language and framework to implement secure and efficient crosschain messaging on Aptos-based networks.\ [Read the Aptos Protocol Overview](../../developers/aptos-move/overview) # Transaction Pricing Model Source: https://docs.layerzero.network/v2/concepts/protocol/transaction-pricing LayerZero's transaction pricing model is designed to fairly distribute costs across the various components that enable secure, reliable crosschain... LayerZero's transaction pricing model is designed to fairly distribute costs across the various components that enable secure, reliable crosschain messaging. Understanding this model helps developers and users make informed decisions about gas allocation and fee optimization. ## Why Crosschain Pricing is Complex Traditional blockchain transactions occur within a single network where gas costs are predictable and uniform. Crosschain messaging introduces unique challenges: * **Source chains have no knowledge** of destination chain state, gas prices, or execution requirements * **Multiple networks** with different native tokens, gas mechanisms, and pricing models must be coordinated * **Off-chain infrastructure** (DVNs and Executors) provides critical services that require compensation * **Message execution** on the destination must be funded upfront from the source chain LayerZero's pricing model addresses these challenges through a transparent, component-based fee structure. ## Four-Component Fee Structure Every LayerZero transaction consists of four distinct cost elements: ### 1. Source Chain Transaction The standard blockchain transaction fee paid to miners/validators on the source network for including your transaction in a block. This follows each chain's native fee mechanism (gas on Ethereum, compute units on Solana, etc.). ### 2. Security Stack Fees Payment to your configured [Decentralized Verifier Networks (DVNs)](../modular-security/security-stack-dvns) for verifying and attesting to your message. These fees: * Vary based on your security configuration (number and type of DVNs) * Scale with the complexity of verification required * Are split among your chosen verifier networks ### 3. Executor Fees Compensation to [Executors](../permissionless-execution/executors) for delivering and executing your message on the destination chain. This covers: * Monitoring source chains for new messages * Submitting transactions on destination chains * Managing the operational infrastructure for reliable delivery ### 4. Destination Gas Purchase The cost of purchasing destination chain gas tokens to fund your message execution. This is calculated by converting your specified gas amount from destination pricing to source chain tokens. ## Crosschain Gas Conversion Since you pay on the source chain but consume gas on the destination chain, LayerZero workers perform real-time conversion using market prices: $$ \text{Source Chain Cost} = \text{gasUnits} \times \text{dstGasPrice} \times \frac{\text{dstTokenPrice}}{\text{srcTokenPrice}} $$ Where: * **gasUnits**: Amount of gas needed on destination chain (e.g., 200,000) * **dstGasPrice**: Gas price on destination chain (e.g., 50 gwei) * **dstTokenPrice**: USD price of destination chain's native token (e.g., \$3,000 for ETH) * **srcTokenPrice**: USD price of source chain's native token (e.g., \$1.50 for POL) The formula works in two steps: 1. **Calculate destination gas cost**: `gasUnits × dstGasPrice` = cost in destination tokens 2. **Convert to source tokens**: Multiply by the price ratio to get equivalent cost in source tokens ### Example Scenario Sending from **Polygon** (POL) to **Ethereum** (ETH): * **gasUnits**: 200,000 units * **dstGasPriceWei**: 50 gwei * **dstTokenPrice**: ETH = \$3,000 * **srcTokenPrice**: POL = \$1.50 **Calculation**: ``` Step 1: Calculate gas cost on destination chain 200,000 gas units × 50 gwei = 10,000,000 gwei = 0.01 ETH Step 2: Convert to source chain tokens using price ratio 0.01 ETH × ($3,000 ETH ÷ $1.50 POL) = 0.01 × 2,000 = 20 POL ``` This ensures you pay the correct amount in your source chain's currency to fund execution on any destination chain. ## Dynamic Pricing Factors Several factors influence the final transaction cost: ### Chain-Specific Variations * **Gas mechanisms** differ across chains (Ethereum's EIP-1559, Arbitrum's L2 fees, Solana's compute units) * **Network congestion** affects base gas prices * **Token price volatility** impacts crosschain conversion rates ### Security Configuration Impact * More DVNs increase verification costs but enhance security * Premium DVN services may charge higher fees * Custom security thresholds affect overall pricing ### Execution Requirements * Complex contract logic requires more destination gas * Composed messages need additional execution allowances * Message size affects processing costs ## Fee Estimation and Quotes LayerZero provides onchain quote mechanisms that calculate exact fees before message submission: ### Quote Components * **Native fee**: Cost in the source chain's native token * **LZ token fee**: Alternative payment option using LayerZero's utility token * **Real-time pricing**: Updates based on current gas prices and token values ### Payment Flexibility Applications can choose between: * **Native token payment**: Using the source chain's gas token (ETH, POL, AVAX, etc.) * **LZ token payment**: Using LayerZero's crosschain utility token for consistent pricing ## Gas Profiling Considerations Destination gas requirements vary significantly based on your application logic: ### Typical Gas Ranges * **Simple token transfers**: 60,000-80,000 gas * **Complex DeFi interactions**: 200,000-500,000 gas * **Multi-step composed operations**: 300,000+ gas ### Optimization Strategies * **Profile your contracts** on each target chain to understand actual consumption * **Include gas buffers** to account for network-specific variations * **Test execution paths** thoroughly to avoid failed deliveries * **Monitor gas costs** across different chains and adjust allocations accordingly ## Best Practices ### For Developers * **Design gas-efficient contracts** to minimize destination execution costs * **Implement proper fee estimation** in your application interfaces * **Consider chain-specific optimizations** for frequently used pathways * **Plan for gas price volatility** in your economic models ### For Users * **Understand total cost breakdown** before initiating transactions * **Consider timing** transactions during periods of lower network congestion * **Monitor crosschain fee patterns** to optimize transaction scheduling * **Plan gas allocations** based on the complexity of your destination operations ## Economic Alignment LayerZero's pricing model creates proper economic incentives: * **Security providers** are compensated for verification services * **Infrastructure operators** earn fees for reliable message delivery * **Gas efficiency** is rewarded through lower total costs * **Fair pricing** ensures each pathway pays for its actual resource consumption This transparent, component-based approach ensures that crosschain messaging costs reflect the true value provided by each part of the LayerZero ecosystem while maintaining predictable pricing for applications and users. # OApp Technical Reference Source: https://docs.layerzero.network/v2/concepts/technical-reference/oapp-reference LayerZero’s Omnichain Application (OApp) standard defines a common set of patterns and interfaces for any smart contract that needs to send and receive... LayerZero’s **Omnichain Application (OApp)** standard defines a common set of patterns and interfaces for any smart contract that needs to send and receive messages across multiple blockchains. By inheriting OApp’s core functionality, higher-level primitives (such as OFT, ONFT, or any custom crosschain logic) can rely on a unified, secure messaging layer. All OApp implementations must handle: * **Message sending**: Encode and dispatch outbound messages * **Message receiving**: Decode and process inbound messages * **Fee handling**: Quote, collect, and refund native & ZRO fees * **Peer management**: Maintain trusted mappings between chains * **Channel management and security**: Control security and execution settings between chains ## Deployment Every OApp needs to be deployed on each chain where it will operate. Initialization involves two steps: ### 1. Integrate with the local Endpoint 1. Pass the local Endpoint V2 address into your constructor or initializer. 2. The Endpoint’s delegate authority is set to your OApp and the address initializing unless overridden. 3. As a delegate, your OApp can call any `endpoint.*` security method (`setSendLibrary`, `setConfig`, etc.) in a secure, authorized manner. ### 2. Configure peers (directional peering) 1. On each chain, the owner calls `setPeer(eid, peerAddress)` to register the remote OApp’s address for a given Endpoint ID. 2. Repeat on the destination chain: register the source chain’s OApp address under its Endpoint ID. 3. Because trust is directional, the receiving OApp checks `peers[srcEid] == origin.sender` before processing inbound messages. For guidelines on channel security, see [**Message Channel Security**](../protocol/message-security). For an example implementation, see the [**OFT Technical Reference**](./oft-reference). ## Core Message Flow OApps follow a three-step life cycle. Developers focus on local state changes and message encoding; LayerZero handles secure routing and final delivery. | Phase | Actors | Responsibility | | ----------------------- | ---------------------------- | ------------------------------------------------------------ | | **1. `send(...)`** | OApp | Perform local state change and encode the message | | **3. Transport** | LayerZero, DVNs, & Executors | Build, verify, and route the packet to the destination chain | | **4. `lzReceive(...)`** | OApp | Validate origin, decode message, apply state change | ### 1. `send(...)` Entrypoint * **Developer-defined logic** 1. Perform a local state change (e.g., burn or lock tokens, record intent). 2. Encode all necessary data (addresses, amounts, or arbitrary instructions) into a byte array. 3. Optionally accept execution options (gas limits, native gas transfers, or LayerZero Executor services). * **Key points** * Your public `send(...)` handles only local logic and message construction. * All packet assembly, peer lookup, and fee handling occur inside the internal call to `endpoint.send(...)`. ### 2. Transport and Routing * **Fee payment and validation** 1. Ensure the caller has supplied exactly the required native or ZRO fee. 2. When `endpoint.send(...)` executes, the Endpoint verifies that the fees match the quote from the chosen messaging library. Underpayment causes a revert. * **Packet construction and dispatch** 1. The Endpoint computes the next outbound nonce for `(sender, dstEid, receiver)` and builds a `Packet` struct with `nonce`, `srcEid`, `sender`, `dstEid`, `receiver`, `GUID`, and the raw `message`. 2. It looks up which send library to use, either a per-OApp override or a default, for `(sender, dstEid)`. 3. The send library serializes the `Packet` into an `encodedPacket` and returns a `MessagingFee` struct. 4. The Endpoint emits a `PacketSent(...)` event so DVNs and Executors know which packet to process. * **DVNs & Executors** * Paid DVNs pick up the packet, verify its integrity, and relay it to the destination chain’s Endpoint V2. * The destination library enforces DVN verification and block-confirmation requirements based on your receive config. * **Destination Endpoint validation** 1. Verify that the packet’s `srcEid` has a registered peer. 2. Confirm that `origin.sender` matches `peers[srcEid]`. * **Invoke `lzReceive(...)`** * If validation succeeds, the destination Endpoint calls your OApp’s public `lzReceive(origin, guid, message, executor, extraData)`. ### 3. `lzReceive(...)` Entrypoint * **Access control and peer check** * Only the Endpoint may call `lzReceive`. * Immediately validate that `_origin.sender == peers[_origin.srcEid]`. * **Internal `_lzReceive(...)` logic** 1. Decode the byte array into original data types (addresses, amounts, or instructions). 2. Execute the intended onchain business logic (e.g., mint tokens, unlock collateral, update balances). 3. If there’s a composable hook, your OApp can invoke `sendCompose(...)` to bundle further crosschain calls. * **Outcome** * Upon completion, the destination chain’s state reflects the source chain’s intent. Any post-processing (events, composable calls) occurs here. This clear separation between local state updates in `send(...)` versus remote updates in `_lzReceive(...)` lets you focus on business logic while LayerZero’s Endpoint V2 manages transport intricacies. ## Security and Channel Management Whether you’re using Solidity, Rust, or Move, these foundational patterns ensure consistent security, extensibility, and developer ergonomics. ### Security and roles * **Owner** * Manages delegates, peers, and enforced gas settings * `setPeer(...)`: update trust mappings * `setDelegate(...)`: assign a new delegate for Endpoint configurations * `setEnforcedOptions(...)`: define per-chain minimum gas for inbound execution * **Delegate** * Manages Endpoint settings and message-channel controls * `setSendLibrary(oappAddress, eid, newLibrary)`: override send library for `(oappAddress, eid)`. * `setReceiveLibrary(oappAddress, eid, newLibrary, gracePeriod)`: override receive library; `gracePeriod` lets the previous library handle retries. * `setReceiveLibraryTimeout(oappAddress, eid, library, newTimeout)`: update how long an old receive library remains valid. * `setConfig(oappAddress, libraryAddress, params[])`: adjust per-library settings (DVNs, Executors, confirmations). * `skip(oappAddress, srcEid, srcSender, nonce)`: advance the inbound nonce without processing when verification fails. * `nilify(oappAddress, srcEid, srcSender, nonce, payloadHash)`: treat the payload as empty and advance the inbound nonce. * `burn(oappAddress, srcEid, srcSender, nonce, payloadHash)`: permanently discard a malicious or irrecoverable payload. * `clear(oappAddress, origin, guid, message)`: mark a verified message as received without executing it, advancing the inbound nonce. Use multisigs or your preferred governance to manage Owner and Delegate roles. The roles above assume the standard `Ownable` OApp. OApps built on role-based access control — such as the [Stablecoin OFT](/v2/developers/evm/stablecoin-oft/rbac-reference) — replace the single `Owner` with `DEFAULT_ADMIN_ROLE`, which gates `setPeer`, `setEnforcedOptions`, and `setMsgInspector`. In that model `setDelegate` always reverts: the Delegate is permanently synced to the `DEFAULT_ADMIN_ROLE` holder and moves only via the two-step `beginDefaultAdminTransfer` → `acceptDefaultAdminTransfer` flow. ### Peering and Trust Management * **`peers` mapping** * Store a mapping from `eid → bytes32 peerAddress`. Using `bytes32` lets you store addresses for various chains. * `setPeer(eid, peerAddress)` updates that mapping. Passing `bytes32(0)` disables the pathway. * **Directional trust** * Registering on Chain A → Chain B does not register the reverse. Each side must call `setPeer` for the other. * On receipt, enforce `peers[origin.srcEid] == origin.sender` to confirm the message is from the expected contract. * **Updating peers** * If you redeploy or upgrade an OApp, call `setPeer` on both old and new deployments to maintain continuity. ## Further Reading * **[Message Channel Security](../protocol/message-security)**\ Deep dive into Endpoint V2's cryptographic guarantees, signature verification, and relayer/oracle incentives. * **[OFT Technical Reference](./oft-reference)**\ A concrete OApp example for fungible token transfers, illustrating how to use OApp's core patterns without platform-specific code. * **[Omnichain Composability](../applications/composer-standard)**\ Patterns for building advanced crosschain primitives (AMM routers, multi-chain staking, governance) on top of OApp's hooks and the Executor model. # Omnichain Fungible Token (OFT) Technical Reference Source: https://docs.layerzero.network/v2/concepts/technical-reference/oft-reference LayerZero's Omnichain Fungible Token (OFT) standard enables a single fungible token to exist across many chains while preserving one global supply. The... LayerZero's **Omnichain Fungible Token** (OFT) standard enables a single fungible token to exist across many chains while preserving one global supply. The standard abstracts away differences in contract languages, so the high-level behavior is identical no matter which VM you deploy on. ## Deployment An OFT contract must be deployed on every network where a token currently exists or will exist. Since OFT contracts inherit all of the core properties of a LayerZero OApp, connecting OFT deployments requires setting a directional channel configuration between the source chain and the destination blockchain. ### Channel Configuration Every OFT deployment must have a directional channel configuration for messaging to be successful. This means the deployer must: * **Connect the messaging channel at the Endpoint level** (establishing the underlying pathway for crosschain messages). * **Pair the OFT deployments at the OApp level** using `setPeer(...)`, so each contract knows its trusted counterpart on the destination chain. For an overview of what a messaging channel is, see [Message Channel Security](../protocol/message-security). For a more thorough explanation of channel configuration and peer relationships, see the [OApp Reference](./oapp-reference). ## Core Transfer Flow When an OFT transfer is initiated, the token balance on the source chain is **debited**. This either burns or locks the tokens inside the OFT contract, similar to an [escrow account](../glossary#escrow-account). A message is then sent via LayerZero to the destination chain where the paired OFT **credits** the recipient by minting or unlocking the same amount. This mechanism guarantees a unified supply across all chains. 1. **Debit on the source chain**\ The sender calls the OFT's `send(...)` function, burning or locking an amount of tokens. 2. **Message dispatch via LayerZero**\ The source OFT packages the transfer details into a LayerZero message and routes it through the protocol's messaging layer. LayerZero's messaging rails handle crosschain routing, verification of the encoded message, and delivery of the message to the destination chain's receiver OFT contract. 3. **Credit on the destination chain**\ The paired OFT receives the message and *credits* the recipient by minting new tokens or unlocking previously-held tokens. The total supply across all chains remains constant, since burned or locked tokens on the source chain are matched 1:1 with minted or unlocked tokens on the destination. 4. **(Optional) Trigger a composing call**\ A composing contract uses the tokens received in a new transaction, delivered automatically by the LayerZero Executor, to trigger some state change (e.g., swap, stake, vote). For more details on how to implement composable OFT transfers, see [Omnichain Composability](../applications/composer-standard). ## Core Concepts This section explains the fundamental design principles that make OFT a flexible, developer-friendly standard for fungible tokens. ### 1. Transferring Value Across Different VMs When transferring tokens across different virtual machines, OFT needs to handle varying decimal precision between chains. This is managed through a few key concepts: * **Local Decimals**\ Blockchains use [integer mathematics](https://www.helius.dev/blog/solana-arithmetic#use-integers-and-minor-units) to represent token amounts, avoiding floating-point precision issues. Each chain's recommended token standard stores tokens as integers but with different decimal place conventions to represent fractional units. For example: * **EVM chains**: ERC-20 tokens typically use 18 decimal places. What users see as "1.0 USDC" is stored onchain as `1000000000000000000` (1 × 10^18) - the smallest unit often called "wei" * **Solana**: SPL tokens commonly use 6 or 9 decimal places. The same "1.0 USDC" would be stored as `1000000` (1 × 10^6) - the smallest unit of SOL called "lamports" (10^-9) * **Aptos**: Fungible Assets may use 6 or 8 decimal places depending on the asset Without proper conversion, transferring the integer value `1000000000000000000` from an 18-decimal EVM chain to a 6-decimal Solana chain would result in an astronomically large amount instead of the intended 1 token. The `localDecimals` field tells the OFT contract how many decimal places that specific blockchain uses to represent the token's smallest units. * **Shared Decimals**\ To ensure consistent value representation, every OFT declares a `sharedDecimals` parameter. Before sending a token crosschain, the OFT logic converts the "local" amount into a normalized "shared" unit. Upon arrival, the destination OFT reconverts that shared unit back into the local representation of its own decimal precision. * **Dust Removal**\ Before converting the local unit amount (`amountLD`) into the shared unit amount (`amountSD`), OFT implementations first "floor" the local amount to the nearest multiple of the conversion rate so that no remainder ("dust") is included in the crosschain transfer. The normalization process works as follows: 1. Compute the conversion rate: $$ \text{decimalConversionRate} = 10^{(\text{localDecimals} - \text{sharedDecimals})} $$ 2. Remove dust by flooring to that multiple (e.g., integer division on the EVM): $$ \text{flooredAmountLD} = \Bigl\lfloor \frac{\text{amountLD}}{\text{decimalConversionRate}} \Bigr\rfloor \times \text{decimalConversionRate} $$ 3. Compute and return the dust remainder to the sender: $$ \text{dust} = \text{amountLD} - \text{flooredAmountLD} $$ That `dust` is refunded to the sender's balance before proceeding with **debiting** the sender's account, and the `flooredAmountLD` is now used as the `amountLD`. 4. Convert the amount in local decimals (`amountLD`) to shared units on the source chain: $$ \text{amountSD} = \frac{\text{amountLD}}{\text{decimalConversionRate}} $$ 5. Transmit the amount in shared decimals (`amountSD`) as part of the LayerZero message. 6. On the destination chain, reconstruct the local amount (`amountLD`): $$ \text{amountLD} = \text{amountSD} \times \text{decimalConversionRate} $$ * **Why This Matters** * **Consistent Economic Value:** "1 OFT" means the same thing on any chain, regardless of differing decimal precision. * **DeFi Compatibility:** Prevents rounding errors and ensures seamless integration with onchain tooling (e.g., AMMs, lending protocols) that expect familiar decimal behavior. * **No Precision Loss:** By using a common `sharedDecimals`, you avoid truncation or expansion mistakes when moving large or small amounts across networks. If you override the vanilla `sharedDecimals` amount or have an existing token supply exceeding `18,446,744,073,709.551615` tokens, extra caution should be applied to ensure `amountSD` and `amountLD` do not overflow. Vanilla OFTs can disregard this admonition. 1. **Shared‐Unit Overflow (`amountSD`)**\ OFT encodes `amountSD` as a 64-bit unsigned integer (`uint64`). The largest representable shared‐unit value is `2^64 − 1`. Therefore, the maximum token supply (in whole‐token terms) is: $$ \frac{2^{64} - 1}{10^{\text{sharedDecimals}}} $$ In vanilla OFT implementations, `sharedDecimals = 6`, yielding a max supply of $$ \frac{2^{64} - 1}{10^6} = 18{,}446{,}744{,}073{,}709.551615 \text{ tokens} $$ If you choose a smaller `sharedDecimals`, the divisor shrinks and you may exceed the `uint64` limit when converting a large `amountLD` into `amountSD`. 2. **Local‐Unit Overflow (`amountLD`)**\ On some chains (e.g., Solana's SPL Token or Aptos's Fungible Asset), the native token amount is also stored as a 64-bit unsigned integer (`uint64`). In those environments, the maximum local amount is `2^64 − 1`. But because `amountLD` must be a multiple of $$ \text{decimalConversionRate} = 10^{(\text{localDecimals} - \text{sharedDecimals})} $$ If `amountSD × decimalConversionRate` would exceed `2^64 − 1`, the reconstructed `amountLD` cannot fit in the native `uint64` type. To avoid both overflow risks: * **Pick `sharedDecimals`** so that your target maximum supply divided by `10^{sharedDecimals}` is ≤ `2^64 − 1`. * **Verify each chain's local type** (e.g., `uint64` on Solana/Aptos or `uint256` on most EVM chains) can accommodate the resulting `amountLD` (i.e., `amountSD × decimalConversionRate` must not exceed the local limit). ### 2. Adapter vs. Direct Patterns: Contract Structure & Bridge Logic #### What Is "Direct" vs. "Adapter"? * **Direct Pattern** * The **token contract itself** contains all bridge logic (send/receive) along with standard token functions (mint, burn, transfer). * When a user initiates a crosschain transfer, the token contract on the source chain invokes internal "debit" logic to burn tokens, packages the message, and sends it through LayerZero. On the destination chain, the **same contract** (deployed there) receives the message and invokes internal "credit" logic to mint new tokens. * **Adapter Pattern** * The **token contract is separate** from the bridge logic. Instead of embedding send/receive in the token, an **adapter contract** handles all crosschain operations. * The adapter holds (locks) user tokens (or has burn and mint roles, e.g., "Mint and Burn Adapter") and communicates with a paired OFT contract on the destination chain, which mints/unlocks or transfers the equivalent amount to the recipient. * From the developer's perspective, the only requirement is that the adapter exists as a standalone contract; the original token contract remains unaware of LayerZero or crosschain flows. #### Key Distinctions * **Separate vs. Combined** * **Direct:** Token + bridge = single deployable. * **Adapter:** Token = unmodified existing contract; Bridge logic = standalone adapter contract. * **Mint And Burn Adapter Example** * Even though it uses mint/burn semantics, it is still an "Adapter" because the **adapter contract**, not the token contract itself, contains all LayerZero business logic. * The adapter delegates calls to mint or burn on a "wrapper" token or calls an interface on the underlying token, separating concerns without requiring the original token code to change. * **User/Integrator Perspective** * **No Difference in UX:** Users call a standard `send` function (or "transfer" wrapper) without caring whether the token is Direct or Adapter. * **Meshability:** Any two OFT-enabled contracts (Direct or Adapter) on different chains can interoperate. This means liquidity can span adapters and direct tokens seamlessly, making the system truly omnichain. #### Implications for Asset Issuers * **Direct Pattern Suits New Tokens** * When launching a brand-new token, embedding OFT logic directly can save on contract count and gas. * Simplifies deployment paths since your token and crosschain logic are co-located. * **Adapter Pattern Suits Existing Tokens** * If you already have an active ERC-20 (or SPL, or Move) token with liquidity and integrations, deploying an adapter contract lets you plug into OFT without migrating your token. * The adapter can implement **mint and burn**, **lock and unlock**, or any hybrid, as long as it abides by the OFT interface. * **Access Control & Governance** * **Direct Token:** You manage roles (Admin, Delegate) within a single contract. * **Adapter + Token:** You may need to coordinate roles and permissions across two deployables. **Fee-on-transfer and rebasing tokens are not supported.** The OFT `_debit` / `_credit` accounting assumes lossless ERC20 transfers (debit amount equals credit amount). To support such tokens, override `_debit` and `_credit` to reconcile against actual balance changes. ### 3. Extensibility & Composability OFT's design prioritizes flexibility and extensibility, allowing developers to customize token behavior and build complex crosschain applications. The standard provides hooks for custom logic and supports composable transfers that can trigger additional actions on the destination chain. #### Hooks Around Debit/Credit * **Beyond Value Transfer** * Many applications require extra functionality during or after crosschain value transfer for example: * **Protocol Fees:** Automatically deduct a small fee on each crosschain transfer and route it to a treasury. * **Rate Limiting:** Applying a limit on the number of tokens that can be sent in a given time-interval. * **Access Control:** Enforce time-based or role-based restrictions, such as requiring KYC verification for large transfers. * **Overrideable Functions** * OFT's core `_debit` and `_credit` methods are declared `virtual` (or their equivalent in non-EVM languages), allowing developers to override them in custom subclasses/modules. * Inject additional checks or side effects (e.g., take fees off transfers, check for rate limits, or validate off-chain context) without rewriting the entire message flow. #### Composability with LayerZero Messaging * **Crosschain Value Transfer + Call** * You can bundle **arbitrary data** with your OFT transfer. For example, trigger a staking action on the destination chain if a recipient stakes a minimum amount, or execute a crosschain governance vote. * The OFT contract simply forwards any extra bytes as a `composeMsg` through LayerZero's endpoint. On the destination, your custom `lzCompose(...)` hook can decode and act on that arbitrary data and token transfer. ## Security & Roles OFTs inherit LayerZero's admin/delegate role model: * **Owner** * Sets required gas limit requests for execution. * Can peer new OApp contracts or remove peers in emergencies. * **Delegate** * Configures connected chain's and messaging channel properties (e.g., Message Libraries, DVNs, and executors). * Can pause or unpause crosschain functionality in emergencies. > **Best Practice:** Use a multisig to manage both Owner and Delegate privileges. ## Further Reading * [EVM OFT Quickstart](../../developers/evm/oft/quickstart) A step-by-step guide to deploying Direct or Adapter OFT contracts on Ethereum-compatible networks. * [Solana OFT Quickstart](../../developers/solana/oft/overview) Detailed instructions and example code for setting up an OFT program with SPL Token / Token 2022 integration. * [Aptos Move OFT Quickstart](../../developers/aptos-move/contract-modules/oft) In-depth documentation on Move module structure, sharedDecimals math, and composability best practices. # Debugging Messages Source: https://docs.layerzero.network/v2/concepts/troubleshooting/debugging-messages Debug LayerZero crosschain messages. Track message lifecycle, verify delivery status, and troubleshoot common issues. Crosschain development with LayerZero... ## Message Lifecycle Every LayerZero message goes through the following high-level steps: * **Source Block Confirmations**: The message remains pending until the source chain finalizes the required number of block confirmations. This ensures that the transaction is securely committed on the source chain. **OFT atomicity:** For a standard OFT, the token debit (burn or lock) is atomic with the `PacketSent` event from the LayerZero Endpoint. Tokens cannot be debited without a corresponding message being emitted — if the send fails, the entire transaction reverts and no tokens are debited. On the destination side, the token credit (mint or unlock) is atomic with `PacketDelivered`. Custom OApps may implement non-atomic behavior in `_lzSend` or `_lzReceive`, but this is not the default OFT behavior. * **[DVN](../glossary#dvn-decentralized-verifier-network)/Verification**: Each Decentralized Verifier Network (DVN) independently verifies the message and submits an onchain transaction attesting to its validity. * **[Committer](../glossary#committer)/Commit Verification**: Once all required DVN attestations are available, a Committer submits a transaction to aggregate and commit these verifications on the destination chain. This step guarantees that the message has been sufficiently validated. * **[Executor](../glossary#executor)/Message Execution**: Finally, an Executor submits a transaction to deliver and execute the verified message on the destination chain. ## Debugging Messages using LayerZero Scan After a LayerZero message is successfully submitted on the source chain, it can be tracked using [LayerZero Scan](https://layerzeroscan.com). OApps can monitor the full message lifecycle, including delivery status and configuration details, directly through the [LayerZero Scan](../../tools/layerzeroscan/overview). For programmatic access, the [LayerZero Scan API](../../tools/layerzeroscan/api) lets you look up messages by transaction hash, OApp address, wallet address, status, pathwayId, GUID, and more. ## Message Statuses Overview Message status is an important indicator of what’s happening with your message. Always check the status first before diving deeper into debugging—it can save significant time. Below are the main statuses on LayerZero Scan: ### *Delivered* The message has been successfully sent and received by the destination chain. The **Delivered** status indicates that the `lzReceive` function was successfully invoked when the message arrived at the destination chain. However, in some cases, the subsequent `lzCompose` execution may fail. If there is a [Composer](../../developers/evm/composer/overview) implemented, review the `lzCompose` message status on LayerZero Scan and follow the provided instructions to [retry message](#retry-message). ### *Inflight* The message is waiting for source block confirmations, verification, or execution on the destination chain. * If DVN verification has not yet started, verify the number of block confirmations required on the source chain (configured in the receiveConfig). DVNs will only begin verification after the source transaction has reached the configured confirmation threshold. * If the required confirmations are reached but the message remains in an inflight state, the issue may fall into one of the following categories: * One or more DVNs have not yet submitted their verification for the message. * All DVNs have submitted verifications, but the Committer has not yet aggregated and committed them on the destination chain. * The Committer has successfully committed the verifications, but the Executor has not yet executed the message. * If a pathway has [Ordered Execution](../../tools/sdks/options#orderedexecution-option) enabled, a message cannot be executed until all preceding messages have been fully verified. Check the Message Execution Options in LayerZero Scan to confirm whether the ordered execution option is set to `true` and identify the first unverified message in the sequence, as subsequent messages will not be executed until it is verified. * At the stage of pending execution, execution can also be triggered manually by calling on the Endpoint's `lzReceive` function. This call is permissionless and can be initiated by anyone. Alternatively, LayerZero Scan provides a built-in option to execute the message directly through its interface. If the message remains inflight and is not delivered within the expected timeframe, contact [community support](https://discord.com/invite/ktbvm8Nkcr) for further assistance. ### *Failed* The message is delivered at destination chain but the message execution failed. LayerZero Scan displays any errors encountered during message execution. If the underlying issue can be resolved, the message can then be retried through the interface. See [Message Execution](#message-execution) for more details. ### *Blocked* The message is prevented from progressing due to configuration issues and requires manual intervention or updates to resolve. A "Blocked" message usually points to configuration issues: * **NotInitializable**: This status typically indicates that the destination OApp is either missing trusted peer settings or the pathway has not been properly initialized. Common causes: * **Incorrect peer configuration**: Ensure that `setPeer()` is correctly called on both the source and destination chains during deployment. Double-check that the address format and endpoint ID are accurate. * **Pathway not initialized correctly**: Confirm that `allowInitializePath()` is properly implemented in your OApp contract. Learn more: [allowInitializePath](../../tools/integration-checklist#set-peers-on-every-pathway). * **Dst OApp Not Found**: The receiver is not a valid contract. * **DVN Mismatch**: All DVN providers must be the same on source and destination. See [DVN Mismatch](../../developers/evm/configuration/dvn-executor-config#dvn-mismatch) for more details. * **Dead DVN**: This configuration includes a Dead DVN. See [Dead DVN](../../developers/evm/configuration/dvn-executor-config#dead-dvn) for more details. * **Block Confirmations Mismatch**: Outbound confirmations must be ≥ inbound confirmations. See [Block Confirmation Mismatch](../../developers/evm/configuration/dvn-executor-config#block-confirmation-mismatch) for more details. ### *Confirming* The Executor has submitted the destination transaction and the system is waiting for it to reach finality on the destination chain. This is a transitional state before the message is marked as `Delivered`. ### *Malformed Command* The command is malformed. The status is only applied to lzRead message. To debug, see [Debugging Malformed or Unresolvable Commands](../../developers/evm/lzread/read-cli#debugging-malformed-or-unresolvable-commands) for more details. ### *Unresolvable Command* The command is unresolvable. This status is only applied to lzRead message. To debug, see [Debugging Malformed or Unresolvable Commands](../../developers/evm/lzread/read-cli#debugging-malformed-or-unresolvable-commands) for more details. For Malformed Command and Unresolvable Command, an OApp must call `skip()` to unblock the message pathway. If `skip()` is not invoked, subsequent messages will not be delivered. See [Skipping Nonce](../../developers/evm/troubleshooting/debugging-messages#skipping-nonce) for more details. To troubleshoot common errors in `lzRead` messages, See [debugging](../../developers/evm/lzread/overview#debugging) for more details. ### *SIMULATION\_REVERTED* This status can be found in the LayerZero Scan API as a sub status inside the `destination` section, indicating the `lzReceive` or `lzCompose` has failed on the destination chain. ## General Debugging Steps ### If the message was not sent successfully #### Quick triage * Confirm the transaction: * Did the source chain transaction finalize? (Check explorer receipt status and logs.) * Look for packet emission * Verify whether the expected LayerZero “PacketSent” event is emitted on the source chain. * Capture context: * Source and destiantion chain * OApp addresses * send params * DVN and Exeutor Configs #### Identify the revert / error trace Run a trace (Foundry/Tenderly/Trace on explorer) and map to the failing contract and error codes. Common errors (causes & fixes): `Please set your OApp's DVNs and/or Executor` * Cause: This error occurs during `getFee`, indicating your OApp configuration is missing the required DVN and/or Executor settings. * Fix: Set valid DVNs and/or executor in the OApp configs. `InsufficientFee()` * Cause: `required.nativeFee` > `suppliedNativeFee` or `required.lzTokenFee` > `suppliedLzTokenFee`; or `msg.value` lower than the quoted amount. * Fix: Call the quote function first, pass the exact fee, and forward enough msg.value. `NativeAmountExceedsCap()` * Cause: Requested native drop on destination exceeds the configured native drop cap. * Fix: Reduce requested airdrop amount in options or raise the cap in the Executor/destination config (owner action). `InvalidWorkerOptions()` * Cause: Worker options is malformed. * Fix: Rebuild options via the Options Builders. `Unauthorized()` * Cause: This error normally occurs at the wiring step. The call is not made by the OApp or an approved delegate. * Fix: Use the permissioned wallet to sign the transactions. `Unsorted()` * Cause: DVNs array contains duplicates or is not strictly sorted. * Fix: Deduplicate and sort DVN addresses deterministically before passing; keep canonical order in code. `UnsupportedEid()` * Cause: The pathway is not connected. * Fix: Use the correct destination EID, verify chain mapping, and contact the support team if a pathway is not wired. #### Configuration & connectivity checklist * **Delegate & Ownership** * Verify the owner and delegate address * [Understand their respective permissions](../../faq#whats-the-difference-between-delegate-and-owner) * **Peers**: * peers are set on both source and destination chain * addresses & EIDs match, and in correct format * **Message Libraries**: * `sendLibrary` and `receiveLibrary` are set to expected addresses/versions. * **DVNs**: * DVN provider set(s) exist * Identical provider(s) on source and destination * Contain no LZ Dead DVNs unless intended * **Executor**: * Executor address is set as intended * Message size doesn't exceed Executor limit * Native drop amount doesn't exceed cap * **Message Exeuction Options**: * Ensure enforcedOptions and/or extraOptions is present; * Profiling destination gas for lzReceive and/or lzCompose to determine the gas units applied in the message execution options * **Connected pathways**: * Confirm whether a pathway is fully connected If these pass but the `send` still fails, simulate the send with the same params and use the error trace to narrow the root cause. ### If the message was sent Now the message is visible on LayerZero Scan. Use Scan to locate the message and walk the lifecycle. * Get transaction hash on the source chain * Start with [Message Status](#message-statuses-overview) on Scan * Statuses map directly to lifecycle stages and tell you where to focus first: * Identify which stage the message is at (decision tree) * No DVN confirmations yet? * Check source confirmations vs. threshold; verify DVN set correctly. * DVNs verified, but not committed? * Check Executor config and contact support team. * Committed, but not Executed? * If it is `orderedExecution`, inspect the first unverified prior message. * Inspect revert transaction and revert reason * Fix root cause * Retry the message * [Retry messages on EVM](../../developers/evm/troubleshooting/debugging-messages#retry-message) * [Retry messages on Solana](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oapp-solana/tasks/solana/retryPayload.ts) * `lzReceive` succeeded but `lzCompose` failed? * Inspect `lzcompose` revert reason * retry `lzCompose`. ### Retrieve Retry Parameters via LayerZero Scan API Use the LayerZero Scan API to fetch a message bundle by source tx hash and inspect the destination execution. If execution failed, the destination section includes failedTx, which typically points to an Executor alert call (e.g., `lzReceiveAlert` or `lzComposeAlert`). The alert transaction’s call data contains the parameters required to retry `lzReceive` or `lzCompose`. #### Endpoint Base URL: `https://scan.layerzero-api.com/v1`\ Method: `GET` `/messages/tx/{tx}` `tx`: source-chain transaction hash (hex string) #### Response Shape Each response returns a `data` array of message objects: * pathway: source/destination networks and EIDs, sender/receiver address, application information * source: transaction details and status on source chain * verification: DVN verification transactions and the committer/sealer transaction for committing verifications * destination: execution status on the target chain (including failed txs) * config: ULN configurations (Confirmations, DVNs, Executor) in effect for this pathway * status: overall message status * guid, created timestamps and updated timestamp In the API response, look for `failedTx` for the transactions that contains the eror for `lzReceive` message. Examine the `revertReason` in the destination section to identify the root cause. API Response Example in the destination section: ```json wrap theme={null} "destination": { "nativeDrop": { "status": "N/A" }, "lzCompose": { "status": "N/A" }, "failedTx": [ { "txHash": "0xa6cf8347a8679866955fbf83175ccc3191f592c27865e89ce69bf99d71542b53", "txError": "CouldNotParseError(string) 0x", "blockHash": "0xa3130ed1fbb60b45bd99638a07f415892118442e72d5be6eda58214a6a41c610", "blockNumber": 23266956, "revertReason": "0x" } ], "status": "SIMULATION_REVERTED" } ``` #### How to get retry parameters * Call `GET /messages/tx/{tx}` with the source tx hash. In `data[0].destination.failedTx`, take the `txHash` (usually an `lzReceiveAlert` or `lzComposeAlert` transaction that the executor called to signal the failure). * Fetch that destination transaction and inspect. * If revertReason is empty (0x), it commonly indicates out-of-gas or a contract-level revert without a reason string. * For custom error, decode the selector using [4byte directory](https://www.4byte.directory/). * Function selector & args of the alert call input; it embeds everything needed to re-invoke lzReceive/lzCompose * Alternatively, all the information can also be retrieved directly from the scan API. #### Skip/Clear/Burn/Nilify **`skip`**: Called by the receiver to skip verification and delivery of a nonce. **`clear`**: Called by the receiver to skip a nonce that has been verified. **`nilify`**: Called by the receiver to temporarily invalidate a nonce. `nilify` can be used to proactively invalidate maliciously generated packets from compromised DVNs. Message can be re-executed. **`burn`**: Called by the receiver to delete and skip a nonce. `burn` can be used if a faulty Security Stack commits an invalid hash to the endpoint, or if an OApp needs to clear a nilified nonce. Message can not be re-executed. See: [Skip/Clear/Burn/Nilify on EVM](../../developers/evm/troubleshooting/debugging-messages#skipping-nonce), [Skip/Clear/Burn/Nilify on Solana](../../tools/sdks/solana-sdk#skip-a-message), and [Skip/Clear/Burn/Nilify on Stellar](../../developers/stellar/troubleshooting/debugging-messages) for full semantics and usage. # Value Transfer Implementations Source: https://docs.layerzero.network/v2/concepts/value-transfer-implementations Value Transfer is specialized messaging with token-specific invariants and settlement logic. Building on Module 1's value transfer concepts and Module 5's... **Value Transfer** is specialized messaging with token-specific invariants and settlement logic. Building on [Module 1's](./interoperability-foundations) value transfer concepts and [Module 5's](./application-design-patterns) messaging patterns, LayerZero provides multiple approaches for implementing crosschain asset movement. ## Value Transfer as OApp Messaging Value transfer uses the same LayerZero messaging for moving data, but adds token-specific invariants as part of the OApp's business logic: ```solidity wrap theme={null} // OFT: sends tokens with invariant logic function send(SendParam memory params) external { // 1. Debit tokens locally (burn or lock) (uint256 sent, uint256 received) = _debit(msg.sender, params.amountLD, params.dstEid); // 2. Send message with token data bytes memory message = OFTMsgCodec.encode(params.to, _toSD(received), params.composeMsg); //highlight-next-line _lzSend(params.dstEid, message, options, fee, refundAddress); } // On destination: credit tokens // highlight-next-line function _lzReceive(..., bytes calldata message, ...) internal override { address to = message.sendTo().bytes32ToAddress(); uint256 amount = _toLD(message.amountSD()); // 3. Credit tokens on destination (mint or unlock) _credit(to, amount, origin.srcEid); } ``` **Token-Specific Requirements**: * **Supply integrity**: Total supply preserved across all chains * **Atomic debit/credit**: Local debit occurs immediately, remote credit on message delivery * **Decimal handling**: Consistent precision across chains with different decimal systems * **Invariant enforcement**: Token-specific rules (rate limits, permissions, etc.) ## Value Transfer Implementation Approaches Both approaches represent value transfer requests via LayerZero messages, with different settlement mechanisms: ### 1. Direct Token Messaging (Omnichain Fungible Token (OFT)) ```mermaid wrap theme={null} graph LR subgraph "Chain A" USER[User] OFT_A[OFT Contract
Debit tokens] end subgraph "Chain B" OFT_B[OFT Contract
Credit tokens] RECEIVER[Receiver] end USER -->|"Transfer request"| OFT_A OFT_A -->|"LayerZero Message
Value transfer data"| OFT_B OFT_B --> RECEIVER ```
**Settlement**: Direct token operations (burn/mint or lock/unlock) via LayerZero messaging ### 2. Pool-Based Settlement (Stargate) ```mermaid wrap theme={null} graph LR subgraph "Chain A" USER[User] POOL_A[Stargate Pool
Deep native liquidity] end subgraph "Chain B" POOL_B[Stargate Pool
Deep native liquidity] RECEIVER[Receiver] end USER -->|"Transfer request"| POOL_A POOL_A -->|"LayerZero Message
Pool settlement data"| POOL_B POOL_B --> RECEIVER ```
**Settlement**: Pool-to-pool coordination via LayerZero messaging with credit guarantees ### 3. Hybrid Settlement (Stargate Hydra OFT) ```mermaid wrap theme={null} graph LR subgraph "Chain A" USER[User] POOL_A[Stargate Pool
Native USDC liquidity] end subgraph "Chain B" HYDRA_OFT[Hydra OFT
USDC.e representation] RECEIVER[Receiver] end USER -->|"Transfer request"| POOL_A POOL_A -->|"LayerZero Message
Value transfer data"| HYDRA_OFT HYDRA_OFT --> RECEIVER ```
**Settlement**: Pool locks native assets on source, OFT mints representations on destination *** While all these settlement types are similar in nature, they have different guarantees, trust assumptions, and target users depending on who the asset issuer is (token issuer, versus chain, versus DeFi user). The implementation details of moving value - and the control mechanisms involved - determine which approach is appropriate for specific use cases. ## OFT: Omnichain Fungible Token Standard OFT extends the OApp standard with token-specific debit and credit logic. The [OFTCore contract](https://github.com/LayerZero-Labs/devtools/blob/de203c58a2064092d1f8fc141c777cb44b8db76b/packages/oft-evm/contracts/OFTCore.sol#L183-L222) implements the standard OApp `_lzSend` and `_lzReceive` functions with token invariant enforcement: ```mermaid wrap theme={null} graph LR subgraph "Ethereum" SENDER["OFT A"] ENDPOINT_A["LayerZero
Endpoint"] end subgraph "Arbitrum" RECEIVER_B["OFT B"] ENDPOINT_B["LayerZero
Endpoint"] end subgraph "Polygon" RECEIVER_C["OFT C"] ENDPOINT_C["LayerZero
Endpoint"] end subgraph "Base" RECEIVER_D["OFT D"] ENDPOINT_D["LayerZero
Endpoint"] end SENDER --> ENDPOINT_A ENDPOINT_A -->|"Channel 1: Token Transfer"| ENDPOINT_B ENDPOINT_A -->|"Channel 2: Token Transfer"| ENDPOINT_C ENDPOINT_A -->|"Channel 3: Token Transfer"| ENDPOINT_D ENDPOINT_B --> RECEIVER_B ENDPOINT_C --> RECEIVER_C ENDPOINT_D --> RECEIVER_D ```
**Key Insight**: OFT is not a separate protocol - it's the OApp standard with standardized `_debit` and `_credit` abstractions for token handling. ## How OFT Messaging Works OFT messaging follows a precise flow that combines token operations with LayerZero messaging: ```mermaid wrap theme={null} graph LR subgraph "Source Chain" USER["User calls send()"] DEBIT["_debit() + OFTMsgCodec.encode()
Burn/Lock tokens + Message encoding"] SEND["_lzSend()
LayerZero messaging"] end subgraph "Destination Chain" RECEIVE["_lzReceive()
LayerZero messaging"] CREDIT["OFTMsgCodec.decode() + _credit()
Message decoding + Mint/Unlock tokens"] COMPLETE["Tokens delivered"] end USER --> DEBIT DEBIT --> SEND SEND --> RECEIVE RECEIVE --> CREDIT CREDIT --> COMPLETE ```
**OFT Message Flow**: 1. **Token Debit**: `_debit()` burns or locks tokens locally 2. **Message Encoding**: `OFTMsgCodec.encode()` creates standardized token message 3. **LayerZero Send**: `_lzSend()` dispatches message via LayerZero protocol 4. **Verification & Delivery**: DVNs verify, Executors deliver (standard LayerZero flow) 5. **Message Decoding**: `OFTMsgCodec.decode()` extracts token data from message 6. **Token Credit**: `_credit()` mints or unlocks tokens on destination ### Why OFT is Popular OFT is simply an interface for value transfer that runs on LayerZero messaging rails. However, as outlined in [Module 3](./interface-coupling-problems), asset issuers have the liberty to define what those rails look like and how they're governed. **Core Benefits**: 1. **Unified Supply**: One token, many chains, consistent total supply across all deployments 2. **No Wrapped Tokens**: Native representation on each chain without bridging artifacts 3. **Direct Transfer Model**: Direct chain-to-chain token transfers without intermediate tokens 4. **Composable**: Send tokens with any message to any address, enabling complex workflows **Governance Flexibility** (from modular architecture): 5. **Custom Security Models**: Asset issuers choose their own DVN configurations per pathway 6. **Configurable Execution**: Define gas settings, retry policies, and delivery guarantees per route 7. **Configurable Pathways**: Change security assumptions without redeploying token contracts 8. **Mixed Trust Models**: Use different verification approaches for different chains based on risk tolerance ## Token Invariants OFT implementations must maintain specific invariants to ensure safe crosschain value transfer: ### Debit/Credit Operations In OFT implementations, value transfer operates through debit/credit operations: * **Debit**: Remove tokens from source chain (burn or lock) * **Credit**: Add tokens to destination chain (mint or unlock) ### Critical Supply Invariants In a global LayerZero token mesh, you can ONLY use these debit/credit combinations: * **Debit lock → Credit mint** (lockbox to minter) * **Debit burn → Credit mint** (burner to minter) * **Debit burn → Credit unlock** (burner to lockbox) **NEVER use**: Debit lock → Credit unlock, as tokens locked in escrow INSIDE the OFT contract cannot guarantee delivery without credit planning. ### Token Decimal Normalization Different blockchains use different decimal precision for representing tokens. For example, EVM chains typically use 18 decimals while Solana often uses 6 or 9 decimals. OFT handles this through a two-tier decimal system: * **Local Decimals**: The native precision of the token on each specific chain * **Shared Decimals**: The normalized precision used in LayerZero messages The conversion follows a mathematical formula where the decimal conversion rate is: $$ \text{decimalConversionRate} = 10^{(\text{localDecimals} - \text{sharedDecimals})} $$ **The Precision Floor Problem**: Different blockchains use different semantics and variable types for storing token balances. EVM chains use `uint256` (unlimited precision), while Solana uses `uint64` (limited precision). Without normalization, a token transfer could succeed on the source chain but fail on the destination due to precision overflow or underflow. **Shared Decimals Solution**: By setting a common precision floor (default 6 decimals), OFT ensures that any amount that can be represented in the shared format will work on all connected chains, regardless of their native decimal systems or storage limitations. **Practical Impact**: With `sharedDecimals = 6`, the minimum crosschain transfer is **0.000001 tokens**. This precision floor prevents issues where: * High-precision chains (18 decimals) send amounts too small for low-precision chains (6 decimals) * Variable type mismatches cause overflow when converting between chain formats * Dust accumulation from repeated conversions leads to accounting errors To transfer smaller amounts than 0.000001 tokens, you would need to increase `sharedDecimals` to a higher precision, but this must be consistent across all chains in your OFT deployment. **Benefits**: * **Crosschain Consistency**: Same economic value regardless of chain decimal differences * **Automatic Conversion**: Developers don't need to handle decimal math manually * **Dust Protection**: Prevents precision loss through automatic dust removal For detailed technical implementation including overflow protection and mathematical formulas, see the [OFT Technical Reference](./technical-reference/oft-reference). ## Architecture Patterns How token contracts and bridge logic are organized: ### Cross-VM Compatibility While the examples below show EVM Solidity implementations, these same patterns apply to all LayerZero-supported blockchains including Solana, Aptos, and other VMs. The core concepts of debit/credit operations, supply invariants, and decimal normalization remain consistent across all virtual machine environments. ### OFT Self (Integrated Token + Bridge) **Architecture**: Single contract combines ERC20 token functionality with LayerZero bridge logic - the token contract IS the bridge contract. ```mermaid wrap theme={null} graph LR subgraph "Ethereum" SENDER["OFT A
Token + Bridge
_debit: burn"] ENDPOINT_A["LayerZero
Endpoint"] end subgraph "Arbitrum" RECEIVER_B["OFT B
Token + Bridge
_credit: mint"] ENDPOINT_B["LayerZero
Endpoint"] end subgraph "Polygon" RECEIVER_C["OFT C
Token + Bridge
_credit: mint"] ENDPOINT_C["LayerZero
Endpoint"] end subgraph "Base" RECEIVER_D["OFT D
Token + Bridge
_credit: mint"] ENDPOINT_D["LayerZero
Endpoint"] end SENDER --> ENDPOINT_A ENDPOINT_A -->|"Channel 1: Token Transfer"| ENDPOINT_B ENDPOINT_A -->|"Channel 2: Token Transfer"| ENDPOINT_C ENDPOINT_A -->|"Channel 3: Token Transfer"| ENDPOINT_D ENDPOINT_B --> RECEIVER_B ENDPOINT_C --> RECEIVER_C ENDPOINT_D --> RECEIVER_D ```
**Characteristics**: * **Contract Structure**: Single contract per chain containing both token and bridge logic * **Deployment**: Must be deployed on every chain where tokens will exist * **Control Requirements**: Must control minting authority on all deployment chains * **Retrofitting**: Cannot be added to existing tokens without code modifications **Use Cases**: New protocol tokens, governance tokens, tokens designed for omnichain deployment from inception **Implementation**: Uses burn/mint operations internally (detailed in Implementation Patterns section below) ### OFT Adapter (Separate Token + Bridge) **Architecture**: Separate bridge contract handles LayerZero messaging while existing token contracts remain unchanged - the token contract is NOT the bridge contract. ```mermaid wrap theme={null} graph LR subgraph "Ethereum" SENDER["OFT Adapter A
_debit: lock"] TOKEN_A["Token A
Existing ERC20"] ENDPOINT_A["LayerZero
Endpoint"] SENDER -.-> TOKEN_A end subgraph "Arbitrum" RECEIVER_B["OFT Adapter B
_credit: unlock"] TOKEN_B["Token B
Existing ERC20"] ENDPOINT_B["LayerZero
Endpoint"] RECEIVER_B -.-> TOKEN_B end subgraph "Polygon" RECEIVER_C["OFT Adapter C
_credit: unlock"] TOKEN_C["Token C
Existing ERC20"] ENDPOINT_C["LayerZero
Endpoint"] RECEIVER_C -.-> TOKEN_C end subgraph "Base" RECEIVER_D["OFT Adapter D
_credit: unlock"] TOKEN_D["Token D
Existing ERC20"] ENDPOINT_D["LayerZero
Endpoint"] RECEIVER_D -.-> TOKEN_D end SENDER --> ENDPOINT_A ENDPOINT_A -->|"Channel 1: Token Transfer"| ENDPOINT_B ENDPOINT_A -->|"Channel 2: Token Transfer"| ENDPOINT_C ENDPOINT_A -->|"Channel 3: Token Transfer"| ENDPOINT_D ENDPOINT_B --> RECEIVER_B ENDPOINT_C --> RECEIVER_C ENDPOINT_D --> RECEIVER_D ```
**Characteristics**: * **Contract Structure**: Separate bridge contract per chain, existing token contracts unchanged * **Deployment**: Bridge contracts deployed alongside existing token deployments * **Control Requirements**: Bridge must have appropriate permissions on token contract * **Retrofitting**: Can be added to existing tokens without modifying token code **Use Cases**: Existing tokens that cannot be modified, established tokens with existing ecosystems **Implementation**: Can use various patterns - lock/unlock, mint/burn, or hybrid approaches (detailed in Implementation Patterns section below) ## Implementation Patterns The technical mechanisms used to transfer value across chains, regardless of architecture: ### Burn/Mint Pattern **Debit/Credit Invariant**: Debit burn → Credit mint **Architecture Compatibility**: * **OFT Self**: Built-in burn/mint operations * **OFT Adapter**: Via `MintBurnOFTAdapter` with `IMintableBurnable` interface **OFT Self Implementation**: Single contracts with integrated burn/mint operations: ```mermaid wrap theme={null} graph LR subgraph "Ethereum" SENDER["OFT A
_debit: burn
-100 tokens"] ENDPOINT_A["LayerZero
Endpoint"] end subgraph "Arbitrum" RECEIVER_B["OFT B
_credit: mint
+100 tokens"] ENDPOINT_B["LayerZero
Endpoint"] end subgraph "Polygon" RECEIVER_C["OFT C
_credit: mint
+100 tokens"] ENDPOINT_C["LayerZero
Endpoint"] end subgraph "Base" RECEIVER_D["OFT D
_credit: mint
+100 tokens"] ENDPOINT_D["LayerZero
Endpoint"] end SENDER --> ENDPOINT_A ENDPOINT_A -->|"Channel 1: Token Transfer"| ENDPOINT_B ENDPOINT_A -->|"Channel 2: Token Transfer"| ENDPOINT_C ENDPOINT_A -->|"Channel 3: Token Transfer"| ENDPOINT_D ENDPOINT_B --> RECEIVER_B ENDPOINT_C --> RECEIVER_C ENDPOINT_D --> RECEIVER_D ```
**MintBurn OFT Adapter Implementation**: Separate bridge contracts with mint/burn permissions on existing tokens: ```mermaid wrap theme={null} graph LR subgraph "Ethereum" SENDER["MintBurn OFT Adapter A
_debit: burn"] TOKEN_A["Token A
Mintable/Burnable
ERC20"] ENDPOINT_A["LayerZero
Endpoint"] SENDER -.-> TOKEN_A end subgraph "Arbitrum" RECEIVER_B["MintBurn OFT Adapter B
_credit: mint"] TOKEN_B["Token B
Mintable/Burnable
ERC20"] ENDPOINT_B["LayerZero
Endpoint"] RECEIVER_B -.-> TOKEN_B end subgraph "Polygon" RECEIVER_C["MintBurn OFT Adapter C
_credit: mint"] TOKEN_C["Token C
Mintable/Burnable
ERC20"] ENDPOINT_C["LayerZero
Endpoint"] RECEIVER_C -.-> TOKEN_C end subgraph "Base" RECEIVER_D["MintBurn OFT Adapter D
_credit: mint"] TOKEN_D["Token D
Mintable/Burnable
ERC20"] ENDPOINT_D["LayerZero
Endpoint"] RECEIVER_D -.-> TOKEN_D end SENDER --> ENDPOINT_A ENDPOINT_A -->|"Channel 1: Token Transfer"| ENDPOINT_B ENDPOINT_A -->|"Channel 2: Token Transfer"| ENDPOINT_C ENDPOINT_A -->|"Channel 3: Token Transfer"| ENDPOINT_D ENDPOINT_B --> RECEIVER_B ENDPOINT_C --> RECEIVER_C ENDPOINT_D --> RECEIVER_D ```
**Characteristics**: * **Supply Invariant**: Global supply remains constant (burn on source = mint on destination) * **Capital Efficiency**: No locked capital requirements * **Approval Required**: Varies by architecture (OFT Self: false, OFT Adapter: true) * **Requirements**: Mint/burn authority or `IMintableBurnable` interface implementation **Use Cases**: New tokens, tokens with existing mint/burn infrastructure ### Lock/Unlock Pattern **Debit/Credit Invariant**: Debit lock → Credit mint (forward) / Debit burn → Credit unlock (reverse) **Architecture Compatibility**: * **OFT Self**: Not applicable (uses burn/mint) * **OFT Adapter**: Via standard `OFTAdapter` with `safeTransfer` operations **Forward Direction (Lock/Mint)**: Tokens locked on source chain, minted on destination chains: ```mermaid wrap theme={null} graph LR subgraph "Ethereum" SENDER["OFT Adapter A
_debit: lock
100 tokens"] ENDPOINT_A["LayerZero
Endpoint"] end subgraph "Arbitrum" RECEIVER_B["OFT B
_credit: mint
+100 tokens"] ENDPOINT_B["LayerZero
Endpoint"] end subgraph "Polygon" RECEIVER_C["OFT C
_credit: mint
+100 tokens"] ENDPOINT_C["LayerZero
Endpoint"] end subgraph "Base" RECEIVER_D["OFT D
_credit: mint
+100 tokens"] ENDPOINT_D["LayerZero
Endpoint"] end SENDER --> ENDPOINT_A ENDPOINT_A -->|"Channel 1: Token Transfer"| ENDPOINT_B ENDPOINT_A -->|"Channel 2: Token Transfer"| ENDPOINT_C ENDPOINT_A -->|"Channel 3: Token Transfer"| ENDPOINT_D ENDPOINT_B --> RECEIVER_B ENDPOINT_C --> RECEIVER_C ENDPOINT_D --> RECEIVER_D ``` **Reverse Direction (Burn/Unlock)**: Tokens burned on destination chains, unlocked from escrow on source chain: ```mermaid wrap theme={null} graph LR subgraph "Arbitrum" SENDER_B["OFT B
_debit: burn
-100 tokens"] ENDPOINT_B["LayerZero
Endpoint"] end subgraph "Ethereum" RECEIVER_A["OFT Adapter A
_credit: unlock
100 tokens"] ENDPOINT_A["LayerZero
Endpoint"] end SENDER_B --> ENDPOINT_B ENDPOINT_B -->|"Token Transfer Back"| ENDPOINT_A ENDPOINT_A --> RECEIVER_A ```
**Characteristics**: * **Supply Invariant**: Global supply increases in forward direction (locked + minted), decreases in reverse (burned, unlocked) * **Capital Efficiency**: Requires reserves on source chain only * **Approval Required**: True on source chain (for locking) * **Requirements**: Mint authority on destination chains **Recommended Architecture**: Deploy Adapter on one chain, OFT Self contracts everywhere else. This avoids permission requirements while enabling omnichain functionality. **Use Cases**: Existing major tokens that need to connect to omnichain ecosystems ### Native Asset Pattern **Debit/Credit Invariant**: Debit lock → Credit mint (forward) / Debit burn → Credit unlock (reverse) **Architecture Compatibility**: * **OFT Self**: Not applicable * **OFT Adapter**: Via `NativeOFTAdapter` with `msg.value` operations **Forward Direction (Lock/Mint)**: Native tokens locked on source chain, wrapped tokens minted on destination chains: ```mermaid wrap theme={null} graph LR subgraph "Ethereum" SENDER["Native OFT Adapter A
_debit: lock
1 ETH"] ENDPOINT_A["LayerZero
Endpoint"] end subgraph "Arbitrum" RECEIVER_B["OFT B
_credit: mint
+1 WETH"] ENDPOINT_B["LayerZero
Endpoint"] end subgraph "Polygon" RECEIVER_C["OFT C
_credit: mint
+1 WETH"] ENDPOINT_C["LayerZero
Endpoint"] end subgraph "Base" RECEIVER_D["OFT D
_credit: mint
+1 WETH"] ENDPOINT_D["LayerZero
Endpoint"] end SENDER --> ENDPOINT_A ENDPOINT_A -->|"Channel 1: Token Transfer"| ENDPOINT_B ENDPOINT_A -->|"Channel 2: Token Transfer"| ENDPOINT_C ENDPOINT_A -->|"Channel 3: Token Transfer"| ENDPOINT_D ENDPOINT_B --> RECEIVER_B ENDPOINT_C --> RECEIVER_C ENDPOINT_D --> RECEIVER_D ``` **Reverse Direction (Burn/Unlock)**: Wrapped tokens burned on destination chains, native tokens unlocked on source chain: ```mermaid wrap theme={null} graph LR subgraph "Arbitrum" SENDER_B["OFT B
_debit: burn
-1 WETH"] ENDPOINT_B["LayerZero
Endpoint"] end subgraph "Ethereum" RECEIVER_A["Native OFT Adapter A
_credit: unlock
1 ETH"] ENDPOINT_A["LayerZero
Endpoint"] end SENDER_B --> ENDPOINT_B ENDPOINT_B -->|"Token Transfer Back"| ENDPOINT_A ENDPOINT_A --> RECEIVER_A ```
**Characteristics**: * **Supply Invariant**: Native tokens locked in contract balances * **Capital Efficiency**: Requires native token reserves on destination chains * **Approval Required**: False (native tokens sent via `msg.value`) * **Requirements**: Contract must handle native asset transfers properly **Important Note**: This pattern is intended for **canonical asset issuers** (e.g., Ethereum Foundation for ETH, Polygon Labs for MATIC) who want to enable their native token on other chains. This is NOT for development teams who want to bridge an existing native asset they don't control to their destination chain. **Use Cases**: Native token bridging by canonical issuers (ETH by Ethereum Foundation, MATIC by Polygon Labs, AVAX by Avalanche Foundation) ## Implementation Guidelines ### Approach Selection **OFT Approach**: Direct token messaging provides unified supply management and composability with other OApps. Token delivery depends on LayerZero message finality, requiring consideration of crosschain latency and destination gas costs. Mint authority requirements limit this approach to new tokens or tokens with controllable minting. **Stargate Approach**: Pool-based settlement provides deep liquidity for established native assets through coordinated pool networks built on LayerZero V2. Settlement depends on LayerZero messaging between pools, with the Hydra mechanism extending liquidity to emerging chains via minted OFTs. Requires liquidity provision and may involve pool-based fees and slippage. See [Stargate Finance](/v2/concepts/applications/stargate-finance) for architecture details. ### Technical Implementation **Decimal Handling**: OFT implements automatic decimal conversion between local and shared decimals across chains. Use the built-in `sharedDecimals()` system rather than custom implementations. **Supply Monitoring**: Track total supply across all chains to ensure supply invariants are maintained. Implement monitoring for supply discrepancies and reconciliation mechanisms. **Gas Planning**: Account for destination chain execution costs in your execution options. Different chains have varying gas costs and mechanisms that affect total transfer costs. **Pattern Selection**: Choose burn/mint patterns for new tokens requiring unified supply. Choose lock/mint patterns for existing tokens requiring omnichain expansion. Use pool-based approaches for high-liquidity native assets. ## See Also * [OFT Quickstart](/v2/developers/evm/oft/quickstart) - Deploy your first OFT * [Stargate Finance](/v2/concepts/applications/stargate-finance) - Liquidity protocol built on LayerZero * [Stargate Documentation](https://docs.stargate.finance) - Full Stargate protocol reference * [OFT Standard](./applications/oft-standard) - Technical specification * [Glossary](./glossary) - Complete terminology reference # LayerZero Worker Services Source: https://docs.layerzero.network/v2/concepts/verification-execution-services LayerZero's separation of verification and execution into independent worker services enables configurable security models and permissionless message... LayerZero's separation of verification and execution into independent worker services enables configurable security models and permissionless message delivery. Worker services are off-chain infrastructure that Message Libraries coordinate to verify and deliver crosschain messages while following onchain rules. ## Two Types of Workers ### DVNs (Decentralized Verifier Networks) **DVNs** are LayerZero's implementation of the "verifier networks" discussed in previous modules. Each DVN is an independent verification service that implements one of the verification approaches from [Module 1](./interoperability-foundations) (ZK proofs, committee consensus, light clients, etc.). ### Executors **Executors** are permissionless services that deliver verified messages to destination chains. They compete to provide fast, reliable message delivery while following execution parameters set by Message Libraries. ## DVN Architecture & Implementation DVNs prove message authenticity according to Message Library rules and fit into the X-of-Y-of-N configuration model: ```mermaid wrap theme={null} graph LR subgraph "Chain A" SENDER["Sender OApp"] ENDPOINT_A["LayerZero
Endpoint"] LIB_A["SendLib"] end subgraph "Verification Layer" DVN1["DVN 1
ZK Proofs"] DVN2["DVN 2
Committee A"] DVN3["DVN 3
Committee B"] DVN4["DVN 4
Middlechain"] DVN5["DVN 5
Native Bridge"] DVN6["DVN 6
Custom"] end subgraph "Chain B" LIB_B["ReceiveLib"] ENDPOINT_B["LayerZero
Endpoint"] RECEIVER_B["Receiver OApp"] end subgraph "Config" CONFIG["X-of-Y-of-N
Required: DVN1, DVN2
Optional: DVN3, DVN4, DVN5, DVN6
Threshold: 2-of-4-of-6"] end SENDER --> ENDPOINT_A ENDPOINT_A --> LIB_A LIB_A --> DVN1 LIB_A --> DVN2 LIB_A --> DVN3 LIB_A --> DVN4 LIB_A --> DVN5 LIB_A --> DVN6 DVN1 --> LIB_B DVN2 --> LIB_B DVN3 --> LIB_B DVN4 --> LIB_B DVN5 --> LIB_B DVN6 --> LIB_B LIB_B --> ENDPOINT_B ENDPOINT_B --> RECEIVER_B CONFIG -.-> LIB_A CONFIG -.-> LIB_B ``` ### DVN Implementation Examples **DVN 1 (ZK Proofs)**: Uses zero-knowledge cryptography for mathematical verification **DVN 2 (Committee A)**: Uses multi-signature consensus from validator set A **DVN 3 (Committee B)**: Uses multi-signature consensus from validator set B **DVN 4 (Middlechain)**: Uses shared security from intermediate consensus layer **DVN 5 (Native Bridge)**: Uses existing chain-to-chain sequencer bridge infrastructure for verification **DVN 6 (Custom)**: Uses specialized verification logic for specific use cases **X-of-Y-of-N in Practice**: The configuration shows a **2-of-4-of-6** setup where DVN1 and DVN2 are required, and any 2 of the 4 optional DVNs (DVN3, DVN4, DVN5, DVN6) must also verify. ### DVN Configuration ```solidity wrap theme={null} // Configure DVNs per pathway (matching our 2-of-4-of-6 example above) SetConfigParam[] memory params = new SetConfigParam[](1); params[0] = SetConfigParam({ eid: remoteEid, // The remote chain configType: 2, // 2 = ULN config config: abi.encode( UlnConfig({ confirmations: 15, requiredDVNCount: 2, // DVN1 (ZK) + DVN2 (Committee A) optionalDVNCount: 4, // DVN3, DVN4, DVN5, DVN6 available optionalDVNThreshold: 2, // Need 2 of the 4 optional DVNs requiredDVNs: sortedAddresses([zkProofsDVN, committeeADVN]), // Must be sorted! optionalDVNs: sortedAddresses([committeeBDVN, middlechainDVN, nativeBridgeDVN, customDVN]) }) ) }); endpoint.setConfig(address(this), receiveLib, params); ``` ### DVN Providers DVNs are independent verification services. Common providers include LayerZero Labs, Google Cloud, Polyhedra (ZK), and others. Each has different trust models, latency, and cost characteristics. See the [DVN Providers page](/v2/deployments/deployed-contracts) for current addresses and availability per chain. ## Executor Architecture & Implementation Execution is permissionless - anyone can deliver verified messages: ```solidity wrap theme={null} // Executors are optional and permissionless // You can opt-out of automated execution and manually call: // - lzReceive() directly // - lzCompose() for composed messages // - Use LayerZero Scan UI for manual execution ``` ### Execution Model **Permissionless**: Anyone can be an executor **Optional**: You can opt out and execute manually **Competitive**: Multiple executors reduce costs **Manual Fallback**: Always available via LayerZero Scan or direct calls **Note**: Execution is separate from verification. DVNs verify, Executors deliver. ### Execution Options Configuration ```solidity wrap theme={null} import {OptionsBuilder} from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; // Options configure EXECUTION, not verification bytes memory options = OptionsBuilder.newOptions() .addExecutorLzReceiveOption( 200_000, // Gas for lzReceive execution 0 // Native token amount for receiver ) .addExecutorNativeDropOption( 1_000_000_000_000_000, // 0.001 ether (uint128) bytes32(uint256(uint160(receiver))) // Receiver as bytes32 ) .addExecutorOrderedExecutionOption(); // For strict ordering // DVNs are NOT configured via options - they're set via pathway config _lzSend(dstEid, payload, options, fee, refundAddress); ``` ## Worker Service Coordination Message Libraries coordinate both DVNs and Executors to ensure secure and reliable message delivery: 1. **Message Libraries** define the rules for verification and execution 2. **DVNs** verify messages according to their specialized verification methods 3. **Executors** deliver messages once verification requirements are met 4. **Onchain enforcement** ensures all rules are followed before message execution This separation enables: * **Independent scaling**: DVNs and Executors can scale independently * **Competitive markets**: Multiple providers can compete on cost and performance * **Flexible security**: Applications can choose verification approaches per pathway * **Reliable delivery**: Multiple execution options with manual fallbacks ## See Also * Module 3: [LayerZero as Master Interface](./layerzero-protocol-architecture) - Message Libraries and pathway configuration * Module 5: [Application Design Patterns](./application-design-patterns) - Using worker services in applications * [Deployments](../deployments/deployed-contracts) - Current DVN and Executor addresses # Workers in LayerZero V2 Source: https://docs.layerzero.network/v2/concepts/workers In the LayerZero V2 protocol, Workers serve as the umbrella term for two key types of service providers: Decentralized Verifier Networks (DVNs) and... In the LayerZero V2 protocol, **Workers** serve as the umbrella term for two key types of service providers: **Decentralized Verifier Networks (DVNs)** and **Executors**. Both play crucial roles in facilitating crosschain messaging and execution by providing verification and execution services. By abstracting these roles under the common interface known as a `worker`, LayerZero ensures a consistent and secure method to interact with both service types. ## What Are Workers? **Workers** are specialized entities that interact with the protocol to perform essential functions: * **Verification as a Service:** Decentralized Verifier Networks (DVNs), verify the authenticity and correctness of messages or transactions across chains. * **Execution as a Service:** Executors are responsible for carrying out actions requiring gas or compute units (transactions) on behalf of applications once verification is complete. These roles are unified under the Worker interface, meaning that whether a service provider is a DVN or an Executor, it interacts with the protocol using a standardized set of methods. ## Common Responsibilities Both DVNs and Executors share several common responsibilities managed through the Worker contract: * **Price Feeds:** Maintaining up-to-date pricing information relevant to transaction fees or service costs. * **Fee Management:** Handling fees associated with using the service, ensuring that both service providers and application owners have clear, consistent cost structures. By consolidating these responsibilities, the protocol simplifies the integration of different types of service providers while maintaining security and performance standards. ## The Role of the Protocol EndpointV2 uses a **MessageLibManager.sol** contract, responsible for the configuration and management of off-chain workers. Key features include: * **Application-specific configurations:** Applications can select specific message libraries, allowing them to tailor the protocol’s behavior to meet their unique security and trust requirements. * **Customizable settings:** Developers can set configurations for how messages are processed within each library, determine which off-chain entities are responsible for handling message delivery, and handle payment for these services. * **Decentralization and flexibility:** Instead of forcing every application into a one-size-fits-all approach,LayerZero V2 provides the flexibility needed to configure off-chain workers in a way that best fits the application’s design and security model. *** This architecture allows LayerZero V2 to provide robust, decentralized crosschain communication while giving application developers the tools needed to fine-tune their security and operational parameters. # Aptos Chain Deployments Source: https://docs.layerzero.network/v2/deployments/aptos-chains/overview LayerZero V2 deployment addresses and configuration for Aptos. Find Endpoint, DVN, and Executor contract addresses for both Aptos Mainnet and Aptos Testnet. LayerZero V2 protocol contracts, DVNs, and OFT deployments for the Aptos networks. ## Mainnet ## Testnet # Abstract Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/abstract LayerZero V2 deployment addresses and configuration for Abstract. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # Polygon Amoy Testnet Source: https://docs.layerzero.network/v2/deployments/chains/amoy-testnet LayerZero V2 deployment addresses and configuration for Polygon Amoy. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Exe... # Animechain Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/animechain LayerZero V2 deployment addresses and configuration for Animechain. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Execu... # Ape Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/ape LayerZero V2 deployment addresses and configuration for Ape. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, an... # Apex Fusion Nexus Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/apexfusionnexus LayerZero V2 deployment addresses and configuration for Apex Fusion Nexus. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN... # Aptos Source: https://docs.layerzero.network/v2/deployments/chains/aptos LayerZero V2 deployment addresses and configuration for Aptos. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Aptos Testnet Source: https://docs.layerzero.network/v2/deployments/chains/aptos-testnet LayerZero V2 deployment addresses and configuration for Aptos. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Arbitrum Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/arbitrum LayerZero V2 deployment addresses and configuration for Arbitrum. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # Arbitrum Sepolia Testnet Source: https://docs.layerzero.network/v2/deployments/chains/arbitrum-sepolia LayerZero V2 deployment addresses and configuration for Arbitrum Sepolia. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN,... # Arc Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/arc LayerZero V2 deployment addresses and configuration for Arc Mainnet. Find Endpoint, DVN, and Executor contract addresses for integration. # Astar Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/astar LayerZero V2 deployment addresses and configuration for Astar. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # AULT Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/ault LayerZero V2 deployment addresses and configuration for AULT Mainnet. Find Endpoint, DVN, and Executor contract addresses for integration. # Near Aurora Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/aurora LayerZero V2 deployment addresses and configuration for Near Aurora. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Exec... # Avalanche Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/avalanche LayerZero V2 deployment addresses and configuration for Avalanche. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Execut... # Base Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/base LayerZero V2 deployment addresses and configuration for Base. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Base Sepolia Testnet Source: https://docs.layerzero.network/v2/deployments/chains/base-sepolia LayerZero V2 deployment addresses and configuration for Base Sepolia. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Exe... # inEVM Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/bb1 LayerZero V2 deployment addresses and configuration for inEVM. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Beam Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/beam LayerZero V2 deployment addresses and configuration for Beam. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Berachain Bepolia Testnet Source: https://docs.layerzero.network/v2/deployments/chains/bepolia-testnet LayerZero V2 deployment addresses and configuration for Berachain Bepolia. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN... # Berachain Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/bera LayerZero V2 deployment addresses and configuration for Berachain. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Execut... # Bitlayer Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/bitlayer LayerZero V2 deployment addresses and configuration for Bitlayer. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # Blast Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/blast LayerZero V2 deployment addresses and configuration for Blast. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # BOB Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/bob LayerZero V2 deployment addresses and configuration for BOB. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, an... # Botanix Source: https://docs.layerzero.network/v2/deployments/chains/botanix LayerZero V2 deployment addresses and configuration for Botanix. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor... # Bouncebit Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/bouncebit LayerZero V2 deployment addresses and configuration for Bouncebit. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Execut... # BNB Smart Chain (BSC) Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/bsc LayerZero V2 deployment addresses and configuration for BNB Smart Chain (BSC). Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint,... # BNB Smart Chain (BSC) Testnet Source: https://docs.layerzero.network/v2/deployments/chains/bsc-testnet LayerZero V2 deployment addresses and configuration for BNB Smart Chain (BSC). Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint,... # Camp Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/camp LayerZero V2 deployment addresses and configuration for Camp Mainnet. Find Endpoint, DVN, and Executor contract addresses for integration. # Canto Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/canto LayerZero V2 deployment addresses and configuration for Canto. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Celo Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/celo LayerZero V2 deployment addresses and configuration for Celo. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Chiliz Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/chiliz LayerZero V2 deployment addresses and configuration for Chiliz Mainnet. Find Endpoint, DVN, and Executor contract addresses for integration. # Citrea Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/citrea LayerZero V2 deployment addresses and configuration for Citrea. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Codex Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/codex LayerZero V2 deployment addresses and configuration for Codex. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Concrete Source: https://docs.layerzero.network/v2/deployments/chains/concrete LayerZero V2 deployment addresses and configuration for Concrete. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # Conflux eSpace Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/conflux LayerZero V2 deployment addresses and configuration for Conflux eSpace. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, E... # CoreDAO Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/coredao LayerZero V2 deployment addresses and configuration for CoreDAO. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor... # Cronos EVM Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/cronosevm LayerZero V2 deployment addresses and configuration for Cronos EVM. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Execu... # Cronos zkEVM Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/cronoszkevm LayerZero V2 deployment addresses and configuration for Cronos zkEVM. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Exe... # Cyber Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/cyber LayerZero V2 deployment addresses and configuration for Cyber. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Degen Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/degen LayerZero V2 deployment addresses and configuration for Degen. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Dexalot Subnet Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/dexalot LayerZero V2 deployment addresses and configuration for Dexalot Subnet. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, E... # DFK Chain Source: https://docs.layerzero.network/v2/deployments/chains/dfk LayerZero V2 deployment addresses and configuration for DFK. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, an... # Dinari Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/dinari LayerZero V2 deployment addresses and configuration for Dinari Mainnet. Find Endpoint, DVN, and Executor contract addresses for integration. # DM2 Verse Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/dm2verse LayerZero V2 deployment addresses and configuration for DM2 Verse. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Execut... # Doma Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/doma LayerZero V2 deployment addresses and configuration for Doma Mainnet. Find Endpoint, DVN, and Executor contract addresses for integration. # DOS Chain Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/dos LayerZero V2 deployment addresses and configuration for DOS Chain. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Execut... # EDU Chain Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/edu LayerZero V2 deployment addresses and configuration for EDU Chain. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Execut... # Ethereal Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/ethereal LayerZero V2 deployment addresses and configuration for Ethereal. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # Ethereum Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/ethereum LayerZero V2 deployment addresses and configuration for Ethereum. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # Etherlink Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/etherlink LayerZero V2 deployment addresses and configuration for Etherlink. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Execut... # Flare Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/flare LayerZero V2 deployment addresses and configuration for Flare. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Flare Testnet Source: https://docs.layerzero.network/v2/deployments/chains/flare-testnet LayerZero V2 deployment addresses and configuration for Flare. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # EVM on Flow Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/flow LayerZero V2 deployment addresses and configuration for EVM on Flow. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Exec... # EVM on Flow Testnet Source: https://docs.layerzero.network/v2/deployments/chains/flow-testnet LayerZero V2 deployment addresses and configuration for EVM on Flow. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Exec... # Fraxtal Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/fraxtal LayerZero V2 deployment addresses and configuration for Fraxtal. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor... # Avalanche Fuji Testnet Source: https://docs.layerzero.network/v2/deployments/chains/fuji LayerZero V2 deployment addresses and configuration for Avalanche Fuji. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, E... # Fuse Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/fuse LayerZero V2 deployment addresses and configuration for Fuse. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Gate Layer Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/gatelayer LayerZero V2 deployment addresses and configuration for Gate Layer. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Execu... # Gensyn Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/gensyn LayerZero V2 deployment addresses and configuration for Gensyn Mainnet. Find Endpoint, DVN, and Executor contract addresses for integration. # Gnosis Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/gnosis LayerZero V2 deployment addresses and configuration for Gnosis. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Goat Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/goat LayerZero V2 deployment addresses and configuration for Goat. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Gravity Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/gravity LayerZero V2 deployment addresses and configuration for Gravity. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor... # Gunz Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/gunz LayerZero V2 deployment addresses and configuration for Gunz. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Harmony Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/harmony LayerZero V2 deployment addresses and configuration for Harmony. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor... # Hedera Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/hedera LayerZero V2 deployment addresses and configuration for Hedera. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Hedera Testnet Source: https://docs.layerzero.network/v2/deployments/chains/hedera-testnet LayerZero V2 deployment addresses and configuration for Hedera. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Hemi Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/hemi LayerZero V2 deployment addresses and configuration for Hemi. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Ethereum Holesky Testnet Source: https://docs.layerzero.network/v2/deployments/chains/holesky-testnet LayerZero V2 deployment addresses and configuration for Ethereum Holesky. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN,... # Hoodi Testnet Source: https://docs.layerzero.network/v2/deployments/chains/hoodi-testnet LayerZero V2 deployment addresses and configuration for Hoodi Testnet. Find Endpoint, DVN, and Executor contract addresses for integration. # Horizen Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/horizen LayerZero V2 deployment addresses and configuration for Horizen. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor... # Hubble Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/hubble LayerZero V2 deployment addresses and configuration for Hubble. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Humanity Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/humanity LayerZero V2 deployment addresses and configuration for Humanity. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # HyperEVM Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/hyperliquid LayerZero V2 deployment addresses and configuration for HyperEVM. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # HyperEVM Testnet Source: https://docs.layerzero.network/v2/deployments/chains/hyperliquid-testnet LayerZero V2 deployment addresses and configuration for HyperEVM. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # Initia Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/initia LayerZero V2 deployment addresses and configuration for Initia. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Initia Testnet Source: https://docs.layerzero.network/v2/deployments/chains/initia-testnet LayerZero V2 deployment addresses and configuration for Initia. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Injective EVM Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/injectiveevm LayerZero V2 deployment addresses and configuration for Injective EVM Mainnet. Find Endpoint, DVN, and Executor contract addresses for integration. # Ink Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/ink LayerZero V2 deployment addresses and configuration for Ink. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, an... # IOTA EVM Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/iota LayerZero V2 deployment addresses and configuration for IOTA EVM. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # IOTA L1 Source: https://docs.layerzero.network/v2/deployments/chains/iota-l1 LayerZero V2 deployment addresses and configuration for IOTA L1. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor... ### LayerZero Deployment Status Note: IOTA L1 uses a native Move implementation, separate from IOTA EVM L2 (EID 30284). # Irys Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/irys LayerZero V2 deployment addresses and configuration for Irys Mainnet. Find Endpoint, DVN, and Executor contract addresses for integration. # Vana Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/islander LayerZero V2 deployment addresses and configuration for Vana. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Japan Open Chain Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/joc LayerZero V2 deployment addresses and configuration for Japan Open Chain. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN,... # Katana Source: https://docs.layerzero.network/v2/deployments/chains/katana LayerZero V2 deployment addresses and configuration for Katana. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Kava Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/kava LayerZero V2 deployment addresses and configuration for Kava. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Kite Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/kite LayerZero V2 deployment addresses and configuration for Kite Mainnet. Find Endpoint, DVN, and Executor contract addresses for integration. # Kaia Mainnet (formerly Klaytn) Source: https://docs.layerzero.network/v2/deployments/chains/klaytn LayerZero V2 deployment addresses and configuration for Kaia Mainnet (formerly Klaytn). Find Endpoint, DVN, and Executor contract addresses for integration. # Lens Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/lens LayerZero V2 deployment addresses and configuration for Lens. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Lightlink Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/lightlink LayerZero V2 deployment addresses and configuration for Lightlink. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Execut... # Linea Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/linea LayerZero V2 deployment addresses and configuration for Linea. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Lisk Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/lisk LayerZero V2 deployment addresses and configuration for Lisk. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Loot Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/loot LayerZero V2 deployment addresses and configuration for Loot. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Lyra Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/lyra LayerZero V2 deployment addresses and configuration for Lyra. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Manta Pacific Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/manta LayerZero V2 deployment addresses and configuration for Manta Pacific. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Ex... # Mantle Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/mantle LayerZero V2 deployment addresses and configuration for Mantle. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Megaeth Source: https://docs.layerzero.network/v2/deployments/chains/megaeth LayerZero V2 deployment addresses and configuration for Megaeth. Find Endpoint, DVN, and Executor contract addresses for integration. # Merlin Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/merlin LayerZero V2 deployment addresses and configuration for Merlin. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Meter Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/meter LayerZero V2 deployment addresses and configuration for Meter. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Metis Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/metis LayerZero V2 deployment addresses and configuration for Metis. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Mode Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/mode LayerZero V2 deployment addresses and configuration for Mode. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Monad Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/monad LayerZero V2 deployment addresses and configuration for Monad. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Monad Testnet Source: https://docs.layerzero.network/v2/deployments/chains/monad-testnet LayerZero V2 deployment addresses and configuration for Monad. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Moonbeam Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/moonbeam LayerZero V2 deployment addresses and configuration for Moonbeam. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # Moonriver Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/moonriver LayerZero V2 deployment addresses and configuration for Moonriver. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Execut... # Morph Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/morph LayerZero V2 deployment addresses and configuration for Morph. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Movement Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/movement LayerZero V2 deployment addresses and configuration for Movement. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # Neo X Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/neox LayerZero V2 deployment addresses and configuration for Neo X Mainnet. Find Endpoint, DVN, and Executor contract addresses for integration. # Nexera Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/nexera LayerZero V2 deployment addresses and configuration for Nexera. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Nibiru Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/nibiru LayerZero V2 deployment addresses and configuration for Nibiru. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Arbitrum Nova Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/nova LayerZero V2 deployment addresses and configuration for Arbitrum Nova. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Ex... # 0G Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/og LayerZero V2 deployment addresses and configuration for 0G. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, and... # OKX Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/okx LayerZero V2 deployment addresses and configuration for OKX. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, an... # opBNB Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/opbnb LayerZero V2 deployment addresses and configuration for opBNB. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # OpenLedger Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/openledger LayerZero V2 deployment addresses and configuration for OpenLedger. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Execu... # Optimism Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/optimism LayerZero V2 deployment addresses and configuration for Optimism. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # Optimism Sepolia Testnet Source: https://docs.layerzero.network/v2/deployments/chains/optimism-sepolia LayerZero V2 deployment addresses and configuration for Optimism Sepolia. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN,... # Orderly Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/orderly LayerZero V2 deployment addresses and configuration for Orderly. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor... # Peaq Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/peaq LayerZero V2 deployment addresses and configuration for Peaq. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Pharos Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/pharos LayerZero V2 deployment addresses and configuration for Pharos Mainnet. Find Endpoint, DVN, and Executor contract addresses for integration. # Plasma Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/plasma LayerZero V2 deployment addresses and configuration for Plasma. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Plume Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/plumephoenix LayerZero V2 deployment addresses and configuration for Plume. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Polygon Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/polygon LayerZero V2 deployment addresses and configuration for Polygon. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor... # Rari Chain Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/rarible LayerZero V2 deployment addresses and configuration for Rari Chain. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Execu... # Rayls Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/rayls LayerZero V2 deployment addresses and configuration for Rayls Mainnet. Find Endpoint, DVN, and Executor contract addresses for integration. # re.al Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/real LayerZero V2 deployment addresses and configuration for re.al. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Redbelly Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/redbelly LayerZero V2 deployment addresses and configuration for Redbelly. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # Reya Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/reya LayerZero V2 deployment addresses and configuration for Reya. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Rise Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/rise LayerZero V2 deployment addresses and configuration for Rise Mainnet. Find Endpoint, DVN, and Executor contract addresses for integration. # Robinhood Chain Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/robinhood LayerZero V2 deployment addresses and configuration for Robinhood Chain Mainnet. Find Endpoint, DVN, and Executor contract addresses for integration. # Rootstock Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/rootstock LayerZero V2 deployment addresses and configuration for Rootstock. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Execut... # Scroll Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/scroll LayerZero V2 deployment addresses and configuration for Scroll. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Sei Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/sei LayerZero V2 deployment addresses and configuration for Sei. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, an... # Ethereum Sepolia Testnet Source: https://docs.layerzero.network/v2/deployments/chains/sepolia LayerZero V2 deployment addresses and configuration for Ethereum Sepolia. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN,... # Shimmer Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/shimmer LayerZero V2 deployment addresses and configuration for Shimmer. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor... # Silicon Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/silicon LayerZero V2 deployment addresses and configuration for Silicon. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor... # Skale Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/skale LayerZero V2 deployment addresses and configuration for Skale. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Solana Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/solana LayerZero V2 deployment addresses and configuration for Solana. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Solana Devnet Source: https://docs.layerzero.network/v2/deployments/chains/solana-testnet LayerZero V2 deployment addresses and configuration for Solana Devnet. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Ex... # Somnia Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/somnia LayerZero V2 deployment addresses and configuration for Somnia. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Soneium Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/soneium LayerZero V2 deployment addresses and configuration for Soneium. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor... # Sonic Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/sonic LayerZero V2 deployment addresses and configuration for Sonic. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Sophon Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/sophon LayerZero V2 deployment addresses and configuration for Sophon. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Otherworld Space Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/space LayerZero V2 deployment addresses and configuration for Otherworld Space. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN,... # Stable Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/stable LayerZero V2 deployment addresses and configuration for Stable. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # Stellar Source: https://docs.layerzero.network/v2/deployments/chains/stellar LayerZero V2 deployment addresses and configuration for Stellar. Find Endpoint, DVN, and Executor contract addresses for integration. # Story Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/story LayerZero V2 deployment addresses and configuration for Story. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Subtensor EVM Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/subtensorevm LayerZero V2 deployment addresses and configuration for Subtensor EVM. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Ex... # Sui Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/sui LayerZero V2 deployment addresses and configuration for Sui. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, an... # Superposition Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/superposition LayerZero V2 deployment addresses and configuration for Superposition. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Ex... # Tac Source: https://docs.layerzero.network/v2/deployments/chains/tac LayerZero V2 deployment addresses and configuration for Tac. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, an... # Taiko Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/taiko LayerZero V2 deployment addresses and configuration for Taiko. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # TelosEVM Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/telos LayerZero V2 deployment addresses and configuration for TelosEVM. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # Tempo Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/tempo LayerZero V2 deployment addresses and configuration for Tempo Mainnet. Find Endpoint, DVN, and Executor contract addresses for integration. # Tenet Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/tenet LayerZero V2 deployment addresses and configuration for Tenet. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, ... # Tiltyard Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/tiltyard LayerZero V2 deployment addresses and configuration for Tiltyard. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # Viction Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/tomo LayerZero V2 deployment addresses and configuration for Viction. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor... # TON Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/ton LayerZero V2 deployment addresses and configuration for TON. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, an... # Tron Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/tron LayerZero V2 deployment addresses and configuration for Tron. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Tron Testnet Source: https://docs.layerzero.network/v2/deployments/chains/tron-testnet LayerZero V2 deployment addresses and configuration for Tron. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Unichain Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/unichain LayerZero V2 deployment addresses and configuration for Unichain. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # Unichain Testnet Source: https://docs.layerzero.network/v2/deployments/chains/unichain-testnet LayerZero V2 deployment addresses and configuration for Unichain. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # Worldchain Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/worldchain LayerZero V2 deployment addresses and configuration for Worldchain. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Execu... # Xai Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/xai LayerZero V2 deployment addresses and configuration for Xai. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, an... # XChain Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/xchain LayerZero V2 deployment addresses and configuration for XChain. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor,... # XDC Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/xdc LayerZero V2 deployment addresses and configuration for XDC. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, an... # X Layer Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/xlayer LayerZero V2 deployment addresses and configuration for X Layer. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor... # XPLA Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/xpla LayerZero V2 deployment addresses and configuration for XPLA. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Zama Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/zama LayerZero V2 deployment addresses and configuration for Zama Mainnet. Find Endpoint, DVN, and Executor contract addresses for integration. # Zircuit Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/zircuit LayerZero V2 deployment addresses and configuration for Zircuit. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor... # Astar zkEVM Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/zkatana LayerZero V2 deployment addresses and configuration for Astar zkEVM. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Exec... # zkSync Era Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/zksync LayerZero V2 deployment addresses and configuration for zkSync Era. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Execu... # zkSync Sepolia Testnet Source: https://docs.layerzero.network/v2/deployments/chains/zksync-sepolia LayerZero V2 deployment addresses and configuration for zkSync Sepolia. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, E... # zkVerify Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/zkverify LayerZero V2 deployment addresses and configuration for zkVerify. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executo... # Zora Mainnet Source: https://docs.layerzero.network/v2/deployments/chains/zora LayerZero V2 deployment addresses and configuration for Zora. Find Endpoint, DVN, and Executor contract addresses for integration. Endpoint, DVN, Executor, a... # Deployed Endpoints, Message Libraries, and Executors Source: https://docs.layerzero.network/v2/deployments/deployed-contracts Learn about Deployed Endpoints, Message Libraries, and Executors in LayerZero V2. Understand the architecture, core concepts, and how it enables omnichain in... Below you can find a description of the main LayerZero V2 contracts and find the corresponding deployment information for each blockchain network LayerZero supports. **Endpoint Id** (`eid`) values have no relation to **Chain Id** (`chainId`) values. Since LayerZero spans both EVM and non-EVM chains, each Endpoint contract has a unique identifier known as the `eid` for determining which chain's `endpoint` to send to or receive messages from. When using LayerZero contract methods, be sure to use the correct `eid` listed below: * `30xxx`: refer to mainnet chains * `40xxx`: refer to testnet chains To see if a specific LayerZero contract supports another, use the `isSupportedEid()` method. ## Contract Description | **Contract Name** | **Description** | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **EndpointV2** | The primary entrypoint into LayerZero V2 responsible for managing crosschain communications. It orchestrates message sending, receiving, and configuration management between various smart contract connections using message library contracts and internal mappings to track `OApp` specific settings. | | **SendUln302** | A message library for sending crosschain messages. It combines functionalities from `SendUlnBase` and `SendLibBaseE2` to ensure secure message dispatch. | | **ReceiveUln302** | A message library for receiving and verifying crosschain messages. It integrates `ReceiveUlnBase` and `ReceiveLibBaseE2` to maintain message integrity. | | **SendUln301** | A version of the send message library compatible with `EndpointV1` for backwards compatibility with `EndpointV2`. | | **ReceiveUln301** | A version of the receive message library compatible with `EndpointV1` for backwards compatibility with `EndpointV2`. | | **LZ Executor** | A contract responsible for executing received crosschain messages automatically with a specified `gas limit` and `msg.value` for a fee. | | **LZ Dead DVN** | Represents a **[Dead Decentralized Verifier Network (DVN)](../concepts/glossary#dead-dvn)**. These contracts are placeholders used when the default LayerZero config is inactive and will require the OApp owner to manually configure the contract's config to use the pathway. | | **Blocked Message Library** | A message library that blocks messages from being sent or received. Used to disable specific pathways at the protocol level. | ## Checking Default Configs To see the default configuration for a given pathway (i.e., from `Chain A` to `Chain B`), you can use [LayerZero Scan's Default Checker](https://layerzeroscan.com/tools/defaults?version=V2). LayerZero Scan Default Configurations interface showing a table of pathway configurations from Ethereum to various destinations, displaying columns for Send Library (SendUln302), Receive Library (ReceiveUln302), DVN providers (LayerZero Labs, Google), Executor, and confirmation requirements # DVN Providers Source: https://docs.layerzero.network/v2/deployments/dvn-addresses Learn about DVN Providers in LayerZero V2. Understand the architecture, core concepts, and how it enables omnichain interoperability. Security configuration ... Seamlessly set up and configure your application's **Security Stack** to include the following Decentralized Verifier Networks (DVNs). To successfully add a DVN to verify a pathway, that DVN must be deployed on both chains!
### Next Steps Add these DVNs to your OApp configuration by following the CLI Guide or an appropriate quickstart: * [CLI Guide](../get-started/create-lz-oapp/start) * [OApp Quickstart](../developers/evm/oapp/overview) * [OFT Quickstart](../developers/evm/oft/quickstart) * [lzRead Quickstart](../developers/evm/lzread/overview) # OFT Ecosystem & Stargate Assets Source: https://docs.layerzero.network/v2/deployments/oft-ecosystem-stargate-assets LayerZero V2 OFT Ecosystem & Stargate Assets. Find contract addresses, endpoint IDs, and configuration for supported chains. LayerZero enables crosschain... Browse [Omnichain Fungible Token (OFT)](/v2/concepts/applications/oft-standard) deployments from various asset issuers across LayerZero-supported chains. OFTs enable seamless crosschain value transfer and can be implemented either as part of the token contract itself (OFT) or as a separate adapter contract wrapping an existing token (OFT Adapter). ### Programmatic Access All assets displayed on this page are available programmatically via LayerZero metadata endpoints: * **Stargate Assets:** [mainnet](https://mainnet.stargate-api.com/v1/metadata?version=v2) and [testnet](https://testnet.stargate-api.com/v1/metadata?version=v2) * **Ecosystem OFTs:** [both mainnet and testnet](https://metadata.layerzero-api.com/v1/metadata/experiment/ofts/list) Learn more about [LayerZero Metadata APIs](/v2/tools/endpoint-metadata). ### Asset Types **Stargate-Managed Assets:** * **StargatePool** - Native asset liquidity pools on Native chains managed by Stargate Finance, providing deep liquidity for crosschain transfers * **StargateOFT** - Minted token representations on Hydra chains backed by StargatePool liquidity, managed by Stargate Finance Stargate assets use an **opinionated security stack configured by LayerZero Labs**, ensuring consistent security standards across all Stargate deployments. Learn more about [Stargate's architecture and security model](/v2/concepts/applications/stargate-finance). **Asset Issuer-Owned Deployments:** * **OFT** - Omnichain Fungible Token standard implementation deployed and fully owned by asset issuers * **OFTAdapter** - Adapter contract wrapping existing tokens to enable omnichain functionality, deployed and owned by asset issuers * **NativeOFTAdapter** - Specialized adapter for native gas tokens (e.g., ETH), deployed and owned by asset issuers These deployments use **security configurations determined by each asset issuer**, allowing projects to customize their crosschain security parameters according to their specific requirements. For details on available security configuration options and DVN (Decentralized Verifier Network) setup, see [Security Stack (DVNs)](/v2/concepts/modular-security/security-stack-dvns) and [DVN & Executor Configuration](/v2/developers/evm/configuration/dvn-executor-config). **Standard Interface:** All assets implement LayerZero's `IOFT` interface, ensuring consistent crosschain functionality and interoperability across the ecosystem. ## About Crosschain Assets All assets listed implement LayerZero's `IOFT` (Omnichain Fungible Token Interface), providing a standardized interface for crosschain token transfers. This includes both Stargate-managed assets (StargatePool and StargateOFT) and ecosystem OFT deployments. The `IOFT` interface ensures consistent functionality across all implementations, enabling seamless integration with LayerZero OApps and composability between different asset types. ### Stargate V2 Stargate V2 provides unified liquidity pools for major stablecoins and native assets across multiple chains. StargatePool contracts hold native assets on core chains with deep liquidity, while StargateOFT contracts provide minted representations backed by pool liquidity on Hydra chains. **Security Configuration:** Stargate uses an opinionated security stack configured and maintained by LayerZero Labs, ensuring consistent security standards across all Stargate assets. This includes carefully selected DVNs (Decentralized Verifier Networks) and security parameters optimized for high-value asset transfers. Learn more at [Stargate Concepts](/v2/concepts/applications/stargate-finance) and [Stargate Finance Documentation](https://docs.stargate.finance). ### Ecosystem OFTs Asset issuers can deploy their own OFT implementations to enable native omnichain functionality. These deployments are fully controlled by the asset issuers and can be configured according to their specific requirements. **Security Configuration:** Each asset issuer determines their own security parameters, including DVN selection, executor configuration, and pathway-specific settings. This flexibility allows projects to tailor their security model to their specific use cases and risk tolerance. # Read Data Channels Source: https://docs.layerzero.network/v2/deployments/read-contracts LayerZero V2 Read Data Channels. Find contract addresses, endpoint IDs, and configuration for supported chains. LayerZero enables crosschain messaging. All of the **LayerZero Read** specific contract addresses and supported chains. Select either an origin chain to request and receive data to, or a data chain to specify where to read data from. The table will update dynamically. ### Next Steps Add these DVNs to your OApp configuration by following the CLI Guide or an appropriate quickstart: * [CLI Guide](../get-started/create-lz-oapp/start) * [OApp Quickstart](../developers/evm/oapp/overview) * [OFT Quickstart](../developers/evm/oft/quickstart) * [lzRead Quickstart](../developers/evm/lzread/overview) # Solana Chain Deployments Source: https://docs.layerzero.network/v2/deployments/solana-chains/overview LayerZero V2 contract addresses and program IDs for Solana. Find endpoint and DVN deployment details for both Solana Mainnet and Solana Devnet. LayerZero V2 protocol contracts, DVNs, and OFT deployments for the Solana networks. ## Mainnet ## Devnet # Aptos DVN and Executor Configuration Source: https://docs.layerzero.network/v2/developers/aptos-move/configuration/dvn-executor-config Configure Aptos DVN and Executor Configuration for your LayerZero application. Set up DVNs, executors, and pathway settings for crosschain messaging. Before setting your DVN and Executor Configuration, you should review the [Security Stack Core Concepts](../../../concepts/modular-security/security-stack-dvns). **Production deployments should use multiple required DVNs from independent operators.** A single-DVN configuration means a compromise of that one verifier results in unrestricted forged messages on the pathway. See the [Integration Checklist](../../../tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway) for production DVN guidance. You can manually configure your Aptos Move OApp’s Send and Receive settings by: * **Reading Defaults:** Use the `get_config` method to see default configurations. * **Setting Libraries:** Call `set_send_library` and `set_receive_library` to choose the correct Message Library version. * **Setting Configs:** Use the `set_config` instruction to update your custom DVN and Executor settings. For both Send and Receive configurations, make sure that for a given [channel](../../../concepts/glossary#channel--lossless-channel): * **Send (Chain A) settings** match the **Receive (Chain B) settings.** * DVN addresses are provided in alphabetical order. * Block confirmations are correctly set to avoid mismatches. ### Use the LayerZero CLI The LayerZero CLI has abstracted these calls for every supported chain. See the [**CLI Setup Guide**](../../../get-started/create-lz-oapp/start) to easily deploy, configure, and send messages using LayerZero. ## Setting Send / Receive Libraries In Aptos, you call the [**`endpoint_v2::endpoint`** module’s](#endpointv2endpoint-key-functions) **entry** or **friend** functions to pick the library you want for **sending** or **receiving** messages. A typical library in current Aptos V2 is the ULN 302 library (`uln_302::msglib`). If you do **not** call `set_send_library` or `set_receive_library`, your OApp falls back to the **default** library for that remote EID. **Note**: The Endpoint has built-in constraints: 1. **`dst_eid`** in `set_send_library(...)` must be valid for that library. 2. **`src_eid`** in `set_receive_library(...)` must be valid for that library (i.e., the library says it supports receiving from that chain). When you set a new library, the old library is replaced. You can optionally specify a **grace\_period** on the receive side so the old library can continue verifying messages for a set time. This is how you “roll over” from one library version to another. ### Typescript Below is an example of how you might call the `endpoint_v2::endpoint::set_send_library` or `endpoint_v2::endpoint::set_receive_library` function using the Aptos JS SDK. ```typescript wrap theme={null} import { Account, Aptos, Ed25519PrivateKey, PrivateKey, PrivateKeyVariants, SimpleTransaction, InputEntryFunctionData, AptosConfig, } from '@aptos-labs/ts-sdk'; const NODE_URL = 'https://fullnode.testnet.aptoslabs.com/v1'; // Replace with your actual private key or create from a local mnemonic const ADMIN_PRIVATE_KEY_HEX = '0x...'; const ADMIN_ACCOUNT_ADDRESS = '0x...'; // OApp data const OAPP_ADDRESS = '0xMyOApp'; // your OApp’s address on Aptos const REMOTE_EID = 30101; // e.g. the remote chain’s EID const MSGLIB_ADDRESS = '0xULN302'; // The “Send” library you want const NETWORK = 'testnet'; // "testnet" or "mainnet" // Create the private key const aptos_private_key = PrivateKey.formatPrivateKey( ADMIN_PRIVATE_KEY_HEX, PrivateKeyVariants.Ed25519, ); // Create the signer account const signer_account = Account.fromPrivateKey({ privateKey: new Ed25519PrivateKey(aptos_private_key), address: ADMIN_ACCOUNT_ADDRESS, }); // Create the Aptos client const aptos = new Aptos(new AptosConfig({network: NETWORK})); ``` The function signature in your OApp might look like: ```rust wrap theme={null} public entry fun set_send_library( account: &signer, remote_eid: u32, msglib: address, ) { ... } ``` Which can be invoked like: ```typescript wrap theme={null} async function setSendLibrary() { // 1. Build the transaction payload and transaction const payload: InputEntryFunctionData = { function: `${OAPP_ADDRESS}::oapp_core::set_send_library`, functionArguments: [REMOTE_EID, MSGLIB_ADDRESS], }; const transaction: SimpleTransaction = await aptos.transaction.build.simple({ sender: ADMIN_ACCOUNT_ADDRESS, data: payload, options: { maxGasAmount: 30000, }, }); // 2. Generate and sign transaction const signedTransaction = await aptos.signAndSubmitTransaction({ signer: signer_account, transaction: transaction, }); // 3. Wait for confirmation const executedTransaction = await aptos.waitForTransaction({ transactionHash: signedTransaction.hash, }); console.log('set_send_library transaction completed:', executedTransaction.hash); } setSendLibrary() .then(() => { console.log('Done setting send library'); }) .catch(console.error); ``` ## Setting Security & Executor Configuration A similar approach to EVM’s `setConfig` is available in Aptos. You can call: ```rust wrap theme={null} public entry fun set_config( account: &signer, msglib: address, eid: u32, config_type: u32, config: vector, ) { assert_authorized(address_of(account)); endpoint::set_config(&oapp_store::call_ref(), msglib, eid, config_type, config); } ``` * **`msglib`** is the library you are configuring (e.g. `@uln_302`). * **`eid`** is the remote endpoint ID you are targeting (e.g. `30101` if referencing “Chain B’s ID”). * **`config_type`** is typically 1 for **Executor** and 2 or 3 for ULN-based “send” or “receive” config. * **`config`** is a serialized bytes array containing your DVN addresses, confirmations, or max message size, etc. ### Typical ULN & Executor Structures The `uln_302::configuration` module references these data structures: #### ULN Config (Security Stack) ```rust wrap theme={null} struct UlnConfig has copy, drop { confirmations: u64, optional_dvn_threshold: u8, required_dvns: vector
, optional_dvns: vector
, use_default_for_confirmations: bool, use_default_for_required_dvns: bool, use_default_for_optional_dvns: bool, } ``` * `confirmations`: how many blocks to wait on the source chain for finality. * `required_dvns`: the DVNs that **must** sign your message. * `optional_dvns`: the DVNs that **may** sign your message if they reach the threshold. * `optional_dvn_threshold`: how many optional DVNs are needed if you have optional DVNs. * `use_default_for_*`: determines if we fallback to a default config for certain fields. In EVM you’d see fields like `requiredDVNCount`, `requiredDVNs`, `optionalDVNCount`, etc. In Aptos, it’s stored as a single struct with arrays for addresses. #### Executor Config ```rust wrap theme={null} struct ExecutorConfig has copy, drop { max_message_size: u32, executor_address: address, } ``` * `max_message_size`: max size of crosschain messages, in bytes. * `executor_address`: which executor is authorized/paid to `lz_receive` your message. #### Distinction vs. EVM Where EVM calls `setConfigParam[]`, on Aptos, we pass a single `(config_type, config)` each time. If you want to set both Executor and ULN in one go, call `set_config` with each config type. Some developers write a convenience function to do both in a single transaction. The `uln_302::configuration` module handles the actual decode: * **`CONFIG_TYPE_EXECUTOR = 1`** * **`CONFIG_TYPE_SEND_ULN = 2`** * **`CONFIG_TYPE_RECV_ULN = 3`** It extracts your config bytes, e.g. `extract_uln_config` for a ULN struct or `extract_executor_config` for an executor struct. **Example**: Setting a “send side” ULN config might look like: ```ts wrap theme={null} async function setUlnConfig(sendLibrary: string, remoteEid: number, serializedConfig: Uint8Array) { // config_type = 2 for "send side" or 3 for "receive side" const CONFIG_TYPE_SEND_ULN = 2; // Suppose your OApp entry function is: // public entry fun set_config(account: &signer, msglib: address, eid: u32, config_type: u32, config: vector) const payload: InputEntryFunctionData = { function: `${OAPP_ADDRESS}::oapp_core::set_config`, functionArguments: [sendLibrary, remoteEid, CONFIG_TYPE_SEND_ULN, serializedConfig], }; const rawTransaction = await aptos.transaction.build.simple({ sender: ADMIN_ACCOUNT_ADDRESS, data: payload, options: { maxGasAmount: 30000, }, }); const signedTransaction = await aptos.signAndSubmitTransaction({ signer: signer_account, transaction: rawTransaction, }); const executedTransaction = await aptos.waitForTransaction({ transactionHash: signedTransaction.hash, }); console.log(`set_config ULN success: ${signedTransaction.hash}`); } ``` The Executor config is `CONFIG_TYPE_EXECUTOR = 1`. You pass a serialized `(max_message_size, executor_address)` structure. ```ts wrap theme={null} async function setExecutorConfig( sendLibrary: string, remoteEid: number, execConfigBytes: Uint8Array, ) { // config_type = 1 for "executor" const CONFIG_TYPE_EXECUTOR = 1; const payload: InputEntryFunctionData = { function: `${OAPP_ADDRESS}::oapp_core::set_config`, functionArguments: [sendLibrary, remoteEid, CONFIG_TYPE_EXECUTOR, execConfigBytes], }; const rawTransaction = await aptos.transaction.build.simple({ sender: ADMIN_ACCOUNT_ADDRESS, data: payload, options: { maxGasAmount: 30000, }, }); const signedTransaction = await aptos.signAndSubmitTransaction({ signer: signer_account, transaction: rawTransaction, }); const executedTransaction = await aptos.waitForTransaction({ transactionHash: signedTransaction.hash, }); console.log(`set_config Executor success: ${signedTransaction.hash}`); } ``` ## Resetting to Default If you pass a config that sets fields like `confirmations = 0`, `required_dvns = []`, and sets `use_default_for_confirmations = true`, then the OApp will fallback to whatever the default is on that chain. Similarly, if you pass an `ExecutorConfig` with `max_message_size = 0` and `executor_address = @0x0`, you revert to default. The `uln_302::configuration` module merges your OApp’s config with the chain’s default config if you set `use_default_for_* = true`. ## Debugging Configurations A **correct** OApp configuration example: | SendUlnConfig (A to B) | ReceiveUlnConfig (B to A) | | ------------------------------------------------------- | ------------------------------------------------------- | | confirmations: 15 | confirmations: 15 | | optionalDVNCount: 0 | optionalDVNCount: 0 | | optionalDVNThreshold: 0 | optionalDVNThreshold: 0 | | optionalDVNs: Array(0) | optionalDVNs: Array(0) | | requiredDVNCount: 2 | requiredDVNCount: 2 | | requiredDVNs: Array(DVN1\_Address\_A, DVN2\_Address\_A) | requiredDVNs: Array(DVN1\_Address\_B, DVN2\_Address\_B) | The sending OApp's **SendLibConfig** (OApp on Chain A) and the receiving OApp's **ReceiveLibConfig** (OApp on Chain B) match! ### Block Confirmation Mismatch An example of an **incorrect** OApp configuration: | SendUlnConfig (A to B) | ReceiveUlnConfig (B to A) | | ------------------------------- | ------------------------------- | | **confirmations: 5** | **confirmations: 15** | | optionalDVNCount: 0 | optionalDVNCount: 0 | | optionalDVNThreshold: 0 | optionalDVNThreshold: 0 | | optionalDVNs: Array(0) | optionalDVNs: Array(0) | | requiredDVNCount: 2 | requiredDVNCount: 2 | | requiredDVNs: Array(DVN1, DVN2) | requiredDVNs: Array(DVN1, DVN2) | The above configuration has a **block confirmation mismatch**. The sending OApp (Chain A) will only wait 5 block confirmations, but the receiving OApp (Chain B) will not accept any message with less than 15 block confirmations. Messages will be blocked until either the sending OApp has increased the outbound block confirmations, or the receiving OApp decreases the inbound block confirmation threshold. #### DVN Mismatch Another example of an incorrect OApp configuration: | SendUlnConfig (A to B) | ReceiveUlnConfig (B to A) | | ----------------------------- | ----------------------------------- | | confirmations: 15 | confirmations: 15 | | optionalDVNCount: 0 | optionalDVNCount: 0 | | optionalDVNThreshold: 0 | optionalDVNThreshold: 0 | | optionalDVNs: Array(0) | optionalDVNs: Array(0) | | **requiredDVNCount: 1** | **requiredDVNCount: 2** | | **requiredDVNs: Array(DVN1)** | **requiredDVNs: Array(DVN1, DVN2)** | The above configuration has a **DVN mismatch**. The sending OApp (Chain A) only pays DVN 1 to listen and verify the packet, but the receiving OApp (Chain B) requires both DVN 1 and DVN 2 to mark the packet as verified. Messages will be blocked until either the sending OApp has added DVN 2's address on Chain A to the SendUlnConfig, or the receiving OApp removes DVN 2's address on Chain B from the ReceiveUlnConfig. #### [Dead DVN](../../../concepts/glossary#dead-dvn) This configuration includes a **Dead DVN**: | SendUlnConfig (A to B) | ReceiveUlnConfig (B to A) | | ----------------------------------- | ---------------------------------------- | | confirmations: 15 | confirmations: 15 | | optionalDVNCount: 0 | optionalDVNCount: 0 | | optionalDVNThreshold: 0 | optionalDVNThreshold: 0 | | optionalDVNs: Array(0) | optionalDVNs: Array(0) | | **requiredDVNCount: 2** | **requiredDVNCount: 2** | | **requiredDVNs: Array(DVN1, DVN2)** | **requiredDVNs: Array(DVN1, DVN\_DEAD)** | The above configuration has a **Dead DVN**. Similar to a DVN Mismatch, the sending OApp (Chain A) pays DVN 1 and DVN 2 to listen and verify the packet, but the receiving OApp (Chain B) has currently set DVN 1 and a Dead DVN to mark the packet as verified. Since a Dead DVN for all practical purposes should be considered a null address, no verification will ever match the dead address. Messages will be blocked until the receiving OApp removes or replaces the Dead DVN from the ReceiveUlnConfig. ## Key Functions in `endpoint_v2::endpoint` Below are the main wiring functions used for configuration. They typically are invoked in your OApp’s admin or delegate entry function. * **`register_receive_pathway(call_ref, src_eid, sender_bytes32)`**: Inform the endpoint that you accept messages from `(src_eid, sender)`. * **`set_send_library(call_ref, remote_eid, msglib)`**: Tells the endpoint which library to use for sending messages to `remote_eid`. * **`set_receive_library(call_ref, remote_eid, msglib, grace_period)`**: Tells the endpoint which library to use for receiving messages from `remote_eid`. Optionally specify a `grace_period` in blocks. * **`set_config(call_ref, msglib, eid, config_type, config_bytes)`**: Instruct the chosen library to store or merge your OApp’s custom config for that EID. ## Conclusion The **Aptos V2** Endpoint wiring parallels the approach on EVM: * **Choose your libraries** for sending and receiving (`set_send_library`, `set_receive_library`). * **Set your ULN or Executor configs** via `set_config` on the chosen library’s address, specifying the remote EID. * Ensure your sending chain’s config aligns with the receiving chain’s config (DVNs, block confirmations, etc.), or your messages may be blocked . * If you want to revert to defaults, pass a config that indicates `use_default_for_* = true` or sets addresses to `@0x0`. By following these steps, you can precisely control the **LayerZero V2** security stack (DVNs), block confirmations, and executor settings on Aptos—just as you would with the EVM-based `setSendLibrary`, `setReceiveLibrary`, and `setConfig` flow. # LayerZero V2 Aptos Move OApp Source: https://docs.layerzero.network/v2/developers/aptos-move/contract-modules/oapp The OApp Standard provides developers with a _generic message passing interface_ to send and receive arbitrary pieces of data between contracts existing... The OApp Standard provides developers with a *generic message passing interface* to **send** and **receive** arbitrary pieces of data between contracts existing on different blockchain networks. Diagram showing crosschain messaging between Network A and Network B using the OApp Standard, with an arrow indicating the message flow via LayerZero Send and Receive Diagram showing crosschain messaging between Network A and Network B using the OApp Standard, with an arrow indicating the message flow via LayerZero Send and Receive This interface can easily be extended to include anything from specific financial logic in a DeFi application, a voting mechanism in a DAO, and broadly any smart contract use case. Below is an overview of how the **Aptos Move OApp Standard** aligns with the **LayerZero V2 OApp Contract Standard** on [EVM](../../evm/oapp/overview) and/or [Solana](../../solana/oapp/overview): 1. **`oapp::oapp`** (main OApp interface and example usage) 2. **`oapp::oapp_compose`** (handles composable message logic) 3. **`oapp::oapp_core`** (contains core utilities such as sending messages, quoting fees, setting config/delegates/peers) 4. **`oapp::oapp_receive`** (handles low-level message reception logic) 5. **`oapp::oapp_store`** (internal persistent storage and admin/delegate logic) This structure replicates in Aptos Move the same interface and flow you would expect from an OApp-based contract on EVM or Solana using LayerZero V2. ## Overview A **LayerZero OApp** (Omnichain Application) is a contract/module that can: * **Send** and **Receive** messages across chains * Optionally **Compose** messages (which is a feature to re-enter the OApp with new logic after a message is processed) * **Quote** fees for sending crosschain messages * Manage **Admin** and **Delegate** roles for secure crosschain interactions In Move, these responsibilities are broken out into the above modules to keep the code well-organized. ### Key Components * **Sending Messages**: Uses the `lz_send` function from `oapp::oapp_core`. * **Quoting Fees**: Uses `lz_quote` from `oapp::oapp_core`. * **Receiving Messages**: Handled by `lz_receive` in `oapp::oapp_receive` and overridden into your OApp’s logic. * **Composing Messages**: Enabled by `lz_compose` in `oapp::oapp_compose`. * **Admin/Delegate Permissions**: Managed through `oapp::oapp_core` and stored in `oapp::oapp_store`. ## Main OApp Module (`oapp::oapp`) The main OApp Module defines entry functions that an application developer can call (for example, to **send** or **quote** crosschain messages). This contract can house your custom logic for receiving messages (though the base code is handled in `oapp_receive`, you can add extra handling via `lz_receive_impl`). ```rust wrap theme={null} module oapp::oapp { use std::signer::address_of; use std::primary_fungible_store; use std::option::{self, Option}; use endpoint_v2_common::bytes32::Bytes32; use oapp::oapp_core::{combine_options, lz_quote, lz_send, refund_fees}; use oapp::oapp_store::OAPP_ADDRESS; const STANDARD_MESSAGE_TYPE: u16 = 1; /// An example "send" entry function for crosschain messages. public entry fun example_message_sender( account: &signer, dst_eid: u32, message: vector, extra_options: vector, native_fee: u64, ) { let sender = address_of(account); // Withdraw fees let native_metadata = object::address_to_object(@native_token_metadata_address); let native_fee_fa = primary_fungible_store::withdraw(account, native_metadata, native_fee); let zro_fee_fa = option::none(); // Build + send the message lz_send( dst_eid, message, combine_options(dst_eid, STANDARD_MESSAGE_TYPE, extra_options), &mut native_fee_fa, &mut zro_fee_fa, ); // Refund any unused fees to the user refund_fees(sender, native_fee_fa, zro_fee_fa); } #[view] /// Quoting the fees for sending a crosschain message public fun example_message_quoter( dst_eid: u32, message: vector, extra_options: vector, ): (u64, u64) { let options = combine_options(dst_eid, STANDARD_MESSAGE_TYPE, extra_options); lz_quote(dst_eid, message, options, false) } public(friend) fun lz_receive_impl( _src_eid: u32, _sender: Bytes32, _nonce: u64, _guid: Bytes32, _message: vector, _extra_data: vector, receive_value: Option, ) { // Deposit the received token, if any option::destroy(receive_value, |value| primary_fungible_store::deposit(OAPP_ADDRESS(), value)); // TODO: OApp developer can add custom logic for incoming messages here. } ... } ``` ### Key Points * **`example_message_sender`** is a reference entry function. Developers can create their own, based on the same pattern, to send a message crosschain. * **`lz_receive_impl`** is the function that your OApp can override/extend with your custom "on-message" logic. By default, this module **imports** functions from [`oapp::oapp_core`](#3-oapp-core-module-oappoapp_core) and [`oapp::oapp_store`](#6-internal-store-module-oappoapp_store) to make its job easier. ## OApp Core Module (`oapp::oapp_core`) The Core Module provides lower-level helper functions to **send** messages, **quote** fees, manage OApp configuration, handle **admin** or **delegate** actions, and keep track of enforced configuration [options](../../evm/configuration/options). ```rust wrap theme={null} module oapp::oapp_core { use endpoint_v2::endpoint; use endpoint_v2_common::bytes32::Bytes32; use std::option::{self, Option}; friend oapp::oapp; /// Sends a crosschain message. public(friend) fun lz_send( dst_eid: u32, message: vector, options: vector, native_fee: &mut FungibleAsset, zro_fee: &mut Option, ): MessagingReceipt { endpoint::send(&oapp_store::call_ref(), dst_eid, get_peer_bytes32(dst_eid), message, options, native_fee, zro_fee) } #[view] /// Quotes the cost of a crosschain message in both native & ZRO tokens. public fun lz_quote( dst_eid: u32, message: vector, options: vector, pay_in_zro: bool, ): (u64, u64) { endpoint::quote(OAPP_ADDRESS(), dst_eid, get_peer_bytes32(dst_eid), message, options, pay_in_zro) } ... } ``` * **`lz_send`**: Calls the underlying LayerZero Endpoint to perform crosschain message sending. * **`lz_quote`**: Returns the quote for fees needed to send the message in the native gas token or ZRO if enabled. * **Peer Management**: The concept of peers (i.e., the paired OApp addresses) is captured by `set_peer(...)`, `has_peer(...)`, etc. per blockchain pathway (i.e., from Aptos to ETH). * **Admin & Delegate**: Functions like `transfer_admin`, `set_delegate`, `assert_authorized`, etc. manage who can update the OApp configuration or call certain restricted functions. * **Enforced Options**: By default, the system can enforce specific message options (like certain gas limits, native gas drops, etc.) for sending to specific destination pathways. This is done via `get_enforced_options` and `combine_options`. ## OApp Receive Module (`oapp::oapp_receive`) When a crosschain message arrives on Aptos, the OApp's configured Executor will route the call into this module’s `lz_receive` or `lz_receive_with_value`. This module then calls **`lz_receive_impl`** in your main `oapp::oapp` (or whichever module is designated). ```rust wrap theme={null} module oapp::oapp_receive { use endpoint_v2::endpoint; /// Main entry for receiving a crosschain message. public entry fun lz_receive( src_eid: u32, sender: vector, nonce: u64, guid: vector, message: vector, extra_data: vector, ) { lz_receive_with_value( src_eid, sender, nonce, wrap_guid(to_bytes32(guid)), message, extra_data, option::none(), ) } /// The actual function that can carry a token value public fun lz_receive_with_value( src_eid: u32, sender: vector, nonce: u64, wrapped_guid: WrappedGuid, message: vector, extra_data: vector, value: Option, ) { // Validation, clearing, then calls your custom logic endpoint::clear(&oapp_store::call_ref(), src_eid, to_bytes32(sender), nonce, wrapped_guid, message); lz_receive_impl( src_eid, to_bytes32(sender), nonce, get_guid_from_wrapped(&wrapped_guid), message, extra_data, value, ); } } ``` This means that: * The configured Executor contract on Aptos calls `lz_receive(...)` on your OApp. * The message is checked to see if it was sent from an authorized peer (i.e. checking if `sender` is one of your OApp’s configured peers). * The function `lz_receive_impl` is invoked from your main OApp module to perform any final business logic. ## Compose Module (`oapp::oapp_compose`) **"Compose"** is a LayerZero feature that allows an OApp to schedule a subsequent call to itself after a message is processed. In [EVM](../../evm/oapp/message-design-patterns#composed), this is typically invoked via specialized calls to the Endpoint contract in the child OApp's lzReceive implementation, and delivered to a contract which implements `ILayerZeroComposer.sol`. In Aptos Move, `oapp::oapp_compose` includes the logic to handle the composition of messages after they are cleared or to initiate them from the local OApp. ```rust wrap theme={null} module oapp::oapp_compose { public entry fun lz_compose( from: address, guid: vector, index: u16, message: vector, extra_data: vector, ) { endpoint::clear_compose(&oapp_store::call_ref(), from, wrap_guid_and_index(guid, index), message); lz_compose_impl( from, to_bytes32(guid), index, message, extra_data, option::none(), ) } public fun lz_compose_with_value( from: address, guid_and_index: WrappedGuidAndIndex, message: vector, extra_data: vector, value: Option, ) { // Similar logic, but includes the possibility of receiving a token in the compose endpoint::clear_compose(&oapp_store::call_ref(), from, guid_and_index, message); lz_compose_impl(from, guid, index, message, extra_data, value); } // Developer can override or fill in the body of lz_compose_impl with custom logic } ``` In typical OApp implementations, you will only need to implement `lz_compose_impl` if your OApp truly needs the advanced external call style logic after a crosschain message has been received. ## Internal Store Module (`oapp::oapp_store`) The internal store **manages** the global OApp state: * The OApp’s own address * The current **Admin** and **Delegate** addresses * A table of recognized **Peers** (paired addresses from other chains) * A table of enforced messaging **options** ```rust wrap theme={null} module oapp::oapp_store { struct OAppStore has key { contract_signer: ContractSigner, admin: address, peers: Table, delegate: address, enforced_options: Table>, } public(friend) fun get_admin(): address acquires OAppStore { store().admin } public(friend) fun has_peer(eid: u32): bool acquires OAppStore { table::contains(&store().peers, eid) } public(friend) fun set_peer(eid: u32, peer: Bytes32) acquires OAppStore { table::upsert(&mut store_mut().peers, eid, peer) } ... } ``` On Aptos, you typically store data via `move_to(account, T { ... })`. This module sets up a global `OAppStore` resource at `@oapp`. Functions like `has_peer()`, `set_peer()`, `get_delegate()`, etc., let the other modules read and write data in a structured manner. ## Putting It All Together 1. **Initialization** * On "init", the modules are registered with the `endpoint_v2` contract. * The OApp store (`oapp::oapp_store::OAppStore`) is created at the address `@oapp`. 2. **Configuration** * You set up your **Admin** address and optional **Delegate** if you want certain calls (e.g. `set_send_library`, `skip`, `burn`, or `nilify`) to be callable by someone other than the admin. * You **set peers** by calling `set_peer(account, remote_eid, remote_peer_address)`. 3. **Sending a Message** * Call your custom send function (like `example_message_sender`) from your main OApp module, which internally calls `lz_send`. * Under the hood, the endpoint collects the message, your fees, and orchestrates crosschain delivery. 4. **Receiving a Message** * The LayerZero Executor calls `oapp::oapp_receive::lz_receive` * This function automatically calls `lz_receive_impl` in your `oapp::oapp`. * You handle the message payload or any FungibleAsset that might have come along with it. 5. **Optional: Composing** * If you want advanced functionality that re-calls the OApp after clearing, implement `lz_compose_impl` in `oapp::oapp_compose`. * Typically only needed for specialized re-entrancy or bridging flows. ## Customizing for Your Own OApp * **Rename your main modules** if desired (e.g., from `oapp::oapp` to `oapp::my_app`). Update the friend usage accordingly. * **Implement** your own send/receive logic in `oapp::oapp` entry functions. * **Override** `lz_receive_impl` to process the crosschain message data (e.g., parse the vector bytes). * **Implement** or skip the `lz_compose_impl` in `oapp_compose` if your OApp doesn’t need composition logic. * **Manage** your OApp’s admin and delegate roles carefully. The admin can set local storage options (like peers), while the delegate can call endpoint-level changes (like DVNs, Executors, Message Libraries). ## Conclusion The **Aptos Move OApp Standard** mirrors the **LayerZero V2 OApp Contract Standard** on EVM and Solana by: * Splitting crosschain responsibilities into send, receive, and optional compose modules. * Offering a straightforward pattern for quoting fees, paying them, and optionally paying them in the ZRO token. * Enforcing the same security patterns around admin/delegates, ensuring that the correct roles handle the correct privileges. * Providing a strong separation of concerns in well-structured modules to keep your OApp’s logic clean and maintainable. Use these modules as your foundation for building powerful, omnichain Move applications on Aptos with the same design concepts you would expect from a LayerZero V2 OApp on other chains. # LayerZero V2 Aptos Move OFT Source: https://docs.layerzero.network/v2/developers/aptos-move/contract-modules/oft Below is comprehensive documentation for Aptos Move OFT modules, explaining both the OFT and OFT Adapter, mirroring the LayerZero V2 OFT Standard you... Below is comprehensive documentation for Aptos Move **OFT** modules, explaining both the **OFT** and **OFT Adapter**, mirroring the **LayerZero V2 OFT Standard** you might see on [EVM](../../evm/oft/quickstart) or [Solana](../../solana/oft/overview). The Omnichain Fungible Token (OFT) Standard allows **fungible tokens** to be transferred across multiple blockchains without asset wrapping or middlechains. This standard works by either debiting (`burn` / `lock`) tokens on the source chain, sending a message via LayerZero, and delivering a function call to credit (`mint` / `unlock`) the same number of tokens on the destination chain. This creates a **unified supply** across all networks that the OFT supports. ### What is OFT? An **Omnichain Fungible Token (OFT)** is a LayerZero-based token that can be sent across chains without wrapping or middle-chains. It supports: * **Burn + Mint** (OFT): Remove supply from the source chain, re-create it on the destination. Diagram showing OFT burn-and-mint mechanism: tokens are burned (subtracted) on the source chain and minted (added) on the destination chain, connected by an arrow representing the crosschain transfer Diagram showing OFT burn-and-mint mechanism: tokens are burned (subtracted) on the source chain and minted (added) on the destination chain, connected by an arrow representing the crosschain transfer * **Lock + Unlock** (OFT Adapter): Move supply into an escrow on the source, release it on the destination. Diagram showing OFT Adapter lock-and-unlock mechanism: tokens are locked in an escrow on the source chain and released on the destination chain Diagram showing OFT Adapter lock-and-unlock mechanism: tokens are locked in an escrow on the source chain and released on the destination chain On EVM, you see this logic embedded in an `OFT.sol` or `OFTAdapter.sol` contract. In Aptos Move, we achieve the same through specialized modules: 1. **`oft::oft_fa`** – OFT “mint/burn” approach. 2. **`oft::oft_adapter_fa`** – OFT Adapter “lock/unlock” approach. 3. **`oft::oft`** – Unified interface for user-level send, quote, and receive entry points. 4. **`oft::oft_core`** – Core bridging logic shared by both OFT and OFT Adapter. 5. **`oft::oft_impl_config`** – Central config for fees, blocklisting, rate limits (used by both). 6. **`oft::oft_store`** – Tracks shared vs. local decimals so each chain can represent the token with different local decimals if needed. 7. **`oft::oapp_core` / `oft::oapp_store`** – The OApp plumbing for bridging messages crosschain, handling admin/delegate roles, peer configuration, etc. The **EVM/Solana OFT** relies on `ERC20`/`SPL` logic for mint/burn or lock/unlock. The **Aptos OFT** relies on Move’s `Fungible Asset` standard. `oft::oft_fa` does actual mint/burn, while `oft::oft_adapter_fa` locks/unlocks an existing `Fungible Asset` in an escrow. ### 2. Relating the OApp and OFT Modules The **OApp Standard** gives your contract the ability to: * **Send** crosschain messages (`lz_send`) * **Receive** crosschain messages (via `lz_receive`) * **Quote** crosschain fees * **Enforce** admin- or delegate-level controls The **OFT Standard** then builds on top of that to specifically handle: * **Fungible Asset** bridging * Local token manipulations (burn/mint or lock/unlock) * Additional rate-limiting, blocklists, bridging fees, etc. All crosschain calls still flow through `lz_send` and `lz_receive` in `oft_core`, which rely on the OApp’s ability to call the LayerZero Endpoint. This is exactly how the EVM `OFT` extends `OApp` to unify crosschain token operations. ## OFT: `oft::oft_fa` When tokens are sent crosschain, the module **burns** tokens from the sender’s local supply. On the receiving chain, it **mints** newly created tokens for the recipient. In EVM, you might see this with an `ERC20` implementation that calls `_burn` in `send()` and `_mint` in `lzReceive()`. On Aptos, `oft_fa.move` uses **Move’s** `FungibleAsset`: ```rust wrap theme={null} public(friend) fun debit_fungible_asset( sender: address, fa: &mut FungibleAsset, min_amount_ld: u64, dst_eid: u32, ): (u64, u64) acquires OftImpl { // 1. Check blocklist assert_not_blocklisted(sender); // 2. Determine the “send” and “receive” amounts (minus dust/fees) let amount_ld = fungible_asset::amount(fa); let (amount_sent_ld, amount_received_ld) = debit_view(amount_ld, min_amount_ld, dst_eid); // 3. Rate limit checks (no exceeding capacity) try_consume_rate_limit_capacity(dst_eid, amount_received_ld); // 4. Subtract the fee from the total let extracted_fa = fungible_asset::extract(fa, amount_sent_ld); if (fee_ld > 0) { ... } // 5. Burn the final extracted tokens fungible_asset::burn(&store().burn_ref, extracted_fa); (amount_sent_ld, amount_received_ld) } ``` ```rust wrap theme={null} public(friend) fun credit( to: address, amount_ld: u64, src_eid: u32, lz_receive_value: Option, ): u64 acquires OftImpl { // 1. (Optional) deposit crosschain wrapped asset to the admin option::for_each(lz_receive_value, |fa| primary_fungible_store::deposit(@oft_admin, fa)); // 2. Release rate limit capacity for net inflows release_rate_limit_capacity(src_eid, amount_ld); // 3. Mint the tokens to the final recipient (or redirect if blocklisted) primary_fungible_store::mint( &store().mint_ref, redirect_to_admin_if_blocklisted(to, amount_ld), amount_ld ); amount_ld } ``` You will want to use the `oft::oft_fa` implementation when you want: * a brand new token on Aptos representing the crosschain supply. * each chain to independently mint/burn. * to bridge a new “canonical” supply for an existing non-Aptos asset on an Aptos chain. ## Adapter OFT: `oft::oft_adapter_fa` Instead of burning/minting tokens, the module **locks** tokens into an escrow on send and **unlocks** them from escrow on receive. This can be used if you already have an existing token on an Aptos Move chain that can’t share its mint/burn capabilities. On EVM, you might see an OFT that uses a "lockbox" to hold user tokens, sending representations of that held asset crosschain. The approach is the same on Aptos: ```rust wrap theme={null} public(friend) fun debit_fungible_asset( sender: address, fa: &mut FungibleAsset, min_amount_ld: u64, dst_eid: u32, ): (u64, u64) acquires OftImpl { // 1. Check blocklist assert_not_blocklisted(sender); // 2. Determine the “send” and “receive” amounts let (amount_sent_ld, amount_received_ld) = debit_view(amount_ld, min_amount_ld, dst_eid); // 3. Subtract fees if any let extracted_fa = fungible_asset::extract(fa, amount_sent_ld); if (fee_ld > 0) { ... } // 4. Deposit the net tokens into an “escrow” account primary_fungible_store::deposit(escrow_address(), extracted_fa); (amount_sent_ld, amount_received_ld) } ``` ```rust wrap theme={null} public(friend) fun credit( to: address, amount_ld: u64, src_eid: u32, lz_receive_value: Option, ): u64 acquires OftImpl { // 1. (Optional) deposit crosschain “wrapped” asset to admin option::for_each(lz_receive_value, |fa| primary_fungible_store::deposit(@oft_admin, fa)); // 2. Release rate limit capacity release_rate_limit_capacity(src_eid, amount_ld); // 3. Unlock from escrow into the final recipient let escrow_signer = &object::generate_signer_for_extending(&store().escrow_extend_ref); primary_fungible_store::transfer( escrow_signer, metadata(), redirect_to_admin_if_blocklisted(to, amount_ld), amount_ld ); amount_ld } ``` You will want to use the `oft::oft_adapter_fa` implementation when you: * have a pre-existing token on Aptos and cannot or do not want to grant mint/burn to the bridging contract. The adapter approach “locks” user tokens, so be mindful of ensuring adequate liquidity in the adapter if bridging in from other chains. Typically, only one chain uses the adapter approach (since the “escrow” is meant to represent the supply on that chain). Other chains should use full mint/burn logic. ## Common Interface: `oft::oft` Both **`oft_fa`** and **`oft_adapter_fa`** feed into the same top-level interface (`oft::oft`). This module: * Exposes user-facing functions like `send_withdraw(...)`, `send(...)`, `quote_oft(...)`, etc. * Delegates the actual bridging logic to either “OFT” or "OFT Adapter" code (by depending on whichever you’ve chosen). * Implements the final `lz_receive_impl(...)` function so that crosschain messages from `oft_core` eventually call your `credit(...)`. Example from `oft.move`: ```rust wrap theme={null} public entry fun send_withdraw( account: &signer, dst_eid: u32, to: vector, amount_ld: u64, ... ) { // 1. Withdraw tokens from user let send_value = primary_fungible_store::withdraw(account, metadata(), amount_ld); // 2. Withdraw crosschain fees let (native_fee_fa, zro_fee_fa) = withdraw_lz_fees(account, native_fee, zro_fee); // 3. Call OFT core “send” logic send_internal( sender, dst_eid, to_bytes32(to), &mut send_value, ... ); // 4. Refund leftover fees & deposit any leftover tokens refund_fees(sender, native_fee_fa, zro_fee_fa); primary_fungible_store::deposit(sender, send_value); } ``` ## Core Logic: `oft::oft_core` Regardless of whether it’s an **OFT** or **OFT Adapter**, the crosschain bridging sequence is the same: 1. **`send(...)`** – Encodes the message, calls your `debit` function, and dispatches it over the LayerZero Endpoint. 2. **`receive(...)`** – Decodes the message, calls your `credit` function, and optionally calls “compose” logic if there is a follow-up message. In an EVM environment, OFT variants do something similar with `_burn`, `_mint`, or `_transfer`. The separation is conceptually the same. ```rust wrap theme={null} public(friend) inline fun send( user_sender: address, dst_eid: u32, to: Bytes32, compose_payload: vector, send_impl: |vector, vector| MessagingReceipt, debit: |bool| (u64, u64), build_options: |u64, u16| vector, inspect: |&vector, &vector|, ): (MessagingReceipt, u64, u64) { let (amount_sent_ld, amount_received_ld) = debit(true); // Construct the message to contain 'amount_received_ld' and 'to' address let (message, msg_type) = encode_oft_msg(user_sender, amount_received_ld, to, compose_payload); let options = build_options(amount_received_ld, msg_type); inspect(&message, &options); let messaging_receipt = send_impl(message, options); // Emit an event for crosschain reference ... } ``` ## Implementation Config: `oft::oft_impl_config` Both **OFT** and **OFT Adapter** share the same configuration for: * **Fees**: `fee_bps`, `fee_deposit_address`. * **Blocklist**: Addresses can be disallowed from sending. Inbound tokens to them are re-routed to the admin. * **Rate Limits**: Each endpoint (chain) can be rate-limited to prevent large surges of bridging. Example for setting fees: ```rust wrap theme={null} public entry fun set_fee_bps(admin: &signer, fee_bps: u64) { assert_admin(address_of(admin)); oft_impl_config::set_fee_bps(fee_bps); } ``` ## Internal Store: `oft::oft_store` Holds two critical values: 1. **`shared_decimals`**: The universal decimals used across all chains. 2. **`decimal_conversion_rate`**: The factor bridging from local decimals to shared decimals. This matches the approach on EVM-based OFT, where you might define a consistent “decimals” across all chains, and each chain adapts locally if it wants a different local representation. ```rust wrap theme={null} public(friend) fun initialize(shared_decimals: u8, decimal_conversion_rate: u64) acquires OftStore { assert!(store().decimal_conversion_rate == 0, EALREADY_INITIALIZED); store_mut().shared_decimals = shared_decimals; store_mut().decimal_conversion_rate = decimal_conversion_rate; } ``` ## Comparison Between OFT and OFT Adapter | Feature | **OFT (mint/burn)** | **OFT Adapter (lock/unlock)** | | ----------------------------------- | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Token Ownership / Supply | The OFT can create (mint) or destroy (burn) tokens. Perfect for brand-new token supply across multiple chains. | The OFT does **not** create or destroy tokens. It merely locks them into an escrow, then unlocks them upon crosschain receive. | | Use Case | Great for truly omnichain tokens that unify supply. Each chain can hold a minted portion. | Ideal if an existing token is already deployed, and you can’t share mint/burn privileges with the bridging contract. | | Implementation Module | `oft_fa.move` | `oft_adapter_fa.move` | | `credit(...)` Behavior | **Mint** the inbound tokens for the recipient. | **Unlock** from escrow and deposit to the recipient. | | `debit(...)` Behavior | **Burn** from the sender’s local supply. | **Lock** tokens in an escrow address. | | Rebalancing or Liquidity Management | Not required for new tokens (the total supply is burned on one side, minted on the other). | Must ensure enough tokens remain in escrow to handle inbound “unlocks” from other chains. If many tokens flow out, local liquidity may be depleted. | ## Putting It All Together 1. **Deploy & Initialize** * Deploy the modules (`oft_adapter_fa`, `oft_fa`, `oft`, etc.). * Call `init_module` or `init_module_for_test`. * For the chosen path (native vs. adapter), run the relevant `initialize(...)` function (e.g., `oft_fa.initialize` or `oft_adapter_fa.initialize`). 2. **Configure** * Adjust fees, blocklists, or rate-limits using `oft::oft_impl_config`. * For the adapter approach, ensure the escrow has enough tokens to handle inbound bridging from other chains. 3. **Sending** * A user calls `send_withdraw(...)` from `oft::oft`. * This performs the local “debit” logic (burn or lock) and constructs a crosschain message. * Then calls the underlying `lz_send(...)` from the OApp layer. 4. **Receiving** * The LayerZero Executor calls your OApp’s `lz_receive_impl(...)`. * This triggers `oft_core::receive(...)`, which decodes the message and calls your `credit(...)` logic (mint or unlock). 5. **Monitor** * Check events: `OftSent` and `OftReceived` in `oft_core`. * Track blocklist changes, fee deposit addresses, and rate limit usage in `oft_impl_config`. ## Conclusion Whether you choose an **OFT** (mint/burn) or an **OFT Adapter** (lock/unlock): * The **core bridging** is consistent with the **LayerZero V2 OFT Standard** on EVM/Solana. * **Fee, blocklist, and rate-limit** logic is shared in `oft_impl_config`. * **Message encoding/decoding** and **compose** features align with `oft_core`. * **Shared decimals** plus local decimals ensure consistent crosschain supply. **OFT** are perfect for new tokens that do not exist outside of the bridging context, while **OFT Adapter** allow you to adopt bridging on an existing, fully deployed token. Both approaches integrate seamlessly with LayerZero’s crosschain messaging on Aptos, providing a robust, modular framework for omnichain fungible tokens. # Quickstart - Create Your First Omnichain App Source: https://docs.layerzero.network/v2/developers/aptos-move/create-lz-oapp/start This guide will walk you through the process of sending a simple crosschain message using LayerZero. We cover both the traditional EVM setup as well as... This guide will walk you through the process of sending a simple crosschain message using LayerZero. We cover both the traditional EVM setup as well as the Aptos (Move‑VM) approach. Choose the section that matches your target environment. LayerZero enables seamless communication between different blockchain networks. In these examples, an action on one chain (e.g. **Ethereum**) triggers a reaction on another (e.g. **Aptos**) without a central relay. Diagram showing crosschain messaging between two blockchain networks (e.g., Ethereum and Aptos), with an arrow indicating the message flow via LayerZero Send and Receive Diagram showing crosschain messaging between two blockchain networks (e.g., Ethereum and Aptos), with an arrow indicating the message flow via LayerZero Send and Receive ## Introduction LayerZero powers omnichain applications (OApps) by enabling cross‑chain messaging. These guides provide step‑by‑step instructions on deploying a simple OApp across chains—using an opinionated default configuration to ease the process. We present two variants: * **EVM-Based:** Using Hardhat (and Foundry) to deploy and wire Solidity contracts. * **Aptos-Based:** Using the Aptos CLI and Move‑VM scripts to deploy and configure your omnichain app (OFT) on Aptos alongside your EVM deployments. ### Disclaimer The Aptos CLI version used in the LayerZero devtools repo is v6.0.1. You can follow the examples and optionally try experimental builds. In the meantime, follow the examples for using the Aptos Typescript SDK to [**deploy and wire**](../configuration/dvn-executor-config). # LayerZero V2 Aptos Move Standards Source: https://docs.layerzero.network/v2/developers/aptos-move/overview Overview of Aptos Move Standards on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure crosschain... **Move** is a safe and flexible programming language for smart contracts, initially developed for the Libra (now Diem) blockchain and later adopted by blockchains like Aptos. With the introduction of LayerZero support for **Aptos Move**, developers can now build omnichain applications (OApps) on Aptos Move-based chains such as **Aptos**, **Initia**, and **Movement**. All of these chains utilize the same version of Move based on the [**Aptos flavor**](https://aptos.dev/en), meaning the Move modules in this section all natively support each chain. ## LayerZero Move Contract Standards The Omnichain Application (OApp) Standard, the boilerplate base for implementing crosschain messaging. Extension of OApp, combining the fungible token standard with core bridge logic to create Omnichain Fungible Tokens. ## Configuration Configure which decentralized verifier networks (DVNs) and Executors secure your messages. Configure how much gas limit and native gas token should be delivered during message execution. Configure which decentralized verifier networks (DVNs) and Executors secure your messages.
To find all of LayerZero's contracts for Aptos Move, visit the [**LayerZero V2 Protocol Repo**](https://github.com/LayerZero-Labs/LayerZero-v2/packages/layerzero-v2/aptos/contracts). ## Tooling LayerZero provides developer tooling to simplify the contract creation, testing, and deployment process on Move-based chains: * [LayerZero Scan](/v2/developers/layerzero-scan-explorer): A comprehensive crosschain explorer, search, API, and analytics platform for tracking and debugging your omnichain transactions. You can also ask for help or follow development in the Discord. # Ecosystem Tools Source: https://docs.layerzero.network/v2/developers/ecosystem-tools Build Ecosystem Tools with LayerZero V2. Developer guide with code examples, configuration, and best practices. LayerZero enables secure crosschain messaging. # Omnichain Composers Source: https://docs.layerzero.network/v2/developers/evm/composer/overview Overview of Omnichain Composers on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure crosschain... Crosschain composability has long been a goal for developers building advanced, interconnected decentralized applications. LayerZero V2 introduces **horizontal composability** — a concept that empowers developers to spread out crosschain calls into multiple, discrete steps. ## Prerequisites Before diving into LayerZero V2 Horizontal Composability, it's essential to have a foundational understanding of the following concepts: * **[Solidity Interfaces](https://blog.paulmcaviney.ca/solidity-interfaces)**: Knowledge of defining and implementing interfaces in Solidity. * **[Solidity Interface Composability](https://dev.to/shlok2740/interfaces-in-solidity-26m3#:~:text=Interfaces%20allow%20for%20composability%20between,any%20contract%20that%20implements%20it.)**: Grasping how interfaces facilitate composability between contracts. Having familiarity with these topics will enable a smoother comprehension of the concepts discussed. ## Workflow LayerZero V2 supports both **Vertical and Horizontal Composability** within crosschain calls. ### What is Vertical Composability? **Vertical Composability** is the traditional model of composability in blockchain applications, where multiple function calls from different contracts are stacked within a single transaction. ```solidity wrap theme={null} // Example of vertical composability with atomicity function _lzReceive( Origin calldata /*_origin*/, bytes32 /*_guid*/, bytes calldata /*_message*/, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { contractA.functionA(); contractB.functionB(); contractC.functionC(); // If any of the above calls fail, the entire transaction reverts } ``` All function calls in the stack execute atomically. This means that either all operations succeed, or the entire transaction reverts if any single operation fails. Vertical composability can present potential **Atomicity Issues** in crosschain interactions: * If an operation on one contract fails, it can produce unintended reversions or inconsistencies across the entire stack. This limits the ability to have instant finality guarantees when receiving crosschain messages. In crosschain contracts, you should minimize the impact of potential message failure by performing only one action per message. ### What is Horizontal Composability? **Horizontal Composability** is an implementation in **LayerZero V2** to address the limitations of vertical composability in crosschain interactions. Unlike vertical composability, which relies on a single, linear stack of function calls, horizontal composability allows for multiple, sequential calls across different chains within a single overarching operation. This facilitates the orchestration of complex, multi-step interactions across multiple chains without being constrained by the depth or complexity of a single call stack. ### How Horizontal Composability Works LayerZero's horizontal composability leverages composed messages that are treated as separate, containerized message packets. These packets are processed independently, allowing for more flexible and controlled interactions across chains. **Workflow Overview:** 1. **Sending Application Logic:** The sender application uses the `OApp._lzSend()` function to dispatch a crosschain message. 2. **Receiving Application Logic:** A destination application receives the message from `EndpointV2.lzReceive()`, does some state change, and then calls `EndpointV2.sendCompose()` to send a new message to the target composer. Crucially, either the `sender` or `receiver` should construct an additional message directed at a `composer`, which will handle subsequent operations in a new method, `EndpointV2.lzCompose()`. This dual-message approach ensures that both the immediate and follow-up actions are clearly defined and routed appropriately. 3. **Composer Application Logic:** A composer application receives the composed message in `lzCompose()` and does a state change to follow up on the first state changes created in `lzReceive()`. This workflow creates a way for delivering some critical state change information in separate steps, reducing the complexity of the call stack and enabling non-critical reverts on the destination chain. ### Horizontally Composing Supported Contracts Implementing horizontal composability involves crafting composed messages to expand on existing crosschain contract workflows. By default, both the `OFT` and `ONFT` standards support horizontally composed calls out of the box. This allows `OFT` or `ONFT` token holders to send tokens crosschain to a trusted `composer` contract on the destination, and trigger some action on behalf of the token holders (e.g., token swaps, token staking, etc). For more advanced implementations, you can design complex `OApp` contracts that have other crosschain `composer` implications. ## Installation To create a `composer` contract, you can install the [OApp package](https://www.npmjs.com/package/@layerzerolabs/oapp-evm) to an existing project: ```bash wrap npm theme={null} npm install @layerzerolabs/oapp-evm ``` ```bash wrap yarn theme={null} yarn add @layerzerolabs/oapp-evm ``` ```bash wrap pnpm theme={null} pnpm add @layerzerolabs/oapp-evm ``` ```bash wrap forge theme={null} forge install layerzero-labs/devtools --no-commit forge install layerzero-labs/LayerZero-v2 --no-commit forge install OpenZeppelin/openzeppelin-contracts --no-commit git submodule add https://github.com/GNSPS/solidity-bytes-utils.git lib/solidity-bytes-utils ``` Then add to your `foundry.toml` under `[profile.default]`: ```toml wrap forge theme={null} [profile.default] src = "src" out = "out" libs = ["lib"] remappings = [ '@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/', '@layerzerolabs/lz-evm-protocol-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/protocol', '@layerzerolabs/lz-evm-messagelib-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/messagelib', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', 'solidity-bytes-utils/=lib/solidity-bytes-utils/', ] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options ``` LayerZero contracts work with both [**OpenZeppelin V5**](https://docs.openzeppelin.com/contracts/5.x/access-control#ownership-and-ownable) and V4 contracts. Specify your desired version in your project's `package.json`: ```typescript wrap theme={null} "resolutions": { "@openzeppelin/contracts": "^5.0.1", } ``` ## Usage To implement a `composer` contract, simply inherit the `IOAppComposer.sol` interface from the `oapp-evm` package: ```solidity wrap theme={null} // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import { IOAppComposer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol"; /** * @title Composer * @notice Demonstrates the minimum `IOAppComposer` interface necessary to receive composed messages via LayerZero. * @dev Implements the `lzCompose` function to process incoming composed messages. */ contract Composer is IOAppComposer { /** * @notice Address of the LayerZero Endpoint. */ address public immutable endpoint; /** * @notice Address of the OApp that is sending the composed message. */ address public immutable oApp; /** * @notice Constructs the contract and initializes state variables. * @dev Stores the LayerZero Endpoint and OApp addresses. * * @param _endpoint The address of the LayerZero Endpoint. * @param _oApp The address of the OApp that is sending composed messages. */ constructor(address _endpoint, address _oApp) { endpoint = _endpoint; oApp = _oApp; } /** * @notice Handles incoming composed messages from LayerZero. * @dev Ensures the message comes from the correct OApp and is sent through the authorized endpoint. * * @param _oApp The address of the OApp that is sending the composed message. */ function lzCompose( address _oApp, bytes32 /* _guid */, bytes calldata /* _message */, address /* _executor */, bytes calldata /* _extraData */ ) external payable override { // Ensure the composed message comes from the correct OApp. require(_oApp == oApp, "ComposedReceiver: Invalid OApp"); require(msg.sender == endpoint, "ComposedReceiver: Unauthorized sender"); // ... execute logic for handling composed messages } } ``` ### Composed Message Execution Options Longer `composer` messages, which contain more bytes encoded instructions, increase the cost of calling `EndpointV2.lzReceive()`. Typically, the reason for the gas increase can be found in the additional length being added to your crosschain message, as well as the cost of invoking `EndpointV2.sendCompose()` inside your `OApp._lzReceive()` function. Ensure that when calling `OFT.send()` and `ONFT.send()` or your own custom OApp, that you correctly estimate the cost of calling `endpoint.sendCompose()` and add the additional `LzReceiveOption` gas limit to your `SendParam.extraOptions` or OApp specific `options` argument: ```ts wrap theme={null} // addExecutorLzReceiveOption(uint128 _gas, uint128 _value) Options.newOptions().addExecutorLzReceiveOption(50000, 0); ``` Besides the increase cost of `EndpointV2.lzReceive()`, you should also take into account the cost of your actual `composer.lzCompose()`. Similar to lzReceive(), you can specify the `gas limit` and `msg.value` the Executor should use when calling the `composer` contract: ```ts wrap theme={null} // addExecutorLzComposeOption(uint16 _index, uint128 _gas, uint128 _value) Options.newOptions().addExecutorLzReceiveOption(50000, 0).addExecutorLzComposeOption(0, 30000, 0); ``` * **`_index`:** Identifies the specific composed call within a batch of composed messages. This allows for distinct execution settings for each call. * **`_gas`:** Specifies the gas limit allocated for the composed call's execution on the destination chain. Gas requirements may vary across chains due to different opcode costs and gas mechanisms. * **`_value`:** Determines the amount of native currency (e.g., ETH) to be sent alongside the composed call, facilitating payable functions or covering additional costs. Review the existing documentation on [Message Execution Options](../configuration/options) to learn more. If not enough `gas limit` or `msg.value` is provided, the `EndpointV2.lzReceive()` will not execute, and will need to be manually retried either via the LayerZero Scan explorer, or manual contract call. ### Composing an OFT / ONFT Both the `OFT` and `ONFT` support sending a composed message along with the crosschain token transfers. ```solidity wrap OFT theme={null} // IOFT.sol /** * @dev Struct representing token parameters for the OFT send() operation. */ struct SendParam { uint32 dstEid; // Destination endpoint ID. // highlight-next-line bytes32 to; // Composer address. uint256 amountLD; // Amount to send in local decimals. uint256 minAmountLD; // Minimum amount to send in local decimals. // highlight-next-line bytes extraOptions; // Compose options supplied by the caller to be used in the LayerZero message. // highlight-next-line bytes composeMsg; // The composed message for the send() operation. bytes oftCmd; // The OFT command to be executed, unused in default OFT implementations. } ``` ```solidity wrap ONFT theme={null} // IONFT.sol /** * @dev Struct representing token parameters for the ONFT send() operation. */ struct SendParam { uint32 dstEid; // Destination LayerZero EndpointV2 ID. // highlight-next-line bytes32 to; // Composer address. uint256 tokenId; // The ERC721 tokenId for the send() operation. // highlight-next-line bytes extraOptions; // Compose options supplied by the caller to be used in the LayerZero message. // highlight-next-line bytes composeMsg; // The composed message for the send() operation. bytes onftCmd; // The ONFT command to be executed, unused in default ONFT implementations. } ``` When calling `send()`, specify the `composer` as the to address, encode a `composeMsg` based on the composer's specification, and add a `ComposeExecutionOption` gas limit and/or msg.value depending on the composer's needs. When creating the `composeMsg`, the OFT / ONFT will already encode specific parameters along with your message for use in the composer. Below is how the `OFTCore` and `ONFT721Core` contracts encode the `composeMsg` and send it to the `composer`: ```solidity wrap OFT theme={null} // OFTCore.sol /** * @dev The `OFTMsgCodec` provides a helper function to extract the `composeMsg` from * the overall message. This ensures that the `composeMsg` is properly formed and can * be processed by the composer. * * @notice The `composeMsg` includes both: * - The `msg.sender` on the source chain (as bytes32). * - The actual `composeMsg` intended for the composer. * * @notice The final encoded message structure is: * abi.encodePacked(_sendTo, _amountShared, addressToBytes32(msg.sender), _composeMsg); */ using OFTMsgCodec for bytes; /** * @dev When sending a message, the `composeMsg` is encoded alongside standard parameters. */ (message, hasCompose) = OFTMsgCodec.encode(_sendParam.to, _toSD(_amountLD), _sendParam.composeMsg()); /** * @dev If the message is composed (i.e., it contains a `composeMsg`), * we extract it and send it to the composer. */ if (_message.isComposed()) { /** * @dev The `composeMsg` sent to the composer includes: * - `_origin.nonce` (to track the originating transaction). * - `_origin.srcEid` (the source chain endpoint ID). * - The actual `composeMsg` extracted from `_message`. */ bytes memory composeMsg = ONFTComposeMsgCodec.encode(_origin.nonce, _origin.srcEid, _message.composeMsg()); /** * @dev Sends the composed message to the specified `toAddress` (the composer). * * @notice The `composeIndex` is always `0` because batching is not implemented. * - If batching is added, the index will need to be properly tracked. */ endpoint.sendCompose(toAddress, _guid, 0 /* the index of composed message */, composeMsg); } ``` Below is how the `ONFT721Core` contract encodes the `composeMsg` and sends it to the `composer`: ```solidity wrap ONFT theme={null} // ONFT721Core.sol /** * @dev The `ONFT721MsgCodec` provides a helper function to extract the `composeMsg` from * the overall message. This ensures that the `composeMsg` is properly formed and can * be processed by the composer. * * @notice The `composeMsg` includes both: * - The `msg.sender` on the source chain (as bytes32). * - The actual `composeMsg` intended for the composer. * * @notice The final encoded message structure is: * abi.encodePacked(_sendTo, _tokenId, addressToBytes32(msg.sender), _composeMsg) */ using ONFT721MsgCodec for bytes; /** * @dev When sending a message, the `composeMsg` is encoded alongside standard parameters. */ (message, hasCompose) = ONFT721MsgCodec.encode(_sendParam.to, _sendParam.tokenId, _sendParam.composeMsg()); /** * @dev If the message is composed (i.e., it contains a `composeMsg`), * we extract it and send it to the composer. */ if (_message.isComposed()) { /** * @dev The `composeMsg` sent to the composer includes: * - `_origin.nonce` (to track the originating transaction). * - `_origin.srcEid` (the source chain endpoint ID). * - The actual `composeMsg` extracted from `_message`. */ bytes memory composeMsg = ONFTComposeMsgCodec.encode(_origin.nonce, _origin.srcEid, _message.composeMsg()); /** * @dev Sends the composed message to the specified `toAddress` (the composer). * * @notice The `composeIndex` is always `0` because batching is not implemented. * - If batching is added, the index will need to be properly tracked. */ endpoint.sendCompose(toAddress, _guid, 0 /* the index of composed message */, composeMsg); } ``` This means that in your composer application, you can decode the `msg.sender` for specific checks, along with the other composer encodings. ### Message Encoding Reference Both OFT and ONFT use a two-step message flow when composing. This section documents the message structures using OFT as the primary example. #### Source Chain Message When sending a crosschain transfer with a `composeMsg`, the token contract encodes the message for transit. For OFT, this uses `OFTMsgCodec`: ``` ┌──────────────────────────────────────────────────────────────────────────┐ │ OFT Message Layout │ ├────────────────────┬───────────────┬─────────────────────────────────────┤ │ sendTo │ amountSD │ composeMsg │ │ 32 bytes │ 8 bytes │ [composeFrom (32)][payload (var)] │ ├────────────────────┴───────────────┴─────────────────────────────────────┤ │ 32 40 │ └──────────────────────────────────────────────────────────────────────────┘ ``` | Bytes | Field | Type | Description | | ----- | ------------ | --------- | ------------------------------------------------------ | | 0–31 | `sendTo` | `bytes32` | Recipient address (composer contract if using compose) | | 32–39 | `amountSD` | `uint64` | Amount in shared decimals (6 decimal precision) | | 40+ | `composeMsg` | `bytes` | Optional: `[composeFrom (msg.sender)][your payload]` | #### Composed Message (What Your Composer Receives) After processing the token transfer in `_lzReceive()`, the destination contract re-encodes the data and calls `endpoint.sendCompose()`. This is the message your composer receives in `lzCompose()`. **OFT Composed Message** (`OFTComposeMsgCodec`): ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ OFT Composed Message Layout │ ├─────────┬─────────┬──────────────────┬──────────────────┬───────────────────┤ │ nonce │ srcEid │ amountLD │ composeFrom │ composeMsg │ │ 8 bytes │ 4 bytes │ 32 bytes │ 32 bytes │ │ ├─────────┴─────────┴──────────────────┴──────────────────┴───────────────────┤ │ 8 12 44 76 │ └─────────────────────────────────────────────────────────────────────────────┘ ``` | Bytes | Field | Type | Description | | ----- | ------------- | --------- | ---------------------------------------------------------- | | 0–7 | `nonce` | `uint64` | Unique identifier for tracking the originating transaction | | 8–11 | `srcEid` | `uint32` | Source endpoint ID (originating chain) | | 12–43 | `amountLD` | `uint256` | Amount of tokens in local decimals (full precision) | | 44–75 | `composeFrom` | `bytes32` | Address of the original sender on the source chain | | 76+ | `composeMsg` | `bytes` | The arbitrary payload you passed to `send()` | ### OFT: amountSD vs amountLD The OFT converts from shared decimals (`amountSD`, uint64) to local decimals (`amountLD`, uint256) before calling your composer. Your composer receives the **full precision** amount in the destination chain's native token decimals. **ONFT Composed Message** (`ONFTComposeMsgCodec`): The ONFT uses a simpler structure without an amount field (since NFTs are unique): | Bytes | Field | Type | Description | | ----- | ------------- | --------- | ---------------------------------------------------------- | | 0–7 | `nonce` | `uint64` | Unique identifier for tracking the originating transaction | | 8–11 | `srcEid` | `uint32` | Source endpoint ID (originating chain) | | 12–43 | `composeFrom` | `bytes32` | Address of the original sender on the source chain | | 44+ | `composeMsg` | `bytes` | The arbitrary payload you passed to `send()` | #### Codec Functions Import the appropriate codec in your composer contract: ```solidity wrap theme={null} // For OFT composers import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol"; // For ONFT composers import { ONFTComposeMsgCodec } from "@layerzerolabs/onft-evm/contracts/libs/ONFTComposeMsgCodec.sol"; ``` **OFTComposeMsgCodec Functions:** | Function | Returns | Description | | ---------------------------------- | -------------- | -------------------------------------------------------- | | `nonce(bytes calldata _msg)` | `uint64` | Extracts the unique transaction identifier | | `srcEid(bytes calldata _msg)` | `uint32` | Extracts the source endpoint ID (originating chain) | | `amountLD(bytes calldata _msg)` | `uint256` | Extracts the token amount in local decimals | | `composeFrom(bytes calldata _msg)` | `bytes32` | Extracts the original sender address on the source chain | | `composeMsg(bytes calldata _msg)` | `bytes memory` | Extracts the arbitrary payload passed to `send()` | | `addressToBytes32(address _addr)` | `bytes32` | Converts an address to bytes32 (left-padded with zeros) | | `bytes32ToAddress(bytes32 _b)` | `address` | Converts bytes32 back to an address | The `ONFTComposeMsgCodec` provides the same functions except `amountLD()` (since NFTs don't have amounts). The `composeFrom` field is useful for authorization checks or refunds back to the source chain. For example, see the following `composer` example which mocks an ERC20 token swap after receiving from an OFT: ```solidity wrap theme={null} // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IOAppComposer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol"; import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol"; /** * @title SwapMock Contract * @notice Mocks an ERC20 token swap in response to receiving an OFT message via LayerZero. * @dev This contract interacts with LayerZero's Omnichain Fungible Token (OFT) Standard, * processing incoming OFT messages (`lzCompose`) and executing a token swap action. */ contract SwapMock is IOAppComposer { using SafeERC20 for IERC20; /// @notice The ERC20 token used for swaps. IERC20 public erc20; /// @notice Address of the LayerZero Endpoint. address public immutable endpoint; /// @notice Address of the OApp that is sending the composed message. address public immutable oApp; /** * @notice Emitted when a token swap is executed. * @dev This event logs the swap details, including the recipient, token, and amount swapped. * * @param user The address of the user who receives the swapped tokens. * @param tokenOut The address of the ERC20 token being swapped. * @param amount The amount of tokens swapped. */ event Swapped(address indexed user, address tokenOut, uint256 amount); /** * @notice Constructs the `SwapMock` contract. * @dev Initializes the contract by setting the ERC20 token, LayerZero endpoint, and OApp address. * * @param _erc20 The address of the ERC20 token that will be used in swaps. * @param _endpoint The LayerZero Endpoint address. * @param _oApp The address of the OApp that is sending the composed message. */ constructor(address _erc20, address _endpoint, address _oApp) { erc20 = IERC20(_erc20); endpoint = _endpoint; oApp = _oApp; } /** * @notice Handles incoming composed messages from LayerZero and executes a token swap. * @dev Decodes the `composeMsg` from `_message`, extracts relevant parameters, and transfers * tokens to the intended recipient. * * The `message` is structured in the sender's contract and includes: * - `_nonce`: A unique identifier for tracking the message. * - `_srcEid`: The source endpoint ID, identifying the originating chain. * - `_amountLD`: The amount of tokens in local decimals being transferred. * - `_composeFrom`: The address of the original sender (encoded as `bytes32`). * - `_composeMsg`: The payload containing the recipient address. * * @param _oApp The address of the originating OApp. * @param _message The encoded message containing the `composeMsg`. */ function lzCompose( address _oApp, bytes32 /*_guid*/, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) external payable override { require(_oApp == oApp, "SwapMock: Invalid OApp"); require(msg.sender == endpoint, "SwapMock: Unauthorized sender"); // Decode the nonce (unique identifier for the transaction) uint64 _nonce = OFTComposeMsgCodec.nonce(_message); // Decode the source endpoint ID (originating chain) uint32 _srcEid = OFTComposeMsgCodec.srcEid(_message); // Decode the amount in local decimals being transferred uint256 _amountLD = OFTComposeMsgCodec.amountLD(_message); // Decode the `composeFrom` address (original sender) from bytes32 to address bytes32 _composeFromBytes = OFTComposeMsgCodec.composeFrom(_message); address _composeFrom = OFTComposeMsgCodec.bytes32ToAddress(_composeFromBytes); // Decode the actual `composeMsg` payload to extract the recipient address bytes memory _actualComposeMsg = OFTComposeMsgCodec.composeMsg(_message); address _receiver = abi.decode(_actualComposeMsg, (address)); // Execute the token swap by transferring `_amountLD` to `_receiver` erc20.safeTransfer(_receiver, _amountLD); // Emit an event for logging the swap details emit Swapped(_receiver, address(erc20), _amountLD); } } ``` ### Composing an OApp 1. **Source OApp:** Sends a crosschain message via `_lzSend()` to a destination chain. 2. **Destination OApp:** Receives the crosschain message via `_lzReceive()` and initiates composed calls using `EndpointV2.sendCompose()`: ```solidity wrap theme={null} /** * @dev Handles incoming LayerZero messages and sends a composed message using `endpoint.sendCompose()`. * @notice This function processes received packets and relays them to a composed receiver. * * @param _guid A globally unique identifier for tracking the packet. * @param payload The encoded message payload. */ function _lzReceive( Origin calldata /*_origin*/, bytes32 _guid, bytes calldata payload, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { /** * @dev Decode the payload based on the expected format from the sender application. * The structure of `payload` depends entirely on how the sender encoded it. * In this case, we assume the sender encoded a string message and a composer address. * If the sender encodes different types or a different order, this decoding must be updated accordingly. */ (string memory _message, address _composedAddress) = abi.decode(payload, (string, address)); // Store received data in the destination OApp data = _message; // Send a composed message to the composed receiver using the same GUID endpoint.sendCompose(_composedAddress, _guid, 0, payload); } ``` 3. **Composer:** Contracts that implement business logic to handle incoming composed messages via `EndpointV2.lzCompose()`. # EVM DVN and Executor Configuration Source: https://docs.layerzero.network/v2/developers/evm/configuration/dvn-executor-config Step-by-step guide to evm dvn and executor configuration using LayerZero V2. Build and deploy omnichain applications with crosschain messaging. Follow step-... Before setting your DVN and Executor Configuration, you should review the [Security Stack Core Concepts](../../../concepts/modular-security/security-stack-dvns). **Production deployments should use multiple required DVNs from independent operators.** A single-DVN configuration means a compromise of that one verifier results in unrestricted forged messages on the pathway. The configuration examples on this page that show `requiredDVNCount: 1` are illustrative only — production pathways should set `requiredDVNCount >= 2` with DVNs from different operators. See the [Integration Checklist](../../../tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway) for production DVN guidance. You can manually configure your EVM OApp's Send and Receive settings by: * **Reading Defaults:** Use the `getConfig` method to see default configurations. * **Setting Libraries:** Call `setSendLibrary` and `setReceiveLibrary` to choose the correct Message Library version. * **Setting Configs:** Use the `setConfig` function to update your custom DVN and Executor settings. For both Send and Receive configurations, make sure that for a given [channel](../../../concepts/glossary#channel--lossless-channel): * **Send (Chain A) settings** match the **Receive (Chain B) settings.** * DVN addresses are provided in alphabetical order. * Block confirmations are correctly set to avoid mismatches. ### Use the LayerZero CLI The LayerZero CLI has abstracted these calls for every supported chain. See the [**CLI Setup Guide**](../../../get-started/create-lz-oapp/start) to easily deploy, configure, and send messages using LayerZero. ### Self-Validation with `cast`
The recipes throughout this page use [Foundry's `cast`](https://book.getfoundry.sh/cast/) so you can read the exact on-chain values that govern message delivery on each pathway. Fill in the environment variables below once per pathway and the snippets in later sections will pick them up. Verify the `EndpointV2` address for your chain at [Deployed Contracts](../../../deployments/deployed-contracts) — the value shown is the current Ethereum mainnet address and most chains share it, but a handful do not. These snippets are EVM-only. Solana, Aptos, Sui, TON, Starknet, Stellar, and Tron OApps must use their respective tooling — see the per-VM configuration pages under [Developers](../../../developers). ```bash wrap theme={null} # LayerZero V2 — environment for self-validation snippets # Fill in for the pathway you are auditing. # Source chain (A) — where messages are sent FROM export RPC_A=https://... # JSON-RPC endpoint for chain A export ENDPOINT_A=0x1a44076050125825900e736c501f859c50fE728c # EndpointV2 on chain A export OAPP_A=0x... # Your OApp address on chain A export EID_B=30106 # Destination endpoint ID (chain B) # Destination chain (B) — where messages are RECEIVED export RPC_B=https://... export ENDPOINT_B=0x1a44076050125825900e736c501f859c50fE728c export OAPP_B=0x... export EID_A=30101 # Source endpoint ID (chain A) # Resolve libraries once per pathway export SEND_LIB_A=$(cast call "$ENDPOINT_A" "getSendLibrary(address,uint32)(address)" "$OAPP_A" "$EID_B" --rpc-url "$RPC_A") export RECV_LIB_B=$(cast call "$ENDPOINT_B" "getReceiveLibrary(address,uint32)(address,bool)" "$OAPP_B" "$EID_A" --rpc-url "$RPC_B" | head -1) ``` The `UlnConfig` tuple signature used in subsequent recipes is `(uint64 confirmations, uint8 requiredDVNCount, uint8 optionalDVNCount, uint8 optionalDVNThreshold, address[] requiredDVNs, address[] optionalDVNs)` and matches the struct defined in [`@layerzerolabs/lz-evm-protocol-v2`](../technical-reference/api#ulnconfig). Older deployments may use different library versions — confirm against the ABI for the library address you resolved above. ### Getting the Default Config You can easily fetch and decode your OApp’s current Send/Receive settings via `endpoint.getConfig(...)`. Below are two options: ```solidity wrap Foundry theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { console } from "forge-std/console.sol"; import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; import { ExecutorConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBase.sol"; /// @title GetConfigScript /// @notice Retrieves and logs the current configuration for the OApp. contract GetConfigScript is Script { /// @notice Calls getConfig on the specified LayerZero Endpoint. /// @dev Decodes the returned bytes as a UlnConfig. Logs some of its fields. /// @param rpcUrl The RPC URL for the target chain. /// @param endpoint The LayerZero Endpoint address. /// @param oapp The address of your OApp. /// @param lib The address of the Message Library (send or receive). /// @param eid The remote endpoint identifier. /// @param configType The configuration type (1 = Executor, 2 = ULN). function getConfig( string memory _rpcUrl, address _endpoint, address _oapp, address _lib, uint32 _eid, uint32 _configType ) external { // Create a fork from the specified RPC URL. vm.createSelectFork(_rpcUrl); vm.startBroadcast(); // Instantiate the LayerZero endpoint. ILayerZeroEndpointV2 endpoint = ILayerZeroEndpointV2(_endpoint); // Retrieve the raw configuration bytes. bytes memory config = endpoint.getConfig(_oapp, _lib, _eid, _configType); if (_configType == 1) { // Decode the Executor config (configType = 1) ExecutorConfig memory execConfig = abi.decode(config, (ExecutorConfig)); // Log some key configuration parameters. console.log("Executor Type:", execConfig.maxMessageSize); console.log("Executor Address:", execConfig.executor); } if (_configType == 2) { // Decode the ULN config (configType = 2) UlnConfig memory decodedConfig = abi.decode(config, (UlnConfig)); // Log some key configuration parameters. console.log("Confirmations:", decodedConfig.confirmations); console.log("Required DVN Count:", decodedConfig.requiredDVNCount); for (uint i = 0; i < decodedConfig.requiredDVNs.length; i++) { console.logAddress(decodedConfig.requiredDVNs[i]); } console.log("Optional DVN Count:", decodedConfig.optionalDVNCount); for (uint i = 0; i < decodedConfig.optionalDVNs.length; i++) { console.logAddress(decodedConfig.optionalDVNs[i]); } console.log("Optional DVN Threshold:", decodedConfig.optionalDVNThreshold); } vm.stopBroadcast(); } } ``` ```typescript wrap Ethers V5 theme={null} import * as ethers from 'ethers'; // Define provider const provider = new ethers.providers.JsonRpcProvider('YOUR_RPC_PROVIDER_HERE'); // Define the smart contract address and ABI const ethereumLzEndpointAddress = '0x1a44076050125825900e736c501f859c50fE728c'; const ethereumLzEndpointABI = [ 'function getConfig(address _oapp, address _lib, uint32 _eid, uint32 _configType) external view returns (bytes memory config)', ]; // Create a contract instance const contract = new ethers.Contract(ethereumLzEndpointAddress, ethereumLzEndpointABI, provider); // Define the addresses and parameters const oappAddress = '0xEB6671c152C88E76fdAaBC804Bf973e3270f4c78'; const sendLibAddress = '0xbB2Ea70C9E858123480642Cf96acbcCE1372dCe1'; const receiveLibAddress = '0xc02Ab410f0734EFa3F14628780e6e695156024C2'; const remoteEid = 30102; // Example target endpoint ID, Binance Smart Chain const executorConfigType = 1; // 1 for executor const ulnConfigType = 2; // 2 for UlnConfig async function getConfigAndDecode() { try { // Fetch and decode for sendLib (both Executor and ULN Config) const sendExecutorConfigBytes = await contract.getConfig( oappAddress, sendLibAddress, remoteEid, executorConfigType, ); const executorConfigAbi = ['tuple(uint32 maxMessageSize, address executor)']; const executorConfigArray = ethers.utils.defaultAbiCoder.decode( executorConfigAbi, sendExecutorConfigBytes, ); console.log('Send Library Executor Config:', executorConfigArray); const sendUlnConfigBytes = await contract.getConfig( oappAddress, sendLibAddress, remoteEid, ulnConfigType, ); const ulnConfigStructType = [ 'tuple(uint64 confirmations, uint8 requiredDVNCount, uint8 optionalDVNCount, uint8 optionalDVNThreshold, address[] requiredDVNs, address[] optionalDVNs)', ]; const sendUlnConfigArray = ethers.utils.defaultAbiCoder.decode( ulnConfigStructType, sendUlnConfigBytes, ); console.log('Send Library ULN Config:', sendUlnConfigArray); // Fetch and decode for receiveLib (only ULN Config) const receiveUlnConfigBytes = await contract.getConfig( oappAddress, receiveLibAddress, remoteEid, ulnConfigType, ); const receiveUlnConfigArray = ethers.utils.defaultAbiCoder.decode( ulnConfigStructType, receiveUlnConfigBytes, ); console.log('Receive Library ULN Config:', receiveUlnConfigArray); } catch (error) { console.error('Error fetching or decoding config:', error); } } // Execute the function getConfigAndDecode(); ``` ### Setting the Send and Receive Libraries ```solidity wrap Foundry theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; contract SetLibraries is Script { function run( address _endpoint, address _oapp, uint32 _eid, address _sendLib, address _receiveLib, address _signer ) external { ILayerZeroEndpointV2 endpoint = ILayerZeroEndpointV2(_endpoint); vm.startBroadcast(_signer); endpoint.setSendLibrary(_oapp, _eid, _sendLib); console.log("Send library set successfully."); endpoint.setReceiveLibrary(_oapp, _eid, _receiveLib); console.log("Receive library set successfully."); vm.stopBroadcast(); } } ``` ```typescript wrap Ethers V5 theme={null} const {ethers} = require('ethers'); // Replace with your actual values const YOUR_OAPP_ADDRESS = '0xYourOAppAddress'; const YOUR_SEND_LIB_ADDRESS = '0xYourSendLibAddress'; const YOUR_RECEIVE_LIB_ADDRESS = '0xYourReceiveLibAddress'; const YOUR_ENDPOINT_CONTRACT_ADDRESS = '0xYourEndpointContractAddress'; const YOUR_RPC_URL = 'YOUR_RPC_URL'; const YOUR_PRIVATE_KEY = 'YOUR_PRIVATE_KEY'; // Define the remote EID const remoteEid = 30101; // Replace with your actual EID // Set up the provider and signer const provider = new ethers.providers.JsonRpcProvider(YOUR_RPC_URL); const signer = new ethers.Wallet(YOUR_PRIVATE_KEY, provider); // Set up the endpoint contract const endpointAbi = [ 'function setSendLibrary(address oapp, uint32 eid, address sendLib) external', 'function setReceiveLibrary(address oapp, uint32 eid, address receiveLib) external', ]; const endpointContract = new ethers.Contract(YOUR_ENDPOINT_CONTRACT_ADDRESS, endpointAbi, signer); async function setLibraries() { try { // Set the send library const sendTx = await endpointContract.setSendLibrary( YOUR_OAPP_ADDRESS, remoteEid, YOUR_SEND_LIB_ADDRESS, ); console.log('Send library transaction sent:', sendTx.hash); await sendTx.wait(); console.log('Send library set successfully.'); // Set the receive library const receiveTx = await endpointContract.setReceiveLibrary( YOUR_OAPP_ADDRESS, remoteEid, YOUR_RECEIVE_LIB_ADDRESS, ); console.log('Receive library transaction sent:', receiveTx.hash); await receiveTx.wait(); console.log('Receive library set successfully.'); } catch (error) { console.error('Transaction failed:', error); } } setLibraries(); ``` ### Asymmetric Library Configuration Pin the send and receive libraries on **both** sides of every pathway. If one side calls `setSendLibrary` / `setReceiveLibrary` and the mirror leaves the library implicit, the implicit side will silently inherit whatever LayerZero Labs ships as that EID's default — and that default can change. **Do:** * Call `EndpointV2.setSendLibrary(oapp, dstEid, sendLib)` on the source side **and** `EndpointV2.setReceiveLibrary(oapp, srcEid, recvLib, gracePeriod)` on the destination side for the same pathway. * Pin the same library version on both sides — for example, `SendUln302` on the sender, `ReceiveUln302` on the receiver. * Re-run the validation snippet below any time you add a new chain, migrate to a new library version, or rotate the OApp's delegate. **Don't:** * Leave one side on the default library because it "works today." Defaults are mutable; LayerZero Labs may publish a new library version and roll the default forward without your involvement. * Assume `getSendLibrary` returning a non-zero address means the library is pinned — it falls through to `defaultSendLibrary` if the OApp has not set its own. Default libraries are not a contract — they are a setting LayerZero Labs controls. If a default migration ships while only one side of your pathway is implicit, the explicit and implicit sides drift, and messages already in flight may stop verifying until you `setConfig` against the new library address. #### How to check ```bash wrap theme={null} # Compare each OApp's effective library against the endpoint's default for that EID. APP_SEND_LIB=$(cast call "$ENDPOINT_A" "getSendLibrary(address,uint32)(address)" "$OAPP_A" "$EID_B" --rpc-url "$RPC_A") DEFAULT_SEND_LIB=$(cast call "$ENDPOINT_A" "defaultSendLibrary(uint32)(address)" "$EID_B" --rpc-url "$RPC_A") [ "$APP_SEND_LIB" = "$DEFAULT_SEND_LIB" ] && echo "A→B sender is on DEFAULT library" || echo "A→B sender pinned: $APP_SEND_LIB" APP_RECV_LIB=$(cast call "$ENDPOINT_B" "getReceiveLibrary(address,uint32)(address,bool)" "$OAPP_B" "$EID_A" --rpc-url "$RPC_B" | head -1) DEFAULT_RECV_LIB=$(cast call "$ENDPOINT_B" "defaultReceiveLibrary(uint32)(address)" "$EID_A" --rpc-url "$RPC_B") [ "$APP_RECV_LIB" = "$DEFAULT_RECV_LIB" ] && echo "B receive is on DEFAULT library" || echo "B receive pinned: $APP_RECV_LIB" ``` If exactly one of the two prints `DEFAULT`, the pathway is asymmetric — pin both sides to the same explicit library. `getSendLibrary` returning the same address as `defaultSendLibrary` does **not** prove the OApp is on the default. The OApp may have explicitly called `setSendLibrary` with the default's address, in which case the side is pinned even though the equality test reports `DEFAULT`. To disambiguate, read `sendLibrary[oapp][eid]` directly from the endpoint: a value equal to the `DEFAULT_LIB` sentinel means implicit, any other address (including one that happens to equal the current default) means explicitly pinned. The same caveat applies to `getReceiveLibrary` / `defaultReceiveLibrary`. ### Setting Custom Send Config (DVN & Executor) In this example, we configure both the ULN (DVN settings) and Executor settings on the sending chain. ```solidity wrap Foundry theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { ILayerZeroEndpointV2, SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; import { ExecutorConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBase.sol"; /// @title LayerZero Send Configuration Script /// @notice Defines and applies ULN (DVN) + Executor configs for cross‑chain messaging via LayerZero Endpoint V2. contract SetSendConfig is Script { uint32 constant EXECUTOR_CONFIG_TYPE = 1; uint32 constant ULN_CONFIG_TYPE = 2; /// @notice Broadcasts transactions to set both Send ULN and Executor configurations function run() external { address endpoint = vm.envAddress("SOURCE_ENDPOINT_ADDRESS"); address oapp = vm.envAddress("SENDER_OAPP_ADDRESS"); uint32 eid = uint32(vm.envUint("REMOTE_EID")); address sendLib = vm.envAddress("SEND_LIB_ADDRESS"); address signer = vm.envAddress("SIGNER"); /// @notice ULNConfig defines security parameters (DVNs + confirmation threshold) /// @notice Send config requests these settings to be applied to the DVNs and Executor /// @dev 0 values will be interpretted as defaults, so to apply NIL settings, use: /// @dev uint8 internal constant NIL_DVN_COUNT = type(uint8).max; /// @dev uint64 internal constant NIL_CONFIRMATIONS = type(uint64).max; UlnConfig memory uln = UlnConfig({ confirmations: 15, // minimum block confirmations required requiredDVNCount: 2, // number of DVNs required optionalDVNCount: type(uint8).max, // optional DVNs count, uint8 optionalDVNThreshold: 0, // optional DVN threshold requiredDVNs: [address(0x1111...), address(0x2222...)], // sorted list of required DVN addresses optionalDVNs: [] // sorted list of optional DVNs }); /// @notice ExecutorConfig sets message size limit + fee‑paying executor ExecutorConfig memory exec = ExecutorConfig({ maxMessageSize: 10000, // max bytes per crosschain message executor: address(0x3333...) // address that pays destination execution fees }); bytes memory encodedUln = abi.encode(uln); bytes memory encodedExec = abi.encode(exec); SetConfigParam[] memory params = new SetConfigParam[](2); params[0] = SetConfigParam(eid, EXECUTOR_CONFIG_TYPE, encodedExec); params[1] = SetConfigParam(eid, ULN_CONFIG_TYPE, encodedUln); vm.startBroadcast(signer); ILayerZeroEndpointV2(endpoint).setConfig(oapp, sendLib, params); vm.stopBroadcast(); } } ``` ```typescript wrap Ethers V5 theme={null} const {ethers} = require('ethers'); // Addresses const oappAddress = 'YOUR_OAPP_ADDRESS'; // Replace with your OApp address const sendLibAddress = 'YOUR_SEND_LIB_ADDRESS'; // Replace with your send message library address // Configuration // UlnConfig controls verification threshold for incoming messages // Receive config enforces these settings have been applied to the DVNs and Executor // 0 values will be interpretted as defaults, so to apply NIL settings, use: // uint8 internal constant NIL_DVN_COUNT = type(uint8).max; // uint64 internal constant NIL_CONFIRMATIONS = type(uint64).max; const remoteEid = 30101; // Example EID, replace with the actual value const ulnConfig = { confirmations: 99, // Example value, replace with actual requiredDVNCount: 2, // Example value, replace with actual optionalDVNCount: 0, // Example value, replace with actual optionalDVNThreshold: 0, // Example value, replace with actual requiredDVNs: ['0xDvnAddress1', '0xDvnAddress2'], // Replace with actual addresses, must be in alphabetical order optionalDVNs: [], // Replace with actual addresses, must be in alphabetical order }; const executorConfig = { maxMessageSize: 10000, // Example value, replace with actual executor: '0xExecutorAddress', // Replace with the actual executor address }; // Provider and Signer const provider = new ethers.providers.JsonRpcProvider(YOUR_RPC_URL); const signer = new ethers.Wallet(YOUR_PRIVATE_KEY, provider); // ABI and Contract const endpointAbi = [ 'function setConfig(address oappAddress, address sendLibAddress, tuple(uint32 eid, uint32 configType, bytes config)[] setConfigParams) external', ]; const endpointContract = new ethers.Contract(YOUR_ENDPOINT_CONTRACT_ADDRESS, endpointAbi, signer); // Encode UlnConfig using defaultAbiCoder const configTypeUlnStruct = 'tuple(uint64 confirmations, uint8 requiredDVNCount, uint8 optionalDVNCount, uint8 optionalDVNThreshold, address[] requiredDVNs, address[] optionalDVNs)'; const encodedUlnConfig = ethers.utils.defaultAbiCoder.encode([configTypeUlnStruct], [ulnConfig]); // Encode ExecutorConfig using defaultAbiCoder const configTypeExecutorStruct = 'tuple(uint32 maxMessageSize, address executor)'; const encodedExecutorConfig = ethers.utils.defaultAbiCoder.encode( [configTypeExecutorStruct], [executorConfig], ); // Define the SetConfigParam structs const setConfigParamUln = { eid: remoteEid, configType: 2, // ULN_CONFIG_TYPE config: encodedUlnConfig, }; const setConfigParamExecutor = { eid: remoteEid, configType: 1, // EXECUTOR_CONFIG_TYPE config: encodedExecutorConfig, }; // Send the transaction async function sendTransaction() { try { const tx = await endpointContract.setConfig( oappAddress, sendLibAddress, [setConfigParamUln, setConfigParamExecutor], // Array of SetConfigParam structs ); console.log('Transaction sent:', tx.hash); const receipt = await tx.wait(); console.log('Transaction confirmed:', receipt.transactionHash); } catch (error) { console.error('Transaction failed:', error); } } sendTransaction(); ``` ### Setting Custom Receive Config (DVN Only) On the receiving chain, only the ULN (DVN) configuration is needed since the Executor is not enforced on destination (i.e., the call can be made by anyone without permission). This config enforces all of the configuration settings from the source chain. Ensure that the DVNs in this config object match the sender side of the channel, otherwise messages will be blocked. Blocked messages can be caused by: * **Mismatch of block confirmations:** if source block confirmations are less than the destination * **Mismatch of DVNs:** the source DVNs do not match the threshold requirements of the destination A mismatch will result in a config error, and in some cases can result in a loss of funds if not caught. Since anyone can call `endpoint.lzReceive(...)` for a verified LayerZero message, if you require specific execution requirements you will need to enforce them in your child contract's internal `_lzReceive(...)`. See the [**Integration Checklist**](../../../tools/integration-checklist#enforce-msgvalue-in-_lzreceive-and-lzcompose) for more details.
```solidity wrap Foundry theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { ILayerZeroEndpointV2, SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; /// @title LayerZero Receive Configuration Script /// @notice Defines and applies ULN (DVN) config for inbound message verification via LayerZero Endpoint V2. contract SetReceiveConfig is Script { uint32 constant RECEIVE_CONFIG_TYPE = 2; function run() external { address endpoint = vm.envAddress("ENDPOINT_ADDRESS"); address oapp = vm.envAddress("OAPP_ADDRESS"); uint32 eid = uint32(vm.envUint("REMOTE_EID")); address receiveLib= vm.envAddress("RECEIVE_LIB_ADDRESS"); address signer = vm.envAddress("SIGNER"); /// @notice UlnConfig controls verification threshold for incoming messages /// @notice Receive config enforces these settings have been applied to the DVNs and Executor /// @dev 0 values will be interpretted as defaults, so to apply NIL settings, use: /// @dev uint8 internal constant NIL_DVN_COUNT = type(uint8).max; /// @dev uint64 internal constant NIL_CONFIRMATIONS = type(uint64).max; UlnConfig memory uln = UlnConfig({ confirmations: 15, // min block confirmations from source requiredDVNCount: 2, // required DVNs for message acceptance optionalDVNCount: type(uint8).max, // optional DVNs count optionalDVNThreshold: 0, // optional DVN threshold requiredDVNs: [address(0x1111...), address(0x2222...)], // sorted required DVNs optionalDVNs: [] // no optional DVNs }); bytes memory encodedUln = abi.encode(uln); SetConfigParam[] memory params = new SetConfigParam[](1); params[0] = SetConfigParam(eid, RECEIVE_CONFIG_TYPE, encodedUln); vm.startBroadcast(signer); ILayerZeroEndpointV2(endpoint).setConfig(oapp, receiveLib, params); vm.stopBroadcast(); } } ``` ```typescript wrap Ethers V5 theme={null} const {ethers} = require('ethers'); // Addresses const oappAddress = 'YOUR_OAPP_ADDRESS'; // Replace with your OApp address const receiveLibAddress = 'YOUR_RECEIVE_LIB_ADDRESS'; // Replace with your receive message library address // Configuration const remoteEid = 30101; // Example EID, replace with the actual value const ulnConfig = { confirmations: 99, // Example value, replace with actual requiredDVNCount: 2, // Example value, replace with actual optionalDVNCount: 0, // Example value, replace with actual optionalDVNThreshold: 0, // Example value, replace with actual requiredDVNs: ['0xDvnAddress1', '0xDvnAddress2'], // Replace with actual addresses, must be in alphabetical order optionalDVNs: [], // Replace with actual addresses, must be in alphabetical order }; // Provider and Signer const provider = new ethers.providers.JsonRpcProvider(YOUR_RPC_URL); const signer = new ethers.Wallet(YOUR_PRIVATE_KEY, provider); // ABI and Contract const endpointAbi = [ 'function setConfig(address oappAddress, address receiveLibAddress, tuple(uint32 eid, uint32 configType, bytes config)[] setConfigParams) external', ]; const endpointContract = new ethers.Contract(YOUR_ENDPOINT_CONTRACT_ADDRESS, endpointAbi, signer); // Encode UlnConfig using defaultAbiCoder const configTypeUlnStruct = 'tuple(uint64 confirmations, uint8 requiredDVNCount, uint8 optionalDVNCount, uint8 optionalDVNThreshold, address[] requiredDVNs, address[] optionalDVNs)'; const encodedUlnConfig = ethers.utils.defaultAbiCoder.encode([configTypeUlnStruct], [ulnConfig]); // Define the SetConfigParam struct const setConfigParam = { eid: remoteEid, configType: 2, // RECEIVE_CONFIG_TYPE config: encodedUlnConfig, }; // Send the transaction async function sendTransaction() { try { const tx = await endpointContract.setConfig( oappAddress, receiveLibAddress, [setConfigParam], // This should be an array of SetConfigParam structs ); console.log('Transaction sent:', tx.hash); const receipt = await tx.wait(); console.log('Transaction confirmed:', receipt.transactionHash); } catch (error) { console.error('Transaction failed:', error); } } sendTransaction(); ``` ## Debugging Configurations A **correct** OApp configuration example: | SendUlnConfig (A to B) | ReceiveUlnConfig (B to A) | | ------------------------------------------------------- | ------------------------------------------------------- | | confirmations: 15 | confirmations: 15 | | optionalDVNCount: 0 | optionalDVNCount: 0 | | optionalDVNThreshold: 0 | optionalDVNThreshold: 0 | | optionalDVNs: Array(0) | optionalDVNs: Array(0) | | requiredDVNCount: 2 | requiredDVNCount: 2 | | requiredDVNs: Array(DVN1\_Address\_A, DVN2\_Address\_A) | requiredDVNs: Array(DVN1\_Address\_B, DVN2\_Address\_B) | The sending OApp's **SendLibConfig** (OApp on Chain A) and the receiving OApp's **ReceiveLibConfig** (OApp on Chain B) match! ### Block Confirmation Mismatch An example of an **incorrect** OApp configuration: | SendUlnConfig (A to B) | ReceiveUlnConfig (B to A) | | ------------------------------- | ------------------------------- | | **confirmations: 5** | **confirmations: 15** | | optionalDVNCount: 0 | optionalDVNCount: 0 | | optionalDVNThreshold: 0 | optionalDVNThreshold: 0 | | optionalDVNs: Array(0) | optionalDVNs: Array(0) | | requiredDVNCount: 2 | requiredDVNCount: 2 | | requiredDVNs: Array(DVN1, DVN2) | requiredDVNs: Array(DVN1, DVN2) | The above configuration has a **block confirmation mismatch**. The sending OApp (Chain A) will only wait 5 block confirmations, but the receiving OApp (Chain B) will not accept any message with less than 15 block confirmations. Messages will be blocked until either the sending OApp has increased the outbound block confirmations, or the receiving OApp decreases the inbound block confirmation threshold. The reverse case — **send confirmations higher than receive** (`send-confirmations-higher`) — does **not** block delivery: messages still verify. The sender simply waits more confirmations than the receiver requires, adding latency with no security gain. Resolve it by lowering the send-side `confirmations` to the receiver's value (or raising the receiver's to match, if you want the extra finality) via `setConfig` on the ULN. #### DVN Mismatch
Another example of an incorrect OApp configuration: | SendUlnConfig (A to B) | ReceiveUlnConfig (B to A) | | ----------------------------- | ----------------------------------- | | confirmations: 15 | confirmations: 15 | | optionalDVNCount: 0 | optionalDVNCount: 0 | | optionalDVNThreshold: 0 | optionalDVNThreshold: 0 | | optionalDVNs: Array(0) | optionalDVNs: Array(0) | | **requiredDVNCount: 1** | **requiredDVNCount: 2** | | **requiredDVNs: Array(DVN1)** | **requiredDVNs: Array(DVN1, DVN2)** | The above configuration has a **DVN mismatch**. The sending OApp (Chain A) only pays DVN 1 to listen and verify the packet, but the receiving OApp (Chain B) requires both DVN 1 and DVN 2 to mark the packet as verified. Messages will be blocked until either the sending OApp has added DVN 2's address on Chain A to the SendUlnConfig, or the receiving OApp removes DVN 2's address on Chain B from the ReceiveUlnConfig. A DVN mismatch is **blocking** when the receiver's required DVNs are not a subset of the sender's effective DVN set, or when the worst-case overlap is smaller than the receiver's threshold — messages cannot accumulate the attestations they need and the channel halts at the next nonce. #### Non-Blocking DVN Mismatch Pin identical DVN sets on both sides of every pathway. Some asymmetries do **not** block delivery — they still let messages verify — but they leave the on-chain enforced posture stricter than the documented send posture, which auditors and on-call engineers reading the send config will get wrong. A non-blocking DVN mismatch occurs when the send and receive DVN sets are not identical, but in every adversarial pick of send's optional DVNs the receiver's required-subset and optional threshold are still satisfied. Messages flow. Observed posture differs from enforced posture. **Do:** * Compare the merged (`getUlnConfig`) configurations on both sides and align them intentionally. To bring the differing side into line, call `setConfig` using the same recipe as [Asymmetric DVN Configuration](#asymmetric-dvn-configuration). * If the sides differ on purpose (for example, the sender pays an additional optional DVN that the receiver does not require, in order to publish extra attestations downstream observers can read), document the rationale next to the deployment artifact. * Treat any silent drift between sides as a regression — promote a CI check that diffs `getUlnConfig(send)` against `getUlnConfig(receive)` for every pathway. **Don't:** * Read only the send config and infer the application's security posture from it — the receive side is the enforcement boundary. * Add a new optional DVN on one side without mirroring on the other. * Assume "messages are still delivering" means the configuration is correct; non-blocking mismatches deliver until the next default rotation changes the merged set. The effective security of a non-blocking mismatch is whichever side is **stricter**, not the union of the two configs. An auditor inspecting only the send config will overcount or undercount the DVNs your messages actually require, depending on which side has the wider set. ##### How to check ```bash wrap theme={null} # Merged config (defaults filled in) is what messages actually enforce. echo "Send-side (A→B) merged:" cast call "$SEND_LIB_A" \ "getUlnConfig(address,uint32)((uint64,uint8,uint8,uint8,address[],address[]))" \ "$OAPP_A" "$EID_B" --rpc-url "$RPC_A" echo "Receive-side (B from A) merged:" cast call "$RECV_LIB_B" \ "getUlnConfig(address,uint32)((uint64,uint8,uint8,uint8,address[],address[]))" \ "$OAPP_B" "$EID_A" --rpc-url "$RPC_B" # Compare requiredDVNs / optionalDVNs / optionalDVNThreshold by hand. # If sets differ but the sender still satisfies the receiver's required-subset # and threshold, this is non-blocking — messages flow, but observed posture # differs from enforced posture. Align both sides intentionally. ``` #### Asymmetric DVN Configuration Pin DVNs explicitly on **both** sides of every pathway. If one side calls `setConfig` with explicit `requiredDVNs` and the mirror leaves the value implicit (the OApp has never called `setConfig` for that field), the implicit side inherits the chain's default DVN set — and LayerZero Labs can change that default at any time without notice. A pathway that is symmetric today can become asymmetric overnight when the default rotates. The same drift applies to `optionalDVNs` and `optionalDVNThreshold`: an implicit threshold of `0` follows the default, not whatever the mirror has pinned. **Do:** * Call `EndpointV2.setConfig(oapp, sendLib, [...])` on the send side **and** `EndpointV2.setConfig(oapp, recvLib, [...])` on the receive side with matching `UlnConfig` values for every pathway. * Pin `requiredDVNs`, `optionalDVNs`, and `optionalDVNThreshold` on both sides — even if the explicit value happens to match today's default. * After every LayerZero default migration, re-run the validation snippet below for every pathway you operate; one side flipping from implicit to a new default is exactly the asymmetry this finding catches. **Don't:** * Rely on `getUlnConfig` (the **merged** view) for parity checks — it hides asymmetry by filling in defaults on both sides. Use `getAppUlnConfig` to see RAW values. * Treat an empty `requiredDVNs: []` and `requiredDVNCount: 0` as "no DVNs" — it means "use the default." Defaults are **mutable**. LayerZero Labs can change the default DVN set for any EID at any time. An OApp on the default for one direction and explicit for the other will silently shift to a new posture when the default updates; messages already in flight may stop verifying until the asymmetric side is brought into alignment. ##### How to check ```bash wrap theme={null} # `getAppUlnConfig` returns RAW values (zeros where the OApp hasn't set anything). # `getUlnConfig` returns the MERGED values (defaults filled in). # Asymmetry = one side has zeros, the other side has non-zeros, for the same field. # Send side (what chain A's OApp pays for) cast call "$SEND_LIB_A" \ "getAppUlnConfig(address,uint32)((uint64,uint8,uint8,uint8,address[],address[]))" \ "$OAPP_A" "$EID_B" --rpc-url "$RPC_A" # Receive side (what chain B's OApp requires) cast call "$RECV_LIB_B" \ "getAppUlnConfig(address,uint32)((uint64,uint8,uint8,uint8,address[],address[]))" \ "$OAPP_B" "$EID_A" --rpc-url "$RPC_B" # Interpret: if either side returns # `(0, 0, 0, 0, [], [])` while the mirror returns non-zero, the configuration is asymmetric. # Resolve by calling setConfig on the side with zeros to pin the same DVNs as the mirror. ``` #### [Dead DVN](../../../concepts/glossary#dead-dvn) This configuration includes a **Dead DVN**: | SendUlnConfig (A to B) | ReceiveUlnConfig (B to A) | | ----------------------------------- | ---------------------------------------- | | confirmations: 15 | confirmations: 15 | | optionalDVNCount: 0 | optionalDVNCount: 0 | | optionalDVNThreshold: 0 | optionalDVNThreshold: 0 | | optionalDVNs: Array(0) | optionalDVNs: Array(0) | | **requiredDVNCount: 2** | **requiredDVNCount: 2** | | **requiredDVNs: Array(DVN1, DVN2)** | **requiredDVNs: Array(DVN1, DVN\_DEAD)** | The above configuration has a **Dead DVN**. Similar to a DVN Mismatch, the sending OApp (Chain A) pays DVN 1 and DVN 2 to listen and verify the packet, but the receiving OApp (Chain B) has currently set DVN 1 and a Dead DVN to mark the packet as verified. Since a Dead DVN for all practical purposes should be considered a null address, no verification will ever match the dead address. Messages will be blocked until the receiving OApp removes or replaces the Dead DVN from the ReceiveUlnConfig. ## Summary * **Retrieve defaults:** Use `getConfig` if you need to review existing settings. * **Set Libraries:** Choose your Message Library version by calling `setSendLibrary` and `setReceiveLibrary`. * **Set Configurations:** Update your DVN (ULN) and Executor settings with `setConfig`. * **Ensure matching configurations:** The Send settings on one chain must match the Receive settings on the other chain. # Interactive Contract Playground Source: https://docs.layerzero.network/v2/developers/evm/contracts-playground Step-by-step guide to interactive contract playground using LayerZero V2. Build and deploy omnichain applications with crosschain messaging. Follow step-by-... Test LayerZero contracts directly from your browser. No coding required. Explore key application functions for message fee calculation, sending, receiving, configuration, and state management. This page focuses on methods relevant to building applications and does not include worker-related functions. ### Real Onchain Methods All functions shown in this playground are **real methods** available in the LayerZero contracts today: * **Endpoint Contract**: [Source Code](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/evm/protocol/contracts) * **OFT Contract**: [Source Code](https://github.com/LayerZero-Labs/devtools/tree/main/packages/oft-evm) We only document OApp-relevant instructions, excluding admin-only functions. State variables are clearly marked as direct account data reads, not instructions. ## LayerZero EndpointV2 The main entry point for all crosschain messaging operations. This contract handles message routing, fee calculation, and configuration management. ### Message Routing Core functions for sending and receiving messages between smart contracts. #### quote() - Get Fee Estimates #### send() - Send Messages #### lzReceive() - Receive Messages #### sendCompose() - Send Compose Messages #### lzCompose() - Execute Compose Messages ### Configuration Management Functions for setting custom verification, execution, and pathway management. #### eid() - Get Endpoint ID #### isRegisteredLibrary() - Check Library Registration #### receiveLibraryTimeout() - Get Library Timeout #### setDelegate() - Set Delegate Address #### setSendLibrary() - Configure Send Library #### setReceiveLibrary() - Configure Receive Library #### setConfig() - Set Configuration Parameters ### Message Recovery & Security Functions for handling message exceptions, security threats, and recovery scenarios. #### clear() - Clear Stored Message #### burn() - Permanently Block Message #### skip() - Skip Message Nonce #### nilify() - Mark Message as Nil ### Status Checks Functions for querying current configuration settings, library assignments, nonce tracking, and message states. #### getConfig() - Check Configuration #### delegates() - Check Delegate Address #### getSendLibrary() - Get Send Library #### getReceiveLibrary() - Get Receive Library #### inboundNonce() - Get Processed Nonce #### lazyInboundNonce() - Get Lazy Nonce #### initializable() - Check Message Initialization #### verifiable() - Check Message Verification #### inboundPayloadHash() - Get Message Payload Hash #### nextGuid() - Get Next Message GUID #### composeQueue() - Check Compose Message Queue #### isSendingMessage() - Check Send State #### getSendContext() - Get Current Send Context ### Events Key events emitted by the EndpointV2 contract. #### PacketSent - Message Sent Event #### PacketVerified - Message Verified Event #### PacketDelivered - Message Delivered Event #### ComposeSent - Compose Message Queued Event #### ComposeDelivered - Compose Message Delivered Event #### DelegateSet - Delegate Configuration Event #### SendLibrarySet - Send Library Configuration Event #### ReceiveLibrarySet - Receive Library Configuration Event #### ReceiveLibraryTimeoutSet - Library Timeout Configuration Event #### InboundNonceSkipped - Nonce Skip Event #### PacketNilified - Message Nilified Event #### PacketBurnt - Message Burnt Event ### Errors #### LZ\_InsufficientFee - Insufficient Fee #### LZ\_InvalidNonce - Invalid Nonce #### LZ\_Unauthorized - Unauthorized Access #### LZ\_SendReentrancy - Send Reentrancy Detected #### Key Functions to Try: * **Messaging Operations:** * `quote()` - Get fee estimates for crosschain messages * `send()` - Send messages to other chains * `lzReceive()` - Receive messages from other chains * `sendCompose()` - Queue compose messages * `lzCompose()` - Execute compose messages * **Configuration Management:** * `setDelegate()` - Assign configuration permissions * `setSendLibrary()` - Choose send message library * `setReceiveLibrary()` - Choose receive message library * `setConfig()` - Set library-specific parameters * **Message Recovery & Security:** * `burn()` - Permanently block malicious messages * `skip()` - Skip flagged message nonces * `nilify()` - Mark messages for re-verification * `clear()` - Clear verified but unexecuted messages * **Status Checks:** * `getConfig()` - Check current configurations * `delegates()` - View current delegate address * `getSendLibrary()` - Check send library for endpoint * `getReceiveLibrary()` - Check receive library for endpoint * `inboundNonce()` - Get highest processed message nonce * `lazyInboundNonce()` - Get highest verified/skipped nonce * `nextGuid()` - Get next message GUID * `composeQueue()` - Check compose queue * `isSendingMessage()` - Check send state * `getSendContext()` - Get send context ## LayerZero Message Libraries Message Libraries handle the core verification and execution logic for LayerZero messages. While developers don't interact with these contracts directly, understanding their error codes and events is crucial for debugging failed transactions. ### Current Message Libraries * **SendUln302**: Handles outbound message verification setup * **ReceiveUln302**: Processes inbound message verification and execution * **ReadLib1002**: Manages crosschain read operations > **When You'll See These**: These errors appear when calling Endpoint methods like `send()` or `lzReceive()`. The Endpoint delegates to these libraries internally, so their errors bubble up through your Endpoint transactions. ### SendUln302 - Outbound Message Library The SendUln302 library manages the configuration and verification setup for outbound messages. It coordinates with DVNs (Decentralized Verifier Networks) and handles fee calculations for message transmission. ### Errors Configuration & Setup Errors #### LZ\_ULN\_InvalidConfigType - Invalid Configuration Type #### LZ\_ULN\_InvalidRequiredDVNCount - Invalid DVN Count #### LZ\_ULN\_InvalidOptionalDVNCount - Invalid Optional DVN Count #### LZ\_ULN\_InvalidOptionalDVNThreshold - Invalid DVN Threshold #### LZ\_ULN\_AtLeastOneDVN - At Least One DVN Required #### LZ\_ULN\_InvalidConfirmations - Invalid Confirmations #### LZ\_ULN\_Unsorted - DVNs Not Sorted Worker & Option Errors #### LZ\_ULN\_InvalidWorkerId - Invalid Worker ID #### LZ\_ULN\_InvalidWorkerOptions - Invalid Worker Options #### LZ\_ULN\_UnsupportedOptionType - Unsupported Option Type #### LZ\_ULN\_InvalidLegacyType1Option - Invalid Legacy Type 1 Option #### LZ\_ULN\_InvalidLegacyType2Option - Invalid Legacy Type 2 Option #### LZ\_ULN\_UnsupportedEid - Unsupported Endpoint ID Fee & Payment Errors #### LZ\_MessageLib\_InvalidAmount - Invalid Fee Amount #### LZ\_MessageLib\_TransferFailed - Fee Transfer Failed Library Access Errors #### LZ\_MessageLib\_OnlyEndpoint - Only Endpoint Allowed #### LZ\_MessageLib\_InvalidExecutor - Invalid Executor #### LZ\_MessageLib\_NotTreasury - Not Treasury #### LZ\_MessageLib\_CannotWithdrawAltToken - Cannot Withdraw Alt Token Message Validation Errors #### LZ\_MessageLib\_InvalidMessageSize - Message Too Large #### LZ\_MessageLib\_ZeroMessageSize - Zero Message Size #### DVN\_InvalidDVNOptions - Invalid DVN Options #### DVN\_InvalidDVNIdx - Invalid DVN Index Transfer Errors #### Transfer\_NativeFailed - Native Transfer Failed #### Transfer\_ToAddressIsZero - Transfer to Zero Address ### Events #### DVNFeePaid - DVN Fee Payment ### ReceiveUln302 - Inbound Message Library The ReceiveUln302 library handles the verification and execution of inbound messages. It validates DVN signatures, manages message ordering, and ensures secure message delivery. ### Errors Verification Errors #### LZ\_ULN\_InvalidPacketHeader - Invalid Packet Header #### LZ\_ULN\_InvalidPacketVersion - Wrong Packet Version #### LZ\_ULN\_InvalidEid - Invalid Endpoint ID Configuration Errors #### LZ\_ULN\_InvalidConfigType - Invalid Configuration Type #### LZ\_ULN\_InvalidRequiredDVNCount - Invalid DVN Count #### LZ\_ULN\_InvalidOptionalDVNCount - Invalid Optional DVN Count #### LZ\_ULN\_InvalidOptionalDVNThreshold - Invalid DVN Threshold #### LZ\_ULN\_AtLeastOneDVN - At Least One DVN Required #### LZ\_ULN\_InvalidConfirmations - Invalid Confirmations #### LZ\_ULN\_Unsorted - DVNs Not Sorted #### LZ\_ULN\_UnsupportedEid - Unsupported Endpoint ID #### LZ\_ULN\_Verifying - Already Verifying #### LZ\_MessageLib\_OnlyEndpoint - Only Endpoint Allowed ### ReadLib1002 - Crosschain Read Library The ReadLib1002 library enables crosschain data reading without state changes. It manages read channels, handles request/response cycles, and ensures data integrity. ### Errors Configuration Errors #### LZ\_RL\_InvalidConfigType - Invalid Configuration Type #### LZ\_RL\_InvalidRequiredDVNCount - Invalid DVN Count #### LZ\_RL\_InvalidOptionalDVNCount - Invalid Optional DVN Count #### LZ\_RL\_InvalidOptionalDVNThreshold - Invalid DVN Threshold Validation Errors #### LZ\_RL\_InvalidPacketHeader - Invalid Packet Header #### LZ\_RL\_InvalidPacketVersion - Wrong Packet Version #### LZ\_RL\_InvalidCmdHash - Invalid Command Hash #### LZ\_RL\_InvalidReceiver - Invalid Receiver #### LZ\_RL\_InvalidAmount - Invalid Fee Amount #### LZ\_RL\_Verifying - Already Verifying #### LZ\_RL\_InvalidEid - Invalid Endpoint ID #### LZ\_RL\_AtLeastOneDVN - At Least One DVN Required #### LZ\_RL\_Unsorted - DVNs Not Sorted #### LZ\_RL\_UnsupportedEid - Unsupported Endpoint ID Library Access Errors #### LZ\_MessageLib\_OnlyEndpoint - Only Endpoint Allowed #### LZ\_RL\_InvalidExecutor - Invalid Executor #### LZ\_RL\_NotTreasury - Not Treasury #### LZ\_RL\_CannotWithdrawAltToken - Cannot Withdraw Alt Token Worker & Option Errors #### LZ\_ULN\_InvalidWorkerId - Invalid Worker ID #### LZ\_ULN\_InvalidWorkerOptions - Invalid Worker Options #### LZ\_ULN\_UnsupportedOptionType - Unsupported Option Type #### LZ\_ULN\_InvalidLegacyType1Option - Invalid Legacy Type 1 Option #### LZ\_ULN\_InvalidLegacyType2Option - Invalid Legacy Type 2 Option DVN Errors #### DVN\_InvalidDVNOptions - Invalid DVN Options #### DVN\_InvalidDVNIdx - Invalid DVN Index Transfer Errors #### Transfer\_NativeFailed - Native Transfer Failed #### Transfer\_ToAddressIsZero - Transfer to Zero Address ### Troubleshooting Common Library Errors #### Fee-Related Issues * **LZ\_MessageLib\_InvalidAmount**: Always use `quote()` before `send()` to get exact fees * **LZ\_MessageLib\_TransferFailed**: Ensure contract has sufficient ETH balance * **LZ\_RL\_InvalidAmount**: Ensure correct fee for read operations * **DVN\_InvalidDVNOptions**: Check DVN option encoding and parameters #### Configuration Issues * **LZ\_ULN\_InvalidRequiredDVNCount**: Must have at least 1 required DVN * **LZ\_ULN\_InvalidOptionalDVNThreshold**: Threshold must be ≤ optional DVN count * **LZ\_ULN\_InvalidConfigType**: Use correct config type (ULN=2, Executor=1) * **LZ\_RL\_InvalidConfigType**: Use correct config type for read library #### Verification Issues * **LZ\_ULN\_Verifying**: Message already being verified, wait for completion * **LZ\_RL\_Verifying**: Read response already being verified * **LZ\_ULN\_InvalidPacketHeader**: Possible message corruption or tampering * **LZ\_RL\_InvalidPacketHeader**: Read packet header malformed * **LZ\_ULN\_InvalidPacketVersion**: Wrong packet version for message library * **LZ\_RL\_InvalidPacketVersion**: Wrong packet version for read library For detailed troubleshooting guides, see [LayerZero Troubleshooting](/v2/developers/evm/troubleshooting/debugging-messages). ## Omnichain Application (OApp) The foundation for building any crosschain application. **OApp** provides the core messaging infrastructure for a smart contract interacting with the **EndpointV2**. ### Core Information #### oAppVersion() - Get OApp Version #### endpoint() - Get Endpoint Address ### Peer Configuration #### peers() - Get Remote Peer Address #### setPeer() - Connect Remote Chains #### setDelegate() - Set Configuration Delegate ### Message Reception #### allowInitializePath() - Check Path Initialization #### nextNonce() - Get Next Message Nonce #### lzReceive() - Receive Crosschain Messages ### Composability #### isComposeMsgSender() - Verify Compose Sender ### Events and Errors Key events and errors emitted by the OApp contract. #### Events #### PeerSet - Peer Configuration Updated #### Errors #### OnlyPeer - Unauthorized Peer Message #### NoPeer - Missing Peer Configuration #### InvalidEndpointCall - Invalid Endpoint Call #### InvalidDelegate - Invalid Delegate Configuration #### NotEnoughNative - Insufficient Native Fee #### OnlyEndpoint - Unauthorized Endpoint Call #### LzTokenUnavailable - LayerZero Token Not Available #### Key Functions to Try: * **Core Information:** * `oAppVersion()` - Get OApp version information * `endpoint()` - Get connected endpoint address * **Peer Configuration:** * `setPeer()` - Connect to remote chains * `peers()` - Check connected chains * `setDelegate()` - Set configuration delegate * **Message Reception:** * `lzReceive()` - Receive crosschain messages * `allowInitializePath()` - Check path initialization * `nextNonce()` - Get message ordering info * **Composability:** * `isComposeMsgSender()` - Verify compose sender #### Tips: * Peers must be set before messaging * Nonces ensure ordered delivery * Options control execution parameters ## Omnichain Application Read (OAppRead) **OAppRead** extends the standard **OApp** with LayerZero Read functionality, enabling crosschain data reading capabilities. It includes all standard methods plus the read channel configuration. #### setReadChannel() - Configure Read Channel #### Key Functions to Try: * `setReadChannel()` - Configure read channel for crosschain data reading * Plus all standard OApp functions listed above ## Omnichain Fungible Token (OFT) **OFT** inherits from **OApp**, providing all crosschain messaging capabilities plus token-specific functionality. Create tokens that work seamlessly across multiple blockchains while maintaining a unified supply. ### Send Tokens #### quoteSend() - Get Transfer Fees #### quoteOFT() - Get Detailed Transfer Quote #### send() - Transfer Tokens ### Token Details #### sharedDecimals() - Get Shared Decimals #### approvalRequired() - Check Approval Requirement #### oftVersion() - Get OFT Version #### token() - Get Underlying Token Address #### decimalConversionRate() - Get Decimal Conversion Factor ### Management Functions #### owner() - Get Current Owner #### transferOwnership() - Transfer Contract Ownership #### renounceOwnership() - Renounce Ownership #### setPeer() - Connect to Remote OFTs #### setEnforcedOptions() - Configure Message Options #### setMsgInspector() - Set Message Inspector ### Events Key events emitted by the OFT contract. #### OFTSent - Token Transfer Sent #### OFTReceived - Token Transfer Received #### Transfer - Standard ERC20 Transfer ### Errors #### InvalidLocalDecimals - Invalid Decimal Configuration #### SlippageExceeded - Transfer Slippage Too High #### AmountSDOverflowed - Shared Decimal Overflow #### Key Functions to Try: * **Transfer Operations:** * `quoteSend()` - Get transfer fee estimates * `send()` - Transfer tokens crosschain * `quoteOFT()` - Get comprehensive transfer quotes * **Token Information:** * `sharedDecimals()` - Check decimal configuration * `approvalRequired()` - Check if approval is needed * `oftVersion()` - Get OFT implementation version * `token()` - Get underlying token address * `decimalConversionRate()` - Get decimal conversion factor * **Management Functions:** * `owner()` - Check current contract owner * `setPeer()` - Connect to OFTs on other chains * `setEnforcedOptions()` - Configure security parameters * `setMsgInspector()` - Set message inspector * `transferOwnership()` - Transfer contract ownership * `renounceOwnership()` - Permanently remove ownership #### Tips: * Always call `quoteSend()` before `send()` to get accurate fees * The `minAmountLD` parameter provides slippage protection * Shared decimals (typically 6-8) may differ from local decimals (e.g., 18 for most ERC20s) * `approvalRequired()` returns false for OFT and true for OFTAdapter * Use `quoteOFT()` for detailed information including transfer limits and fee breakdowns * OFTAdapter requires approval on the underlying token before sending * Must call `setPeer()` to connect OFTs on different chains before transfers * Only the contract owner can call management functions * Use `setEnforcedOptions()` to enforce minimum gas limits for security * `renounceOwnership()` is irreversible - use with extreme caution For complete contract documentation including all functions, events, and technical details: * [Contract Standards Overview](/v2/developers/evm/overview) * [OApp Technical Reference](/v2/concepts/technical-reference/oapp-reference) * [OFT Technical Reference](/v2/concepts/technical-reference/oft-reference) * [Protocol Contracts](/v2/developers/evm/protocol-contracts-overview) ## Decentralized Verifier Network (DVN) DVNs provide independent verification of crosschain messages. Each DVN can set custom pricing and confirmation requirements for different destination chains. ### getFee() - Get DVN Verification Fee InteractiveDVN temporarily disabled. ### dstConfig() - Get DVN Configuration InteractiveDVN temporarily disabled. ## Executor Executors handle the final delivery of verified messages on destination chains. They manage gas pricing, execution parameters, and delivery guarantees. ### getFee() - Get Execution Fee ### dstConfig() - Get Destination Configuration Contract ABIs shown here are from the latest deployment. Always verify addresses and ABIs for your specific use case. # LayerZero EVM Chain Compatibility Source: https://docs.layerzero.network/v2/developers/evm/evm-variants/evm-compatible-variants LayerZero V2 connects a diverse ecosystem of blockchain networks that support Ethereum's Virtual Machine (EVM). Because different chains implement the EVM... LayerZero V2 connects a diverse ecosystem of blockchain networks that support Ethereum's Virtual Machine (EVM). Because different chains implement the EVM in various ways, it's important for developers—especially those building omnichain applications (OApp), OFT, and ONFT—to understand whether a network is **EVM Compatible** or **EVM Equivalent**. This documentation focuses on the practical impacts when integrating LayerZero: * **Fee delivery:** LayerZero endpoints expect worker fees to be delivered via `payable` (`msg.value`) using the chain's native token. Some chains (e.g. SKALE) use an alternative `ERC20` fee token, which requires alternative LayerZero contracts (e.g., [`EndpointV2Alt`](../../../concepts/protocol/layerzero-endpoint-alt), `OAppAlt`, `OFTAlt`). * **Token standards:** LayerZero EVM token standards rely on the normal `ERC20`/`ERC721` conventions. * **Data queries (lzRead):** LayerZero Read functions may use `block.number` and `block.timestamp` to reference "latest" state. However, on some chains these values may drift or be unreliable (for example, Arbitrum's `block.number` may return the L1 block number), potentially causing mismatches in state queries. * **Gas estimation:** Accurate gas limits are critical to ensure successful crosschain message delivery. Each chain may have a unique fee model, which impacts how gas estimates should be calculated for [`lzReceive`](../../../concepts/glossary#lzreceive) and [`lzCompose`](../../../concepts/glossary#lzcompose). ## Concept: Compatibility vs. Equivalence > **EVM compatibility:**\ > While these chains run Ethereum smart contracts, they may require adjustments in deployment scripts, fee handling, gas estimation, and verification. For example, zkSync requires its own compiler (`zkSolc`), and SKALE's "free gas" model requires an alternative fee token for crosschain fees. These differences can affect how LayerZero contracts pay/receive fees and how developers should interact with the chain. > >
> >
> > **EVM equivalence:**\ > These chains replicate Ethereum's execution environment so closely that standard clients, deployment scripts, and tooling work without modification. Most LayerZero integrations (like OApp/OFT/ONFT and lzRead queries) work as on Ethereum—with only subtle differences. These differences directly impact: * **Fee payment:** Standard `msg.value` fee delivery is expected by LayerZero endpoints. Some chains, however, require alternative tokens or extra configuration. * **Onchain data:** lzRead can depend on `block.number` and `block.timestamp`. Variability or drift in these values (for instance, Arbitrum may return L1 `block.number`) may result in inaccurate or outdated state queries. * **Gas management:** Accurate gas limits must be set to ensure `lzReceive` and `lzCompose` execution succeeds across chains. ## Detailed Chain-Specific Overviews Below is a summary for each chain type with key impacts for LayerZero integrations and links to more documentation. ### EVM Diff Checker For a quick way to identify opcode differences between networks, check out the [**EVM Diff Checker**](https://www.evmdiff.com/). This tool is particularly useful if you're troubleshooting or optimizing across various EVM implementations. ### **Optimism (OP) Stack: EVM Equivalence** OP Stack chains aim for out-of-the-box Ethereum compatibility. You can use standard Ethereum tools and wallets without modification​. **Examples:** * Optimism, Base **Key details for LayerZero:** * **Toolchain & compilers:**\ Use standard Ethereum tools and the regular Solidity compiler.\ [Optimism Docs – Differences](https://docs.optimism.io/stack/differences) * **Fee payment:**\ Fees are paid in ETH via `msg.value` with no alternative fee token needed. * **Onchain reads:**\ `Block.number` and `block.timestamp` behave similarly to Ethereum, with a fixed \~2-second block time. * **Further documentation:**\ [Optimism Documentation](https://docs.optimism.io/) ### **Arbitrum Orbit: EVM Equivalence** Arbitrum uses normal EVM bytecode (Arbitrum Nitro incorporates the Ethereum Yellow Paper spec), meaning you can compile with the same solc version you'd use on Ethereum mainnet. **Examples:** * Arbitrum One, Arbitrum Nova, ApeChain (Orbit) **Key details for LayerZero:** * **Toolchain & compilers:**\ Standard Ethereum tools work; no special compiler is needed.\ [Arbitrum Developer Portal](https://developer.arbitrum.io/) * **Fee payment:**\ On Arbitrum One/Nova, fees are paid in ETH (or bridged ArbETH). However, some Orbit chains may use a custom `ERC20` (e.g. APE on ApeChain). * **Onchain reads:**\ Arbitrum's `block.number` may reflect L1's block number, and its flexible sequencer-controlled `block.timestamp` can potentially drift by up to 24 hours in the past or 1 hour in the future. In the worst case scenario, this variability may cause lzRead to return historical or mismatched state. [Arbitrum Docs – Arbitrum vs Ethereum](https://docs.arbitrum.io/build-decentralized-apps/arbitrum-vs-ethereum/block-numbers-and-time) * **Further documentation:**\ [Arbitrum Developer Documentation](https://developer.arbitrum.io/) ### **Avalanche Subnet: EVM Equivalent** Avalanche subnets that run the EVM (Subnet-EVM) allow you to use the same Ethereum development tools as expected. By default, Avalanche's Subnet-EVM does not remove or alter EVM opcodes – it's EVM-equivalent. Avalanche subnets can have custom fee tokens and models. By default, when you create a subnet EVM, you specify the native token (it could be an existing ERC20 or a new token created as the native asset). **Examples:** * Avalanche, Dexalot, DeFi Kingdom **Key details for LayerZero:** * **Toolchain & compilers:**\ Use standard Ethereum development tools with the subnet's RPC and chain ID. * **Fee payment:**\ Fees are paid in the subnet's native token (AVAX or a custom token). Make sure your `msg.value` fee delivery aligns with the chain's requirements. You may need [OFTAlt](../oft/oft-patterns-extensions#oft-alt) if the Subnet requires a custom ERC20 token for fees. * **Onchain reads:**\ `block.number` and `block.timestamp` update more frequently (typically 1–2 seconds per block) compared to Ethereum. This faster cadence can affect lzRead if your contracts assume Ethereum-like intervals. * **Further documentation:**\ [Avalanche Subnets Docs](https://docs.avax.network/subnets) ### **zkSync Elastic Chains: EVM Compatible** zkSync Era is a ZK-rollup that supports Solidity, but you should use zkSync's provided tooling for the smoothest experience. Incorporate Matter Labs' toolchain additions: use `zksolc` compiler, and the specialized Hardhat or Foundry integration​ for a frictionless dev experience. **Examples:** * zkSync Era, Abstract **Key details for LayerZero:** * **Toolchain & compilers:**\ Use [zkSync's Hardhat](https://docs.zksync.io/zksync-era/tooling/hardhat) or [Foundry](https://docs.zksync.io/zksync-era/tooling/foundry/overview) plugin with the `zksolc` compiler. * **Fee payment:**\ Fees are paid in `msg.value`, with no alternative fee token needed. * **Onchain reads:**\ Due to rollup batching, `block.number` and `block.timestamp` may jump in batches rather than update continuously. This requires careful handling in lzRead to ensure you query the intended state. * **Further documentation:**\ [zkSync Era Documentation](https://docs.zksync.io/) ### **SKALE: EVM Compatible** SKALE is a multi-chain network where each chain is an EVM-compatible blockchain (often called an "Elastic Sidechain"). For deploying and interacting with contracts on a SKALE chain, you mostly use standard Ethereum tools – with a couple of caveats due to network specifics. **Examples:** * SKALE **Key details for LayerZero:** * **Toolchain & compilers:**\ Standard Ethereum tools work, with configuration changes for SKALE's RPC and chain ID.\ [SKALE Network Differences](https://docs.skale.network/technology/differences) * **Fee payment:**\ SKALE uses a "free gas" model with a dummy token (sFUEL). However, LayerZero workers require an alternative ERC20 token to handle destination gas payments. The LayerZero Endpoint will expect fee delivery in this token rather than `msg.value`. For more information see [LayerZero Endpoint Alt](../../../concepts/protocol/layerzero-endpoint-alt) and [OFT Alt](../oft/oft-patterns-extensions#oft-alt). * **Onchain reads:**\ `Block.number` and `block.timestamp` are generally reliable, but note that gas fees aren't paid in ETH. * **Further documentation:**\ [SKALE Documentation](https://docs.skale.network/) ### **BTC L2 Chains: EVM Compatible** Most BTC L2s are EVM‑compatible. You can generally use standard Ethereum development tools and can compile with standard solc. **Examples:** * GOAT, Rootstock, Bitlayer, Bouncebit, and Citrea **Key details for LayerZero:** * **Toolchain & compilers:**\ Standard Ethereum tools work, but may vary from L2 to L2. * **Fee payment:**\ Fees are paid in RBTC on RSK or zBTC on GOAT. LayerZero endpoints must receive fees in the proper native token. * **Onchain reads:**\ `block.timestamp` and `block.number` may differ substantially from Ethereum (e.g., RSK's \~30-second blocks). In GOAT, state finality depends on Bitcoin settlement; this could impact lzRead if using local chain data. * **Further documentation:**\ [Rootstock Documentation](https://dev.rootstock.io/) | [GOAT Network Documentation](https://docs.goat.network/) | [Bitlayer Documentation](https://docs.bitlayer.org/docs/Learn/Introduction/) ### **HyperEVM: EVM Equivalence** **Key details for LayerZero:** * **Toolchain & compilers:**\ Use standard Ethereum tools (Hardhat, ethers.js) with HyperEVM's RPC and chain ID.\ [HyperLiquid Docs](https://hyperliquid.gitbook.io/hyperliquid-docs/) * **Fee payment:**\ Fees are paid in HYPE (HyperLiquid's native token). Your LayerZero endpoints will expect msg.value in HYPE. Note the dual-block architecture may require higher gas limits for heavy transactions. * **Contract Standards:** While the normal OApp/OFT/ONFT can be used out-of-the-box on the HyperEVM, you will want to deploy a custom HyperOFT to have automatic delivery to the Hyperliquid Spot. See the HyperOFT documentation for more information. * **Onchain reads:**\ While HyperEVM's `block.timestamp` and `block.number` behave similarly to Ethereum's, the dual-block design (small vs. big blocks) may introduce discrepancies—especially if a heavy transaction is scheduled in a "big" block. * **Further documentation:**\ [HyperLiquid HyperEVM Documentation](https://hyperliquid.gitbook.io/hyperliquid-docs/) Below are separate sections for Hedera and Tron, with additional detail on Hedera's unique requirements. In Hedera's case, many DeFi protocols use the Hedera Token Service (HTS) rather than a standard ERC20, which can necessitate custom contract changes when integrating with LayerZero. ### **Hedera: EVM Compatible** Hedera is a public distributed ledger built on Hashgraph consensus that supports high-speed, fair, and secure transactions while also offering an EVM-compatible environment via the Hedera EVM. **Key details for LayerZero:** * **Toolchain & compilers:**\ Hedera supports EVM-compatible smart contracts through the Hedera EVM. However, developers may need to use the Hedera Web3 SDK and adjust configurations to work with Hedera's network.\ [Hedera EVM Docs](https://hedera.com/technology/hedera-evm) * **Fee payment:**\ Fees are paid in HBAR, Hedera's native token. Additionally, many DeFi applications on Hedera rely on the Hedera Token Service (HTS) for token issuance and transfers instead of standard ERC20 tokens. This means that a standard ERC20 OFT may not work as expected on Hedera. * **Onchain reads:**\ Not available. * **Further documentation:**\ [Hedera EVM Documentation](https://hedera.com/technology/hedera-evm) | [Hedera Token Service Overview](https://hedera.com/technology/token-service) ### **Tron: EVM Compatible** Tron is a blockchain platform focused on decentralizing the internet and digital entertainment, utilizing its native TRX token and offering an EVM-compatible environment through its Tron Virtual Machine (TVM). **Key details for LayerZero:** * **Toolchain & compilers:**\ Tron supports an EVM-like environment (via Tron Virtual Machine, TVM), but many projects rely on Tron-specific libraries such as TronWeb. While you can deploy standard Solidity contracts, some adaptations may be needed to interface with Tron's unique APIs.\ [Tron Developer Hub](https://developers.tron.network/) * **Fee payment:**\ Fees are paid in TRX, Tron's native token. The Tron ecosystem uses standards such as TRC20 (similar to ERC20) for token contracts, so LayerZero integrations that rely on ERC20 conventions generally translate well. * **Onchain reads:**\ Not available. * **Further documentation:**\ [Tron Developer Documentation](https://developers.tron.network/) ## Conclusion **EVM Equivalent chains** generally allow you to deploy and operate with minimal changes. However, be sure to account for subtle differences that may impact your contract's logic (e.g., different behaviour in `block.number` or `block.timestamp`). **EVM Compatible chains** may require adjustments in deployment, fee handling, and gas estimation. **Developer checklist:** * **Network configuration:** Update your deployment scripts with the correct RPC endpoints, chain IDs, and native token details. * **Fee handling:** Verify that your payable functions deliver fees in the correct native token as required by the chain. * **Gas estimation:** Test gas limits on your target chain to ensure that calls execute successfully. * **Onchain data:** Validate that your logic correctly executes as expected, accounting for any drift or inconsistencies. * **Toolchain adjustments:** Use chain-specific SDKs or compilers as needed (e.g., zkSync's Hardhat plugin) to guarantee compatibility. By understanding these nuances and consulting the chain-specific documentation linked above, you can adapt your LayerZero crosschain messaging and token integrations to work reliably across all supported networks. # Integrating lzAsset Transfers Source: https://docs.layerzero.network/v2/developers/evm/lzasset/overview Overview of Integrating lzAsset Transfers on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure... lzAsset contracts implement the standard **IOFT interface** for [Omnichain Fungible Tokens (OFTs)](../oft/quickstart). However, transferring between the **Native Mesh** (issuer-controlled) and the **lzAsset Mesh** (LayerZero-managed) requires routing through a **Hub Chain** using the `MultiHopComposer`. For architecture and concepts, see [lzAsset Managed Service](/v2/concepts/applications/lzasset). ## The IOFT Interface All lzAsset token contracts (on both Native and lzAsset chains) implement the standard [`IOFT` interface](https://github.com/LayerZero-Labs/devtools/blob/main/packages/oft-evm/contracts/interfaces/IOFT.sol). You interact with them using the standard `send()` and `quoteSend()` methods, identical to any other OFT deployment. ```solidity wrap theme={null} // lzAsset tokens use the standard IOFT interface import { IOFT, SendParam, MessagingFee } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; // Quote and send work exactly like any OFT MessagingFee memory fee = IOFT(lzAssetToken).quoteSend(sendParam, false); IOFT(lzAssetToken).send{value: fee.nativeFee}(sendParam, fee, refundAddress); ``` ## Transfer Types There are two types of transfers in the lzAsset ecosystem: 1. **Direct Transfers** (Within the same mesh) * **Within the lzAsset mesh**: Standard OFT transfer. * **Native to Native**: Standard OFT transfer (if multiple native chains exist). 2. **Cross-Mesh Transfers** (Between meshes) * **Native → lzAsset**: Must route through the Hub Chain. * **lzAsset → Native**: Must route through the Hub Chain. ### Direct Transfers (Standard) For transfers within the same mesh (e.g., lzAsset chain A to lzAsset chain B), simply construct a standard `SendParam` with the destination endpoint ID and recipient address. No special composition is required. ```solidity wrap theme={null} // Standard OFT Transfer (Direct) SendParam memory sendParam = SendParam({ dstEid: dstEid, // Destination lzAsset chain to: bytes32(uint256(uint160(recipient))), amountLD: amount, minAmountLD: amount, extraOptions: options, // Standard gas options composeMsg: "", oftCmd: "" }); ``` ## Cross-Mesh Transfers (Multi-Hop) To move assets between the Native Mesh and the lzAsset Mesh, the transfer must be routed through the **MultiHopComposer** on the Hub Chain. This is a **two-hop** process: 1. **Hop 1**: Source Chain → Hub Chain (Composer) 2. **Hop 2**: Hub Chain (Composer) → Destination Chain You initiate this entire flow from the source chain by encoding the instructions for the second hop into the `composeMsg` of the first hop. ### Constructing the Send You must construct two `SendParam` structs: one for the final destination (Hop 2) and one for the Hub (Hop 1). #### 1. Prepare the Second Hop (Hub → Destination) First, define where the tokens should go *after* they reach the Hub. ```solidity wrap theme={null} // The parameters for the transfer from Hub to Final Destination SendParam memory nextHopParam = SendParam({ dstEid: finalDstEid, // The final destination chain ID to: bytes32(uint256(uint160(finalRecipient))), amountLD: amount, // The amount to forward minAmountLD: 0, // Set to 0 (slippage handled by Composer) extraOptions: nextHopOptions, // Options for delivery to final recipient composeMsg: "", // Usually empty unless chaining further oftCmd: "" // Standard transfer }); // Encode this struct to be passed as the compose message bytes memory composeMsg = abi.encode(nextHopParam); ``` #### 2. Prepare the First Hop (Source → Hub) Next, wrap the second hop inside the parameters for the first hop. The destination is the **Hub Chain**, and the recipient is the **MultiHopComposer** contract. **Critical Requirements**: * `dstEid`: Must be the Hub Chain ID. * `to`: Must be the address of the `MultiHopComposer` on the Hub. * `composeMsg`: Must be the encoded `nextHopParam` from Step 1. * **Gas & Value**: You must pay for the second hop's gas and fee *on the source chain*. ```solidity wrap theme={null} // Calculate the fee required for the second hop (native fee on Hub) // This implies quoting the OFT lockbox on the Hub chain off-chain (see Fee Estimation below) // You must call IOFT.quoteSend() on the Hub chain and use the returned MessagingFee.nativeFee uint128 gasForLzReceive = 100_000; // Gas for Hub OFT's _lzReceive (credits tokens + calls sendCompose) uint128 gasForCompose = 500_000; // Gas to execute lzCompose on Hub (MultiHopComposer logic) uint128 msgValueForNextHop = quoteFromHub.nativeFee; // Add execution options for the Hub - BOTH lzReceive AND lzCompose are required bytes memory firstHopOptions = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(gasForLzReceive, 0) .addExecutorLzComposeOption(0, gasForCompose, msgValueForNextHop); // The parameters for the transfer from Source to Hub SendParam memory sendParam = SendParam({ dstEid: hubEid, // The Hub Chain ID to: bytes32(uint256(uint160(composerAddress))), // The MultiHopComposer address amountLD: amount, minAmountLD: amount, extraOptions: firstHopOptions, composeMsg: composeMsg, // The encoded nextHopParam oftCmd: "" // Must use Taxi mode for lzCompose }); ``` ### Full Implementation Example Here is a complete Solidity example of how to format and send a cross-mesh lzAsset transfer. ```solidity wrap theme={null} import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; import { IOFT, SendParam, MessagingFee } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; contract LzAssetSender { using OptionsBuilder for bytes; // The lzAsset token contract on this chain IOFT public lzAssetToken; // The address of the MultiHopComposer on the Hub Chain address public composerOnHub; // The Endpoint ID of the Hub Chain uint32 public hubEid; constructor(address _token, address _composer, uint32 _hubEid) { lzAssetToken = IOFT(_token); composerOnHub = _composer; hubEid = _hubEid; } function sendCrossMesh( uint32 _finalDstEid, address _finalRecipient, uint256 _amount, uint128 _nextHopNativeFee ) external payable { // 1. PREPARE SECOND HOP (Hub -> Destination) // Options for the final delivery (Executor gas on destination) bytes memory nextHopOptions = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(200000, 0); SendParam memory nextHopParam = SendParam({ dstEid: _finalDstEid, to: bytes32(uint256(uint160(_finalRecipient))), amountLD: _amount, minAmountLD: 0, // Composer handles amount adjustment extraOptions: nextHopOptions, composeMsg: "", oftCmd: "" }); // 2. PREPARE FIRST HOP (Source -> Hub) // Encode the second hop instructions as the composeMsg // The OFT will wrap this with (nonce, srcEid, amountLD, composeFrom) before delivery // The MultiHopComposer extracts it via OFTComposeMsgCodec.composeMsg() bytes memory composeMsg = abi.encode(nextHopParam); // Options for the Hub: BOTH lzReceive AND lzCompose gas are required // - lzReceive: Hub OFT credits tokens and calls endpoint.sendCompose() // - lzCompose: MultiHopComposer executes and calls OFT.send() for second hop bytes memory firstHopOptions = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(65_000, 0) .addExecutorLzComposeOption(0, 500_000, _nextHopNativeFee); SendParam memory sendParam = SendParam({ dstEid: hubEid, to: bytes32(uint256(uint160(composerOnHub))), amountLD: _amount, minAmountLD: _amount, // Normal slippage for first hop extraOptions: firstHopOptions, composeMsg: composeMsg, oftCmd: "" }); // 3. QUOTE AND SEND MessagingFee memory fee = lzAssetToken.quoteSend(sendParam, false); // Ensure enough value was sent require(msg.value >= fee.nativeFee, "Insufficient fee"); // Send lzAssetToken.send{value: fee.nativeFee}( sendParam, fee, msg.sender // Refund address ); } } ``` ### composeMsg Encoding When you call `send()` with a `composeMsg`, the OFT automatically wraps your data with additional context before delivering it to the composer: * `nonce` - Transaction tracking * `srcEid` - Source chain endpoint ID * `amountLD` - Amount transferred * `composeFrom` - Original sender address (bytes32) * Your `composeMsg` payload The `MultiHopComposer` uses `OFTComposeMsgCodec.composeMsg(_message)` to extract your encoded `SendParam` from the full message. ## Fee Estimation Estimating the fee for a multi-hop transaction requires a two-step process because the source chain cannot directly quote the cost of the second hop (Hub → Destination). ### Step 1: Quote the Second Hop (Hub → Destination) You must call `IOFT.quoteSend()` on the **Hub Chain's OFT contract** (the lockbox or adapter) to get the exact fee for the transfer from Hub to final destination. This quote must be obtained **off-chain** before constructing your source transaction: ```javascript wrap theme={null} import {ethers} from 'ethers'; // Connect to Hub Chain const hubProvider = new ethers.JsonRpcProvider(HUB_RPC_URL); const hubOFT = new ethers.Contract(HUB_OFT_ADDRESS, IOFT_ABI, hubProvider); // Construct the nextHopParam (same as what you'll encode in composeMsg) const nextHopParam = { dstEid: FINAL_DESTINATION_EID, to: ethers.zeroPadValue(recipientAddress, 32), amountLD: amountToSend, minAmountLD: 0, extraOptions: nextHopOptions, // lzReceive gas for final destination composeMsg: '0x', oftCmd: '0x', }; // Quote the second hop on the Hub chain const nextHopQuote = await hubOFT.quoteSend(nextHopParam, false); const nextHopNativeFee = nextHopQuote.nativeFee; // Use this in Step 2 ``` ### Step 2: Pack Quote into First Hop Options Once you have the `nativeFee` for the second hop, include it in the options for the first hop (Source → Hub): 1. **Add `lzReceiveOption`**: Gas for the Hub OFT's `_lzReceive` (credits tokens + calls `sendCompose`) 2. **Add `lzComposeOption`**: Gas for `MultiHopComposer.lzCompose()` execution, plus the `nativeFee` as `msg.value` 3. **Quote First Hop**: Call `quoteSend()` on the source chain. The returned fee includes everything needed for the full journey. ```javascript wrap theme={null} // Build first hop options with the quoted second hop fee const firstHopOptions = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(65000, 0) // Hub _lzReceive gas .addExecutorLzComposeOption(0, 500000, nextHopNativeFee); // Compose gas + second hop fee // Now quote the first hop on source chain const sourceOFT = new ethers.Contract(SOURCE_OFT_ADDRESS, IOFT_ABI, sourceProvider); const firstHopQuote = await sourceOFT.quoteSend(sendParam, false); // firstHopQuote.nativeFee is your total msg.value for the entire cross-mesh transfer ``` ### Fee Flow The `msg.value` passed via `addExecutorLzComposeOption` is delivered to the `MultiHopComposer` on the Hub. The composer then uses this value to pay for the `IOFT.send()` call that initiates the second hop. ### Finding Addresses Always verify the correct `MultiHopComposer` address for your specific lzAsset token pair. Some deployments may use different Hub chains or composer instances. # Omnichain Queries (LayerZero Read) Source: https://docs.layerzero.network/v2/developers/evm/lzread/overview Step-by-step guide to omnichain queries (layerzero read) using LayerZero V2. Build and deploy omnichain applications with crosschain messaging. Follow step-... **LayerZero Read (lzRead)** enables smart contracts to request and retrieve onchain state from other blockchains using LayerZero's crosschain infrastructure. Unlike traditional messaging that sends data from source to destination, lzRead implements a request-response pattern where contracts can pull external state data from other blockchains. For conceptual information about how Omnichain Queries work, see the [Read Standard Overview](../../../concepts/applications/read-standard). #### Key Differences from Push-based Messaging | Feature | **Omnichain Message** | **Omnichain Read** | | ----------- | ---------------------------------------- | ---------------------------------------------- | | **Flow** | Source sends data to destination | Source requests data, source receives response | | **Data** | `bytes` sent = `bytes` received | `bytes` request ≠ `bytes` response | | **Purpose** | ***Push*** state changes to other chains | ***Pull*** external state from other chains | ## Supported Chains lzRead requires compatible Message Libraries (`ReadLib1002`) and DVNs with archival node access. See [Read Paths](../../../deployments/read-contracts) for available chains and DVNs. Table showing lzRead compatible configurations with columns for Origin Chain, DVN Provider, Origin Chain Library (ReadLib1002), Origin Chain DVN, and Target Data Chains including Arbitrum, Base, Ethereum, and Optimism Mainnet Table showing lzRead compatible configurations with columns for Origin Chain, DVN Provider, Origin Chain Library (ReadLib1002), Origin Chain DVN, and Target Data Chains including Arbitrum, Base, Ethereum, and Optimism Mainnet ## Installation To start using LayerZero Read in a new project, use the LayerZero CLI tool, [**create-lz-oapp**](../../../get-started/create-lz-oapp/start). The CLI tool allows developers to create any omnichain application with read capabilities quickly! Get started by running the following from your command line: ```bash wrap theme={null} LZ_ENABLE_READ_EXAMPLE=1 npx create-lz-oapp@latest --example oapp-read ``` Select the **OApp Read** template when prompted. This creates a complete project with: * Example contracts with read capabilities * Crosschain unit tests for read operations * Custom LayerZero read configuration files * Deployment scripts and setup To use LayerZero Read contracts in an existing project, you can install the **OApp package** directly: ```bash wrap npm theme={null} npm install @layerzerolabs/oapp-evm ``` ```bash wrap yarn theme={null} yarn add @layerzerolabs/oapp-evm ``` ```bash wrap pnpm theme={null} pnpm add @layerzerolabs/oapp-evm ``` ```bash wrap forge theme={null} forge init forge install layerzero-labs/devtools forge install layerzero-labs/LayerZero-v2 forge install OpenZeppelin/openzeppelin-contracts git submodule add https://github.com/GNSPS/solidity-bytes-utils.git lib/solidity-bytes-utils ``` Then add to your `foundry.toml` under `[profile.default]`: ```toml wrap forge theme={null} [profile.default] src = "src" out = "out" libs = ["lib"] remappings = [ '@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/', '@layerzerolabs/lz-evm-protocol-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/protocol', '@layerzerolabs/lz-evm-messagelib-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/messagelib', 'solidity-bytes-utils/=lib/solidity-bytes-utils/', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', ] ``` LayerZero contracts work with both [**OpenZeppelin V5**](https://docs.openzeppelin.com/contracts/5.x/access-control#ownership-and-ownable) and V4 contracts. Specify your desired version in your project's package.json: ```typescript wrap theme={null} "resolutions": { "@openzeppelin/contracts": "^5.0.1", } ``` ## Custom Read Contract To build your own crosschain read application, inherit from `OAppRead.sol` and implement three key pieces: 1. **Read request construction**: How you build queries for external data 2. **Fee estimation**: How you calculate costs before sending requests 3. **Response handling**: How you process returned data in `_lzReceive` Below is the complete example that comes with the CLI tool, showing: * A constructor setting up the LayerZero endpoint, owner, and read channel * A `readData(...)` function that builds and sends read requests * A `quoteReadFee(...)` function to estimate costs before sending * An override of `_lzReceive(...)` that processes returned data * A target contract interface for type-safe interactions ```solidity wrap theme={null} // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; // Import necessary interfaces and contracts import { AddressCast } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/AddressCast.sol"; import { MessagingFee, MessagingReceipt } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { ReadCodecV1, EVMCallRequestV1 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/ReadCodecV1.sol"; import { OAppRead } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppRead.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; /// @title IExampleContract /// @notice Interface for the ExampleContract's `data()` function. interface IExampleContract { function data() external view returns (uint256); } /// @title ReadPublic /// @notice An OAppRead contract example to read a public state variable from another chain. contract ReadPublic is OAppRead, OAppOptionsType3 { /// @notice Emitted when the data is received. /// @param data The value of the public state variable. event DataReceived(uint256 data); /// @notice LayerZero read channel ID. uint32 public READ_CHANNEL; /// @notice Message type for the read operation. uint16 public constant READ_TYPE = 1; /** * @notice Constructor to initialize the OAppRead contract. * * @param _endpoint The LayerZero endpoint contract address. * @param _delegate The address that will have ownership privileges. * @param _readChannel The LayerZero read channel ID. */ constructor( address _endpoint, address _delegate, uint32 _readChannel ) OAppRead(_endpoint, _delegate) Ownable(_delegate) { READ_CHANNEL = _readChannel; _setPeer(_readChannel, AddressCast.toBytes32(address(this))); } // ────────────────────────────────────────────────────────────────────────────── // 0. (Optional) Quote business logic // // Example: Get a quote from the Endpoint for a cost estimate of reading data. // Replace this to mirror your own read business logic. // ────────────────────────────────────────────────────────────────────────────── /** * @notice Estimates the messaging fee required to perform the read operation. * * @param _targetContractAddress The address of the contract on the target chain containing the `data` variable. * @param _targetEid The target chain's Endpoint ID. * @param _extraOptions Additional messaging options. * * @return fee The estimated messaging fee. */ function quoteReadFee( address _targetContractAddress, uint32 _targetEid, bytes calldata _extraOptions ) external view returns (MessagingFee memory fee) { return _quote( READ_CHANNEL, _getCmd(_targetContractAddress, _targetEid), combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions), false ); } // ────────────────────────────────────────────────────────────────────────────── // 1a. Send business logic // // Example: send a read request to fetch data from a remote contract. // Replace this with your own read request logic. // ────────────────────────────────────────────────────────────────────────────── /** * @notice Sends a read request to fetch the public state variable `data`. * * @dev The caller must send enough ETH to cover the messaging fee. * * @param _targetContractAddress The address of the contract on the target chain containing the `data` variable. * @param _targetEid The target chain's Endpoint ID. * @param _extraOptions Additional messaging options. * * @return receipt The LayerZero messaging receipt for the request. */ function readData( address _targetContractAddress, uint32 _targetEid, bytes calldata _extraOptions ) external payable returns (MessagingReceipt memory receipt) { // 1. Build the read command for the target contract and function bytes memory cmd = _getCmd(_targetContractAddress, _targetEid); // 2. Send the read request via LayerZero // - READ_CHANNEL: Special channel ID for read operations // - cmd: Encoded read command with target details // - combineOptions: Merge enforced options with caller-provided options // - MessagingFee(msg.value, 0): Pay all fees in native gas; no ZRO // - payable(msg.sender): Refund excess gas to caller return _lzSend( READ_CHANNEL, cmd, combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions), MessagingFee(msg.value, 0), payable(msg.sender) ); } // ────────────────────────────────────────────────────────────────────────────── // 1b. Read command construction // // This function defines WHAT data to fetch from the target network and WHERE to fetch it from. // This is the core of LayerZero Read - specifying exactly which contract function to call // on which chain and how to handle the request. // ────────────────────────────────────────────────────────────────────────────── /** * @notice Constructs the read command to fetch the `data` variable from target chain. * @dev This function defines the core read operation - what data to fetch and from where. * Replace this logic to read different functions or data from your target contracts. * * @param _targetContractAddress The address of the contract containing the `data` variable. * @param _targetEid The target chain's Endpoint ID. * * @return cmd The encoded command that specifies what data to read. */ function _getCmd(address _targetContractAddress, uint32 _targetEid) internal view returns (bytes memory cmd) { // 1. Define WHAT function to call on the target contract // Using the interface selector ensures type safety and correctness // You can replace this with any public/external function or state variable bytes memory callData = abi.encodeWithSelector(IExampleContract.data.selector); // 2. Build the read request specifying WHERE and HOW to fetch the data EVMCallRequestV1[] memory readRequests = new EVMCallRequestV1[](1); readRequests[0] = EVMCallRequestV1({ appRequestLabel: 1, // Label for tracking this specific request targetEid: _targetEid, // WHICH chain to read from isBlockNum: false, // Use timestamp (not block number) blockNumOrTimestamp: uint64(block.timestamp), // WHEN to read the state (current time) confirmations: 15, // HOW many confirmations to wait for to: _targetContractAddress, // WHERE - the contract address to call callData: callData // WHAT - the function call to execute }); // 3. Encode the complete read command // No compute logic needed for simple data reading // The appLabel (0) can be used to identify different types of read operations cmd = ReadCodecV1.encode(0, readRequests); } // ────────────────────────────────────────────────────────────────────────────── // 2. Receive business logic // // Override _lzReceive to handle the returned data from the read request. // The base OAppReceiver.lzReceive ensures: // • Only the LayerZero Endpoint can call this method // • The sender is a registered peer (peers[srcEid] == origin.sender) // ────────────────────────────────────────────────────────────────────────────── /** * @notice Handles the received data from the target chain. * * @dev This function is called internally by the LayerZero protocol. * @dev _origin Metadata (source chain, sender address, nonce) * @dev _guid Global unique ID for tracking this response * @param _message The data returned from the read request (uint256 in this case) * @dev _executor Executor address that delivered the response * @dev _extraData Additional data from the Executor (unused here) */ function _lzReceive( Origin calldata /*_origin*/, bytes32 /*_guid*/, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { // 1. Decode the returned data from bytes to uint256 uint256 data = abi.decode(_message, (uint256)); // 2. Emit an event with the received data emit DataReceived(data); // 3. (Optional) Apply your custom logic here. // e.g., store the data, trigger additional actions, etc. } // ────────────────────────────────────────────────────────────────────────────── // 3. Admin functions // // Functions for managing the read channel configuration. // ────────────────────────────────────────────────────────────────────────────── /** * @notice Sets the LayerZero read channel. * * @dev Only callable by the owner. * * @param _channelId The channel ID to set. * @param _active Flag to activate or deactivate the channel. */ function setReadChannel(uint32 _channelId, bool _active) public override onlyOwner { _setPeer(_channelId, _active ? AddressCast.toBytes32(address(this)) : bytes32(0)); READ_CHANNEL = _channelId; } } ``` ### Target Contract The read contract interacts with a simple target contract deployed on other chains. This example demonstrates how you can **call a public data variable on a destination network and get the current state from another network** using LayerZero Read: ```solidity wrap theme={null} // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; /** * @title ExampleContract * @notice A simple contract with a public state variable that can be read crosschain. * @dev This contract would be deployed on target chains (e.g., Ethereum, Polygon, Arbitrum) * and the ReadPublic contract can fetch its `data` value from any other supported chain. */ contract ExampleContract { /// @notice Public state variable that can be read from other chains /// @dev The public keyword automatically generates a getter function data() uint256 public data; constructor(uint256 _data) { data = _data; } } ``` **Crosschain Reading Example:** * Deploy `ExampleContract` on a target network with `data = 100` * Deploy `ReadPublic` on your source network * Call `readData(targetContractAddress, targetEid, "0x")` from source * The contract's configured DVNs will fetch the current value of `data` (100) from the target and emit `DataReceived(100)` on source This enables real-time access to state from any supported blockchain without complex bridging or manual oracle updates. ### Constructor * Pass the Endpoint V2 address, owner address, and read channel ID into the base contracts. * `OAppRead(_endpoint, _delegate)` binds your contract to LayerZero and sets the delegate * `Ownable(_delegate)` makes the delegate the only address that can change configurations * `_setPeer(_readChannel, AddressCast.toBytes32(address(this)))` establishes the read channel ### readData(...) 1. **Build the read command** * `_getCmd()` constructs the query specifying what data to fetch and from where * Uses `IExampleContract.data.selector` for type-safe function selection 2. **Send the read request** * `_lzSend()` packages and dispatches the read request via LayerZero * `READ_CHANNEL` is the special channel ID for read operations * `_combineOptions()` merges enforced options with caller-provided options ### \_lzReceive(...) 1. **Endpoint verification** * Only the LayerZero Endpoint can invoke this function * The call succeeds only if the sender matches the registered read channel peer 2. **Decode the returned data** * Use `abi.decode(_message, (uint256))` to extract the original data * The data format matches what the target contract's `data()` function returns 3. **Process the result** * Emit `DataReceived` event with the fetched data * Add any custom business logic needed for your application ### (Optional) quoteReadFee(...) You can call the internal `_quote(...)` method to get accurate cost estimates before sending read requests. **Example usage:** ```solidity wrap theme={null} // Get fee estimate first MessagingFee memory fee = readPublic.quoteReadFee( targetContractAddress, targetEid, "0x" // no additional options ); // Then send with the estimated fee readPublic.readData{value: fee.nativeFee}( targetContractAddress, targetEid, "0x" ); ``` ## Deployment and Wiring lzRead wiring is **significantly simpler** than traditional crosschain messaging setup. Unlike OApp messaging where you need to configure peer connections between each contract on every chain pathway, lzRead only requires configuring the **source chain** (where your `OAppRead` child contract lives and where response data will be returned to). **Key Simplifications:** * **Single-sided peer wiring**: You only need to set the `OAppRead` address itself as the peer * **Dynamic target selection**: Target chains are specified in the read command itself, not in wiring * **DVN requirements**: DVNs must support the target chains and `block.number` or `block.timestamp` you want to read * **Single-direction setup**: Only configure the source chain to receive responses This means you can read from **any supported target chain** without additional wiring by just specifying the target in your `EVMCallRequestV1` and ensuring your DVNs support that chain. #### Execution Options for lzRead lzRead uses different execution options than standard messaging. Instead of `addExecutorLzReceiveOption`, you must use `addExecutorLzReadOption` with **calldata size estimation**: ```solidity wrap theme={null} // Standard messaging options OptionsBuilder.newOptions().addExecutorLzReceiveOption(100000, 0); // lzRead options (note the size parameter) OptionsBuilder.newOptions().addExecutorLzReadOption(100000, 64, 0); // gas size value ``` **Key difference**: The `size` parameter estimates your response data size in bytes. If your actual response exceeds this size, the executor won't deliver automatically. **Size estimation**: uint256 = 32 bytes, address = 20 bytes, etc. The `enforcedOptions` in your configuration (shown below) should account for your expected response sizes. For details on options configuration, see [**Execution Options**](../configuration/options). #### Deploy and Wire Deploy your lzRead OApp: ```bash wrap theme={null} # Deploy contracts npx hardhat lz:deploy ``` Then, review the `layerzero.config.ts` with read-specific settings: ```typescript wrap theme={null} import {ChannelId, EndpointId} from '@layerzerolabs/lz-definitions'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {type OAppReadOmniGraphHardhat, type OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; const arbsepContract: OmniPointHardhat = { eid: EndpointId.ARBSEP_V2_TESTNET, contractName: 'ReadPublic', }; const config: OAppReadOmniGraphHardhat = { contracts: [ { contract: arbsepContract, config: { readChannelConfigs: [ { channelId: ChannelId.READ_CHANNEL_1, active: true, readLibrary: '0x54320b901FDe49Ba98de821Ccf374BA4358a8bf6', ulnConfig: { requiredDVNs: ['0x5c8c267174e1f345234ff5315d6cfd6716763bac'], executor: '0x5Df3a1cEbBD9c8BA7F8dF51Fd632A9aef8308897', }, enforcedOptions: [ { msgType: 1, optionType: ExecutorOptionType.LZ_READ, gas: 80000, size: 1000000, value: 0, }, ], }, ], }, }, ], connections: [], }; export default config; ``` Deploy and configure your read OApp: ```bash wrap theme={null} # Wire read configuration npx hardhat lz:oapp-read:wire --oapp-config layerzero.config.ts ``` This automatically: * Sets the ReadLib1002 as send/receive library * Configures required DVNs for read operations * Activates specified read channels * Sets up executor configuration For manual setup, you need to configure four components **on your source chain only**: ```solidity wrap theme={null} // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import { Script, console } from "forge-std/Script.sol"; import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; import { ExecutorOptions } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/ExecutorOptions.sol"; import { EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { ReadLibConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/readlib/ReadLibBase.sol"; import { SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManager.sol"; import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { ReadPublic } from "../src/ReadPublic.sol"; contract SetConfigScript is Script { using OptionsBuilder for bytes; // Configuration constants - REPLACE WITH YOUR VALUES uint32 public constant READ_CHANNEL = 4294967295; // LayerZero Read Channel ID address public constant ENDPOINT_ADDRESS = 0x1a44076050125825900e736c501f859c50fE728c; // LayerZero V2 Endpoint address public constant READ_LIB_ADDRESS = 0xbcd4CADCac3F767C57c4F402932C4705DF62BEFf; // ReadLib1002 address for your chain - UPDATE THIS address public constant READ_COMPATIBLE_DVN = 0x1308151a7ebaC14f435d3Ad5fF95c34160D539A5; // DVN that supports read operations - UPDATE THIS // Contract addresses to configure - SET THESE AFTER DEPLOYMENT address public readPublicAddress; function setUp() public { // Set your deployed ReadPublic contract address here readPublicAddress = vm.envAddress("READ_PUBLIC_ADDRESS"); } function run() public { vm.startBroadcast(); console.log("Configuring ReadPublic contract at:", readPublicAddress); // Get contract instances ILayerZeroEndpointV2 endpoint = ILayerZeroEndpointV2(ENDPOINT_ADDRESS); ReadPublic myReadApp = ReadPublic(readPublicAddress); // 1. Set Read Library (only on source chain) console.log("Step 1: Setting Read Library..."); endpoint.setSendLibrary(readPublicAddress, READ_CHANNEL, READ_LIB_ADDRESS); endpoint.setReceiveLibrary(readPublicAddress, READ_CHANNEL, READ_LIB_ADDRESS, 0); // 2. Configure DVNs (must support target chains you want to read from) console.log("Step 2: Configuring DVNs..."); SetConfigParam[] memory params = new SetConfigParam[](1); address[] memory requiredDVNs = new address[](1); requiredDVNs[0] = READ_COMPATIBLE_DVN; address[] memory optionalDVNs = new address[](0); params[0] = SetConfigParam({ eid: READ_CHANNEL, configType: 1, // LZ_READ_LID_CONFIG_TYPE config: abi.encode(ReadLibConfig({ executor: address(0x31CAe3B7fB82d847621859fb1585353c5720660D), // Executor address - UPDATE THIS requiredDVNCount: 1, optionalDVNCount: 0, optionalDVNThreshold: 0, requiredDVNs: requiredDVNs, optionalDVNs: optionalDVNs })) }); endpoint.setConfig(readPublicAddress, READ_LIB_ADDRESS, params); // 3. Activate Read Channel (enables receiving responses) console.log("Step 3: Activating Read Channel..."); myReadApp.setReadChannel(READ_CHANNEL, true); // 4. Set Enforced Options (with lzRead-specific options) console.log("Step 4: Setting Enforced Options..."); EnforcedOptionParam[] memory enforcedOptions = new EnforcedOptionParam[](1); enforcedOptions[0] = EnforcedOptionParam({ eid: READ_CHANNEL, msgType: 1, // READ_MSG_TYPE options: OptionsBuilder.newOptions().addExecutorLzReadOption(50000, 128, 0) }); myReadApp.setEnforcedOptions(enforcedOptions); console.log("Configuration complete!"); vm.stopBroadcast(); } } ``` **No target chain configuration needed!** Target chains and contracts are specified dynamically in your `_getCmd()` function via the `targetEid` and `to` parameters in `EVMCallRequestV1`. ## Usage Once deployed and wired, you can begin reading data from contracts on other chains. ### Read data The LayerZero CLI provides a convenient task for reading crosschain data that automatically handles fee estimation and transaction execution. #### Using the Read Task The CLI includes a built-in `lz:oapp-read:read` task that: 1. Finds your deployed ReadPublic contract automatically 2. Quotes the gas cost using your contract's `quoteReadFee()` function 3. Sends the read request with the correct fee 4. Provides tracking links for the transaction **Basic usage:** ```bash wrap theme={null} npx hardhat lz:oapp-read:read --target-contract 0x1234567890123456789012345678901234567890 --target-eid 30101 ``` **Required Parameters:** * `--target-contract`: Address of the contract to read from on the target chain * `--target-eid`: Target chain endpoint ID (e.g., 30101 for Ethereum) **Optional Parameters:** * `--options`: Additional execution options as hex string (default: "0x") **Example with options:** ```bash wrap theme={null} npx hardhat lz:oapp-read:read \ --target-contract 0x1234567890123456789012345678901234567890 \ --target-eid 30101 \ --options 0x00030100110100000000000000000000000000030d40 ``` The task automatically: * Finds your deployed ReadPublic contract from deployment artifacts * Quotes the exact gas fee needed using `quoteReadFee()` * Sends the read request with proper fee payment * Provides block explorer and LayerZero Scan links for tracking * Shows the transaction details and gas usage **Example output:** ``` ✅ SENT_READ_REQUEST: Successfully sent read request from arbitrum-sepolia to ethereum ✅ TX_HASH: Block explorer link for source chain arbitrum-sepolia: https://sepolia.arbiscan.io/tx/0x... ✅ EXPLORER_LINK: LayerZero Scan link for tracking read request: https://testnet.layerzeroscan.com/tx/0x... 📖 Read request sent! The data will be received and emitted in a DataReceived event. Check the ReadPublic contract for the DataReceived event to see the result. ``` For manual deployment and testing with Foundry, use the following deployment scripts: #### 1. Deploy Target Contract First, deploy the `ExampleContract` that will be read from: ```solidity wrap theme={null} // script/DeployExampleContract.s.sol forge script script/DeployExampleContract.s.sol:DeployExampleContractScript \ --rpc-url $RPC_URL_TARGET \ --private-key $PRIVATE_KEY \ --broadcast ``` #### 2. Deploy ReadPublic Contract Deploy the lzRead contract on your source chain: ```solidity wrap theme={null} // script/DeployReadPublic.s.sol forge script script/DeployReadPublic.s.sol:DeployReadPublicScript \ --rpc-url $RPC_URL_SOURCE \ --private-key $PRIVATE_KEY \ --broadcast ``` Make sure to update the `ENDPOINT_ADDRESS` and `READ_CHANNEL` constants in the script to match your deployment network. #### 3. Configure the Read Contract After deployment, configure your contract with the proper LayerZero settings: ```solidity wrap theme={null} // script/SetConfig.s.sol forge script script/SetConfig.s.sol:SetConfigScript \ --rpc-url $RPC_URL_SOURCE \ --private-key $PRIVATE_KEY \ --broadcast ``` This script: * Sets the Read Library addresses * Configures DVNs using `ReadLibConfig` struct * Activates the read channel * Sets enforced options for lzRead The configuration uses `ReadLibConfig` (not `UlnConfig`) with these required fields: * `executor`: Address of the executor * `requiredDVNCount`: Number of required DVNs * `optionalDVNCount`: Number of optional DVNs * `optionalDVNThreshold`: Threshold for optional DVNs * `requiredDVNs`: Array of required DVN addresses * `optionalDVNs`: Array of optional DVN addresses #### 4. Test the Read Functionality Execute a test read to verify everything is working: ```solidity wrap theme={null} // script/TestRead.s.sol forge script script/TestRead.s.sol:TestReadScript \ --rpc-url $RPC_URL_SOURCE \ --private-key $PRIVATE_KEY \ --broadcast ``` This script will: 1. Quote the fee for the read operation 2. Send the read request with the proper fee 3. Display transaction details and tracking information #### Complete Example Workflow ```bash wrap theme={null} # 1. Set environment variables export PRIVATE_KEY="your_private_key" export RPC_URL_TARGET="https://rpc.target-chain.com" export RPC_URL_SOURCE="https://rpc.source-chain.com" # 2. Deploy target contract (e.g., on Optimism) forge script script/DeployExampleContract.s.sol:DeployExampleContractScript \ --rpc-url $RPC_URL_TARGET \ --private-key $PRIVATE_KEY \ --broadcast # 3. Add deployed address to .env echo "EXAMPLE_CONTRACT_ADDRESS=0x..." >> .env # 4. Deploy ReadPublic on source chain (e.g., on Arbitrum) forge script script/DeployReadPublic.s.sol:DeployReadPublicScript \ --rpc-url $RPC_URL_SOURCE \ --private-key $PRIVATE_KEY \ --broadcast # 5. Add deployed address to .env echo "READ_PUBLIC_ADDRESS=0x..." >> .env # 6. Configure the ReadPublic contract forge script script/SetConfig.s.sol:SetConfigScript \ --rpc-url $RPC_URL_SOURCE \ --private-key $PRIVATE_KEY \ --broadcast # 7. Test the read functionality forge script script/TestRead.s.sol:TestReadScript \ --rpc-url $RPC_URL_SOURCE \ --private-key $PRIVATE_KEY \ --broadcast ``` ## Advanced Read Contracts lzRead supports several advanced patterns for crosschain data access. Each pattern addresses different use cases, from simple data retrieval to complex multi-chain aggregation with compute logic. ### tip **Complete examples** are available in the [**LayerZero devtools repository**](https://github.com/LayerZero-Labs/devtools/tree/82285d5c566d6cea4d7d0cc05899c9838b0a4c6c/examples/). These examples provide full contract implementations you can deploy and test. ### Call View/Pure Functions You can use lzRead to call any `view` or `pure` function on a target chain and bring the returned data back to your source chain contract. This is the fundamental lzRead pattern that enables crosschain function execution without state changes. **Core concept:** Instead of deploying identical contracts on every chain or building complex bridging infrastructure, lzRead lets you call functions on any supported chain and receive the results natively. The target function executes via `eth_call`, ensuring no state modification occurs. **Use cases:** * **Crosschain calculations**: Call mathematical functions, pricing algorithms, or complex computations * **Remote contract queries**: Access getter functions, view state, or computed values from contracts on other chains * **Protocol integration**: Query external protocols (like AMMs, lending protocols, oracles) without deploying wrappers * **Data aggregation**: Collect information from various chains' contracts for unified processing * **Validation**: Verify conditions or states across multiple chains before executing local logic **Key implementation details:** * Single `EVMCallRequestV1` targeting specific function with parameters * Target function must be `view` or `pure` to ensure no state changes * Raw response data returned directly to `_lzReceive` - no compute processing needed * Function selector and parameter encoding handled via standard ABI encoding * Works with any function signature: simple getters, complex multi-parameter functions, struct returns #### Installation Get started quickly with a pre-built lzRead example for reading view or pure functions: ```bash wrap theme={null} LZ_ENABLE_READ_EXAMPLE=1 npx create-lz-oapp@latest --example view-pure-read ``` This creates a complete lzRead project with: * Example contracts for reading `view`/`pure` functions * Deploy and configuration scripts * Test suites demonstrating all patterns * Ready-to-use implementations you can customize #### Contract Example ```solidity wrap theme={null} // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; // Import necessary interfaces and contracts import { AddressCast } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/AddressCast.sol"; import { MessagingFee, MessagingReceipt } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { OAppRead } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppRead.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { EVMCallRequestV1, ReadCodecV1 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/ReadCodecV1.sol"; /// @title IExampleContract /// @notice Interface for the ExampleContract's `add` function. interface IExampleContract { function add(uint256 a, uint256 b) external pure returns (uint256); } /// @title ReadViewOrPure Example /// @notice An OAppRead contract that calls view/pure functions on target chains and receives results contract ReadViewOrPure is OAppRead, OAppOptionsType3 { /// @notice Emitted when crosschain function data is successfully received event SumReceived(uint256 sum); /// @notice LayerZero read channel ID for crosschain data requests uint32 public READ_CHANNEL; /// @notice Message type identifier for read operations uint16 public constant READ_TYPE = 1; /// @notice Target chain's LayerZero Endpoint ID (immutable after deployment) uint32 public immutable targetEid; /// @notice Address of the contract to read from on the target chain address public immutable targetContractAddress; /** * @notice Initialize the crosschain read contract * @dev Sets up LayerZero connectivity and establishes read channel peer relationship * @param _endpoint LayerZero endpoint address on the source chain * @param _readChannel Read channel ID for this contract's operations * @param _targetEid Destination chain's endpoint ID where target contract lives * @param _targetContractAddress Contract address to read from on target chain */ constructor( address _endpoint, uint32 _readChannel, uint32 _targetEid, address _targetContractAddress ) OAppRead(_endpoint, msg.sender) Ownable(msg.sender) { READ_CHANNEL = _readChannel; targetEid = _targetEid; targetContractAddress = _targetContractAddress; // Establish read channel peer - contract reads from itself via LayerZero _setPeer(READ_CHANNEL, AddressCast.toBytes32(address(this))); } /** * @notice Configure the LayerZero read channel for this contract * @dev Owner-only function to activate/deactivate read channels * @param _channelId Read channel ID to configure * @param _active Whether to activate (true) or deactivate (false) the channel */ function setReadChannel(uint32 _channelId, bool _active) public override onlyOwner { // Set or clear the peer relationship for the read channel _setPeer(_channelId, _active ? AddressCast.toBytes32(address(this)) : bytes32(0)); READ_CHANNEL = _channelId; } /** * @notice Execute a crosschain read request to call the target function * @dev Builds the read command and sends it via LayerZero messaging * @param _a First parameter for the target function * @param _b Second parameter for the target function * @param _extraOptions Additional execution options (gas, value, etc.) * @return receipt LayerZero messaging receipt containing transaction details */ function readSum( uint256 _a, uint256 _b, bytes calldata _extraOptions ) external payable returns (MessagingReceipt memory) { // 1. Build the read command specifying target function and parameters bytes memory cmd = _getCmd(_a, _b); // 2. Send the read request via LayerZero return _lzSend( READ_CHANNEL, cmd, combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions), MessagingFee(msg.value, 0), payable(msg.sender) ); } /** * @notice Get estimated messaging fee for a crosschain read operation * @dev Calculates LayerZero fees before sending to avoid transaction failures * @param _a First parameter for the target function * @param _b Second parameter for the target function * @param _extraOptions Additional execution options * @return fee Estimated LayerZero messaging fee structure */ function quoteReadFee( uint256 _a, uint256 _b, bytes calldata _extraOptions ) external view returns (MessagingFee memory fee) { // Build the same command as readSum and quote its cost return _quote(READ_CHANNEL, _getCmd(_a, _b), combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions), false); } /** * @notice Build the LayerZero read command for target function execution * @dev Constructs EVMCallRequestV1 specifying what data to fetch and from where * @param _a First parameter to pass to target function * @param _b Second parameter to pass to target function * @return Encoded read command for LayerZero execution */ function _getCmd(uint256 _a, uint256 _b) internal view returns (bytes memory) { // 1. Build the function call data // Encode the target function selector with parameters bytes memory callData = abi.encodeWithSelector(IExampleContract.add.selector, _a, _b); // 2. Create the read request structure EVMCallRequestV1[] memory readRequests = new EVMCallRequestV1[](1); readRequests[0] = EVMCallRequestV1({ appRequestLabel: 1, // Request identifier for tracking targetEid: targetEid, // Which chain to read from isBlockNum: false, // Use timestamp instead of block number for data freshness blockNumOrTimestamp: uint64(block.timestamp), // Read current state confirmations: 15, // Wait for block finality before executing to: targetContractAddress, // Target contract address callData: callData // The function call to execute }); // 3. Encode the command (no compute logic needed for simple reads) return ReadCodecV1.encode(0, readRequests); } /** * @notice Process the received data from the target chain * @dev Called by LayerZero when the read response is delivered * @param _message Encoded response data from the target function call */ function _lzReceive( Origin calldata /*_origin*/, bytes32 /*_guid*/, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { // 1. Validate response format require(_message.length == 32, "Invalid message length"); // 2. Decode the returned data (matches target function return type) uint256 sum = abi.decode(_message, (uint256)); // 3. Process the result (emit event, update state, trigger logic, etc.) emit SumReceived(sum); } } // Example target contract for demonstration contract ExampleContract { /** * @notice Adds two numbers. * @param a First number. * @param b Second number. * @return sum The sum of a and b. */ function add(uint256 a, uint256 b) external pure returns (uint256 sum) { return a + b; } } ``` **Crosschain View/Pure Function Reading:** * Deploy `ReadViewOrPure` on your source network * Call `readSum(5, 10, "0x")` to execute the add function on the target chain * The contract's DVNs fetch the result directly and deliver it to `SumReceived(15)` event * No compute processing - raw response delivered directly to your contract This enables direct access to any view/pure function across supported chains without complex bridging infrastructure. #### Constructor * Pass the Endpoint V2 address, owner address, read channel ID, target chain ID, and target contract address * `OAppRead(_endpoint, msg.sender)` binds your contract to LayerZero and sets the delegate * `Ownable(msg.sender)` makes the deployer the only address that can change configurations * `_setPeer(READ_CHANNEL, AddressCast.toBytes32(address(this)))` establishes the read channel peer relationship #### readSum(...) 1. **Build the read command** * `_getCmd()` constructs the query specifying target function with parameters * Uses `IExampleContract.add.selector` for type-safe function selection 2. **Send the read request** * `_lzSend()` packages and dispatches the read request via LayerZero * `combineOptions()` merges enforced options with caller-provided options * Caller must provide sufficient native fee for crosschain execution #### \_getCmd(...) 1. **Encode the function call** * Build `callData` using function selector and parameters * Standard ABI encoding for target contract interface 2. **Create the read request** * Single `EVMCallRequestV1` targeting specific chain and contract * `appRequestLabel: 1` for request tracking and identification * Uses current timestamp for fresh data reads 3. **Encode the command** * `ReadCodecV1.encode(0, readRequests)` with no compute logic * AppLabel 0 indicates basic read without additional processing #### \_lzReceive(...) 1. **Endpoint verification** * Only LayerZero Endpoint can invoke this function * Validates sender matches registered read channel peer 2. **Decode the response** * Extract raw data using `abi.decode(_message, (uint256))` * Data format matches target function's return type exactly 3. **Process the result** * Emit `SumReceived` event with the fetched data * Add custom business logic, state updates, or trigger additional operations #### (Optional) quoteReadFee(...) Estimates messaging fees before sending to avoid transaction failures: ```solidity wrap theme={null} // Get fee estimate first MessagingFee memory fee = readContract.quoteReadFee(5, 10, "0x"); // Then send with estimated fee readContract.readSum{value: fee.nativeFee}(5, 10, "0x"); ``` #### Real-world applications The example above shows a simple mathematical function, but lzRead can call any view/pure function across chains: ```solidity wrap theme={null} // Query token balances on other chains function getBalance(address user) external view returns (uint256); // Access oracle prices from different chains function getLatestPrice() external view returns (uint256 price, uint256 timestamp); // Check protocol states across deployments function getTotalSupply() external view returns (uint256); function getReserves() external view returns (uint112 reserve0, uint112 reserve1); // Validate conditions before crosschain actions function isEligibleForRewards(address user) external view returns (bool eligible, uint256 amount); // Query governance states function getProposalState(uint256 proposalId) external view returns (uint8 state); ``` The key advantage is **data locality** - instead of bridging tokens or deploying contracts everywhere, you can query any chain's data directly and use it in your source chain logic. ### Add Compute Logic to Responses Add off-chain data processing to transform, validate, or format response data before it reaches your contract. The compute layer executes between the DVN(s) getting the response data from your target contract and delivering it to your `_lzReceive` function, allowing complex data manipulation without additional gas costs. **Core concept:** After DVNs fetch your requested data, the compute layer can process it off-chain using your custom `lzMap` and `lzReduce` functions. This enables data transformation, validation, aggregation, and formatting without consuming gas on your source chain. **How compute processing works:** 1. **DVNs fetch data** from your target contract using the specified function call 2. **lzMap executes** (if configured) to transform each individual response 3. **lzReduce executes** (if configured) to aggregate all mapped responses into a final result 4. **Final result delivered** to your `_lzReceive` function on the source chain The compute layer acts as a **middleware processing step** that runs off-chain but is cryptographically verified, giving you powerful data manipulation capabilities without gas costs. **Use cases:** * **Data transformation**: Convert complex structs into simpler formats your contract needs * **Response validation**: Filter out invalid responses or apply business logic rules * **Unit conversion**: Convert between different decimal places, currencies, or measurement units * **Data cleaning**: Remove outliers, normalize formats, or standardize responses * **Aggregation prep**: Process individual responses before combining them * **Format standardization**: Ensure all responses follow consistent encoding patterns **Key implementation details:** * Implement `IOAppMapper` for individual response processing and/or `IOAppReducer` for aggregation * Add `EVMCallComputeV1` struct to specify compute configuration * Configure `computeSetting`: `0` = lzMap only, `1` = lzReduce only, `2` = both * Compute functions execute off-chain, reducing gas costs for complex operations * `lzMap` processes each response individually; `lzReduce` combines all mapped responses #### Installation Get started quickly with a pre-built lzRead compute example: ```bash wrap theme={null} LZ_ENABLE_READ_EXAMPLE=1 npx create-lz-oapp@latest --example view-pure-read ``` The generated project includes the ReadViewOrPureAndCompute contract demonstrating the complete compute pipeline. #### Contract Example ```solidity wrap theme={null} // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; // Import necessary interfaces and contracts import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { OAppRead } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppRead.sol"; // highlight-start import { IOAppMapper } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppMapper.sol"; import { IOAppReducer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppReducer.sol"; // highlight-end import { EVMCallRequestV1, EVMCallComputeV1, ReadCodecV1 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/ReadCodecV1.sol"; import { AddressCast } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/AddressCast.sol"; import { MessagingFee, MessagingReceipt, ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; /// @title IExampleContract /// @notice Interface for the ExampleContract's `add` function. interface IExampleContract { function add(uint256 a, uint256 b) external pure returns (uint256); } /// @title ReadViewOrPureAndCompute /// @notice Crosschain read contract with compute processing for data transformation and aggregation contract ReadViewOrPureAndCompute is OAppRead, IOAppMapper, IOAppReducer, OAppOptionsType3 { /// @notice Emitted when final computed result is received from the compute pipeline event SumReceived(uint256 sum); /// @notice LayerZero read channel ID for crosschain data requests with compute uint32 public READ_CHANNEL; /// @notice Message type identifier for read operations with compute processing uint16 public constant READ_TYPE = 1; /// @notice Target chain's LayerZero Endpoint ID (immutable after deployment) uint32 public immutable targetEid; /// @notice Address of the contract to read from on the target chain address public immutable targetContractAddress; /** * @notice Initialize the crosschain read contract with compute capabilities * @dev Sets up LayerZero connectivity, establishes read channel, and enables compute processing * @param _endpoint LayerZero endpoint address on the source chain * @param _readChannel Read channel ID for this contract's operations * @param _targetEid Destination chain's endpoint ID where target contract lives * @param _targetContractAddress Contract address to read from on target chain */ constructor( address _endpoint, uint32 _readChannel, uint32 _targetEid, address _targetContractAddress ) OAppRead(_endpoint, msg.sender) Ownable(msg.sender) { READ_CHANNEL = _readChannel; targetEid = _targetEid; targetContractAddress = _targetContractAddress; // Establish read channel peer - contract processes its own compute functions _setPeer(READ_CHANNEL, AddressCast.toBytes32(address(this))); } /** * @notice Configure the LayerZero read channel for compute-enabled operations * @dev Owner-only function to activate/deactivate read channels with compute processing * @param _channelId Read channel ID to configure * @param _active Whether to activate (true) or deactivate (false) the channel */ function setReadChannel(uint32 _channelId, bool _active) public override onlyOwner { // Set or clear the peer relationship for compute-enabled read operations _setPeer(_channelId, _active ? AddressCast.toBytes32(address(this)) : bytes32(0)); READ_CHANNEL = _channelId; } /** * @notice Execute a crosschain read request with compute processing pipeline (Step 1) * @dev Builds read command with compute configuration and sends via LayerZero * @param _a First parameter for the target function * @param _b Second parameter for the target function * @param _extraOptions Additional execution options (gas, value, etc.) * @return receipt LayerZero messaging receipt containing transaction details */ function readSum( uint256 _a, uint256 _b, bytes calldata _extraOptions ) external payable returns (MessagingReceipt memory) { // 1. Build the read command with compute configuration bytes memory cmd = _getCmd(_a, _b); // 2. Send the read request with compute processing enabled return _lzSend( READ_CHANNEL, cmd, combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions), MessagingFee(msg.value, 0), payable(msg.sender) ); } /** * @notice Get estimated messaging fee for crosschain read with compute processing * @dev Calculates LayerZero fees including compute overhead before sending * @param _a First parameter for the target function * @param _b Second parameter for the target function * @param _extraOptions Additional execution options * @return fee Estimated LayerZero messaging fee structure */ function quoteReadFee( uint256 _a, uint256 _b, bytes calldata _extraOptions ) external view returns (MessagingFee memory fee) { // Build the same command as readSum (including compute config) and quote its cost return _quote(READ_CHANNEL, _getCmd(_a, _b), combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions), false); } /** * @notice Build the LayerZero read command with compute processing configuration * @dev Constructs both the target function call AND the compute pipeline setup * @param _a First parameter to pass to target function * @param _b Second parameter to pass to target function * @return Encoded read command with compute configuration for LayerZero execution */ function _getCmd(uint256 _a, uint256 _b) internal view returns (bytes memory) { // 1. Build the target function call data (same as basic read) bytes memory callData = abi.encodeWithSelector(IExampleContract.add.selector, _a, _b); // 2. Create the read request structure EVMCallRequestV1[] memory readRequests = new EVMCallRequestV1[](1); readRequests[0] = EVMCallRequestV1({ appRequestLabel: 1, // Request identifier for tracking through compute pipeline targetEid: targetEid, // Which chain to read from isBlockNum: false, // Use timestamp for data freshness blockNumOrTimestamp: uint64(block.timestamp), // Read current state confirmations: 15, // Wait for block finality before processing to: targetContractAddress, // Target contract address callData: callData // The function call to execute }); // highlight-start // 3. Configure the compute processing pipeline - THIS IS THE KEY DIFFERENCE EVMCallComputeV1 memory computeRequest = EVMCallComputeV1({ computeSetting: 2, // 0=lzMap only, 1=lzReduce only, 2=both lzMap and lzReduce targetEid: ILayerZeroEndpointV2(endpoint).eid(), // Execute compute on source chain (this chain) isBlockNum: false, // Use timestamp for compute execution timing blockNumOrTimestamp: uint64(block.timestamp), // When to execute compute functions confirmations: 15, // Confirmations needed before compute processing begins to: address(this) // Contract address containing lzMap/lzReduce implementations }); // 4. Encode the complete command (read requests + compute configuration) return ReadCodecV1.encode(0, readRequests, computeRequest); // highlight-end } // highlight-start /** * @notice Transform individual read responses during compute processing (Step 2 of compute pipeline) * @dev Called by LayerZero's compute layer for each raw response from target chains * @param _request Original request data (unused in this example, but available for context) * @param _response Raw response data from the target chain function call * @return Processed response data to pass to lzReduce (or final result if no reduce step) */ function lzMap( bytes calldata /*_request*/, bytes calldata _response ) external pure override returns (bytes memory) { // 1. Decode the raw response from target function (uint256 from add function) uint256 sum = abi.decode(_response, (uint256)); // 2. Apply transformation logic (example: increment by 1) // This could be: unit conversion, validation, filtering, formatting, etc. sum += 1; // 3. Re-encode for lzReduce or final delivery return abi.encode(sum); } /** * @notice Aggregate all mapped responses into final result (Step 3 of compute pipeline) * @dev Called after all lzMap operations complete, receives array of mapped responses * @param _cmd Original command data (unused in this example, but available for context) * @param _responses Array of processed responses from lzMap function * @return Final aggregated data to deliver to _lzReceive */ function lzReduce( bytes calldata /*_cmd*/, bytes[] calldata _responses ) external pure override returns (bytes memory) { uint256 totalSum = 0; // Process each mapped response and aggregate them for (uint256 i = 0; i < _responses.length; i++) { // 1. Validate each response format require(_responses[i].length == 32, "Invalid response length"); // 2. Decode the mapped response uint256 sum = abi.decode(_responses[i], (uint256)); // 3. Apply aggregation logic (example: sum all responses) totalSum += sum; } // 4. Return final aggregated result return abi.encode(totalSum); } // highlight-end /** * @notice Process the final computed result from the compute pipeline (Step 4) * @dev Called by LayerZero when compute processing is complete and result is delivered * @param _message Final processed data from the compute pipeline (lzReduce output) */ function _lzReceive( Origin calldata /*_origin*/, bytes32 /*_guid*/, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { // 1. Validate final result format require(_message.length == 32, "Invalid message length"); // 2. Decode the final computed result uint256 sum = abi.decode(_message, (uint256)); // 3. Process the result (emit event, update state, trigger logic, etc.) emit SumReceived(sum); } } ``` **Crosschain Reading with Compute Processing:** * Deploy `ReadViewOrPureAndCompute` on your source network * Call `readSum(5, 10, "0x")` to execute the add function on the target chain with compute processing * The contract's DVNs fetch the result, lzMap transforms it (+1), lzReduce aggregates each response (by default only 1 response, so unchanged), and the final computed result is delivered to `SumReceived(16)` event This enables sophisticated data processing pipelines where raw crosschain data is transformed and aggregated off-chain before reaching your contract. #### Constructor * Initialize the contract with compute capabilities enabled via `IOAppMapper` and `IOAppReducer` interfaces * Sets up LayerZero connectivity and establishes read channel peer relationship for compute operations * The contract becomes both the read requester and the compute processor (via `address(this)` in compute configuration) #### readSum(...) **Step 1 of compute pipeline:** Dispatch read request with compute command 1. **Build the compute command** * `_getCmd()` constructs both the read request AND the compute configuration * Specifies which compute functions to use (`lzMap`, `lzReduce`, or both) 2. **Send the read request** * `_lzSend()` packages and dispatches the read request with compute processing enabled * Higher fees due to compute overhead compared to basic reads #### \_getCmd(...) **Key difference from basic reads:** Includes `EVMCallComputeV1` configuration * **Read request structure:** Same as basic pattern - specifies target function and parameters * **Compute configuration:** Defines the processing pipeline that will execute after data retrieval * `computeSetting: 2` enables both `lzMap` and `lzReduce` processing * `to: address(this)` specifies this contract contains the compute function implementations #### lzMap(...) **Step 2 of compute pipeline:** Individual response transformation 1. **Decode raw response** * Extract data from target chain function call result 2. **Apply transformation logic** * Convert formats, validate data, apply business rules * Example: increment by 1, but could be unit conversion, filtering, etc. 3. **Re-encode for next step** * Prepare data for `lzReduce` or final delivery to `_lzReceive` #### lzReduce(...) **Step 3 of compute pipeline:** Response aggregation 1. **Process mapped responses** * Receive array of all `lzMap` outputs * Validate each response format and content 2. **Apply aggregation logic** * Combine responses using your business logic * Example: sum all values, but could be averaging, min/max, weighted calculations 3. **Return final result** * Single aggregated value to deliver to `_lzReceive` #### \_lzReceive(...) **Step 4 of compute pipeline:** Final result processing 1. **Receive computed result** * Data has already been through `lzMap` and `lzReduce` processing * Final result is delivered, not raw target chain response 2. **Process final data** * Emit events, update state, trigger additional logic * Result represents the fully processed and aggregated data #### (Optional) quoteReadFee(...) Fee estimation includes compute processing overhead. Costs are higher than basic reads due to: * Additional compute execution processing * Data transformation and aggregation operations * Multiple processing steps in the pipeline **Example usage:** ```solidity wrap theme={null} // Get fee estimate for read with compute MessagingFee memory fee = readContract.quoteReadFee(5, 10, "0x"); // Send with computed processing readContract.readSum{value: fee.nativeFee}(5, 10, "0x"); ``` ### Call Non-View Functions lzRead can also query functions that aren't marked `view` or `pure`, but still return valuable data without modifying state. This pattern leverages `eth_call` to safely execute functions that would normally require gas, enabling access to sophisticated onchain computations. **Core concept:** Many useful functions (especially in DeFi) aren't marked `view` because they rely on calling other non-view functions internally, even though they don't modify state. lzRead uses `eth_call` to execute these functions safely, capturing their return values without gas costs or state changes. **Use cases:** * **DEX price quotations**: Uniswap V3's `quoteExactInputSingle` simulates swaps to calculate output amounts * **Lending protocol queries**: Calculate borrow rates, collateral requirements, or liquidation thresholds * **Yield farming calculations**: Determine pending rewards, APR calculations, or harvest amounts * **Options pricing**: Complex mathematical models for derivative pricing * **Arbitrage detection**: Calculate profit opportunities across different protocols * **Liquidation analysis**: Determine if positions are liquidatable and expected returns **Why these functions aren't `view`:** * They call other non-view functions internally (like Uniswap's swap simulation) * They use try-catch blocks or other constructs that prevent `view` designation * They access external contracts that may not be `view`-compatible * They perform complex state reads that the compiler can't verify as non-modifying **Key implementation details:** * Functions must not revert during execution - test parameters thoroughly * Use proper struct encoding for complex parameters (like Uniswap's `QuoteExactInputSingleParams`) * Handle multi-return-value responses with correct ABI decoding * Target functions execute via `eth_call`, so no actual state changes or gas consumption occur * DVNs verify these calls can execute successfully before returning data #### Installation Get started quickly with a pre-built Uniswap V3 quote reader example: ```bash wrap theme={null} LZ_ENABLE_READ_EXAMPLE=1 npx create-lz-oapp@latest --example uniswap-read ``` This creates a complete project with: * Uniswap V3 QuoterV2 integration contracts * Non-view function calling examples * Multi-chain price aggregation patterns * Ready-to-deploy implementations for major chains #### Contract Example ```solidity wrap theme={null} // contracts/UniswapV3QuoteDemo.sol // // ────────────────────────────────────────────────────────────────────────────── // 1b. Read Command Construction // ────────────────────────────────────────────────────────────────────────────── /// @notice Constructs the read command to fetch Uniswap V3 quotes from each configured chain. /// @return cmd Encoded command for crosschain price queries. function getCmd() public view returns (bytes memory cmd) { uint256 count = targetEids.length; EVMCallRequestV1[] memory requests = new EVMCallRequestV1[](count); for (uint256 i = 0; i < count; ++i) { uint32 eid = targetEids[i]; ChainConfig memory cfg = chainConfigs[eid]; bytes memory data = abi.encodeWithSelector( IQuoterV2.quoteExactInputSingle.selector, IQuoterV2.QuoteExactInputSingleParams({ tokenIn: cfg.tokenInAddress, tokenOut: cfg.tokenOutAddress, amountIn: 1 ether, fee: cfg.fee, sqrtPriceLimitX96: 0 }) ); requests[i] = EVMCallRequestV1({ appRequestLabel: uint16(i + 1), targetEid: eid, isBlockNum: false, blockNumOrTimestamp: uint64(block.timestamp), confirmations: cfg.confirmations, to: cfg.quoterAddress, callData: data }); } EVMCallComputeV1 memory compute = EVMCallComputeV1({ computeSetting: 2, targetEid: ILayerZeroEndpointV2(endpoint).eid(), isBlockNum: false, blockNumOrTimestamp: uint64(block.timestamp), confirmations: 15, to: address(this) }); return ReadCodecV1.encode(0, requests, compute); } // ────────────────────────────────────────────────────────────────────────────── // 2. Map & Reduce Logic // ────────────────────────────────────────────────────────────────────────────── /// @notice Maps individual Uniswap quote responses to encoded amounts. /// @param _response Raw response bytes from the quoted call. /// @return Encoded amountOut for a single chain. function lzMap(bytes calldata, bytes calldata _response) external pure returns (bytes memory) { require(_response.length >= 32, "Invalid response length"); (uint256 amountOut,,,) = abi.decode(_response, (uint256, uint160, uint32, uint256)); return abi.encode(amountOut); } /// @notice Reduces multiple mapped responses to a single average value. /// @param _responses Array of encoded amountOut responses from each chain. /// @return Encoded average of all responses. function lzReduce(bytes calldata, bytes[] calldata _responses) external pure returns (bytes memory) { require(_responses.length > 0, "No responses"); uint256 sum; for (uint i = 0; i < _responses.length; i++) { sum += abi.decode(_responses[i], (uint256)); } uint256 avg = sum / _responses.length; return abi.encode(avg); } // ────────────────────────────────────────────────────────────────────────────── // 3. Receive Business Logic // ────────────────────────────────────────────────────────────────────────────── /// @notice Handles the final averaged quote from LayerZero and emits the result. /// @dev _origin LayerZero origin metadata (unused). /// @dev _guid Unique message identifier (unused). /// @param _message Encoded average price bytes. function _lzReceive( Origin calldata /*_origin*/, bytes32 /*_guid*/, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { uint256 averagePrice = abi.decode(_message, (uint256)); emit AggregatedPrice(averagePrice); } ``` **Crosschain Price Aggregation Example:** * Deploy `UniswapV3QuoteDemo` on your source network (configured for Ethereum, Base, and Optimism) * Call `readAverageUniswapPrice("0x")` to query WETH/USDC prices across all three chains simultaneously * The contract's DVNs fetch prices from each chain's Uniswap V3 deployment * `lzMap` extracts the `amountOut` from each chain's complex response * `lzReduce` calculates the average price across all chains * Final averaged price is delivered to `AggregatedPrice(averagePrice)` event This enables sophisticated crosschain price feeds, governance aggregation, and multi-chain protocol monitoring in a single transaction. #### Constructor Pre-configures three major chains with their respective Uniswap V3 deployments using hardcoded constants: * **Ethereum Mainnet**: EID 30101 with WETH/USDC addresses and QuoterV2 contract * **Base Mainnet**: EID 30184 with chain-specific token addresses * **Optimism Mainnet**: EID 30111 with chain-specific token addresses * Sets up LayerZero connectivity and establishes read channel peer relationship * **Key advantage:** Ready-to-deploy with major chains pre-configured #### readAverageUniswapPrice(...) 1. **Build multi-chain command** * `getCmd()` constructs read requests for ALL three configured chains * Each request queries `quoteExactInputSingle` with 1 WETH input amount 2. **Send aggregated request** * Single `_lzSend()` operation handles all three chains simultaneously * More cost-effective than separate requests per chain * Includes compute configuration for price averaging #### getCmd(...) **Multi-chain request construction with compute:** 1. **Iterate through target chains** * Build `EVMCallRequestV1` for Ethereum, Base, and Optimism * Use unique `appRequestLabel` (1, 2, 3) to track responses during compute processing 2. **Chain-specific parameters** * Each request uses that chain's specific QuoterV2, WETH, and USDC addresses * Maintains consistent `amountIn: 1 ether` across all chains for comparable results * Uses chain-specific confirmation requirements (5 blocks each) 3. **Compute configuration** * `computeSetting: 2` enables both `lzMap` and `lzReduce` for response processing * `targetEid` points to source chain for compute execution * `to: address(this)` specifies this contract contains the compute functions #### lzMap(...) - Price Extraction **Individual chain response processing:** ```solidity wrap theme={null} // Uniswap returns: (amountOut, sqrtPriceX96After, initializedTicksCrossed, gasEstimate) (uint256 amountOut,,,) = abi.decode(_response, (uint256, uint160, uint32, uint256)); return abi.encode(amountOut); // Extract only the price data we need ``` **Purpose:** Extract `amountOut` (USDC amount for 1 WETH) from Uniswap's complex 4-value response, normalizing all chains to simple price values. #### lzReduce(...) - Price Averaging **Crosschain aggregation logic:** ```solidity wrap theme={null} uint256 sum; for (uint i = 0; i < _responses.length; i++) { sum += abi.decode(_responses[i], (uint256)); } uint256 avg = sum / _responses.length; // Simple average across 3 chains ``` **Current implementation:** Simple arithmetic mean of all three chain prices. **Potential enhancements:** * **Weighted averaging:** Weight by liquidity, volume, or chain importance * **Outlier filtering:** Remove prices that deviate significantly from median * **Confidence scoring:** Account for different chain finality requirements #### \_lzReceive(...) - Final Price Delivery Receives the final aggregated price representing the crosschain average WETH/USDC price: * **Event emission:** Emits `AggregatedPrice(averagePrice)` with the computed average * **Result format:** Single `uint256` representing average USDC amount for 1 WETH across all three chains **Use cases for the aggregated price:** * **Crosschain arbitrage detection:** Compare with local prices to find opportunities * **Multi-chain price oracles:** Provide robust price feeds aggregating multiple sources * **Risk management:** Monitor price discrepancies across deployments * **Liquidity routing:** Direct users to chains with optimal pricing #### Architecture Benefits **Single Transaction Efficiency:** * One read request handles 3+ chains instead of separate transactions * Reduced gas costs and complexity compared to multiple individual requests **Atomic Consistency:** * All chain data fetched and processed together * No timing discrepancies between separate async requests **Failure Resilience:** * Built-in retry logic across all target chains * Graceful handling of individual chain failures without affecting others **Why These Functions Aren't View:** * **Internal non-view calls:** Uniswap's quoter calls other non-view functions internally during swap simulation * **Try-catch blocks:** Error handling constructs prevent `view` designation even when no state changes occur * **Compiler restrictions:** Complex state reads that the compiler can't verify as non-modifying **DVN Verification:** DVNs use `eth_call` to execute these functions, ensuring: * No actual state changes occur on the target chain * No gas consumption on the target chain * Results are cryptographically verified and delivered to your source chain ### Multi-Chain Aggregation Execute identical or related queries across multiple chains simultaneously and combine the results into a single, meaningful response. This is lzRead's most powerful pattern, enabling true crosschain data synthesis and decision-making. **Core concept:** Instead of making separate read requests to different chains and manually combining results, multi-chain aggregation fetches data from multiple networks in a single lzRead command. The compute layer processes and combines all responses before delivering the final result to your source chain. **Use cases:** * **Crosschain price feeds**: Get token prices from major DEXes on different chains and calculate weighted averages * **Multi-chain governance**: Aggregate voting results across different network deployments of your protocol * **Liquidity analysis**: Compare pool depths, trading volumes, and rates across chains to find optimal routing * **Risk assessment**: Analyze protocol health by checking reserves, utilization rates, and other metrics across deployments * **Arbitrage detection**: Find price discrepancies and calculate potential profits across multiple networks * **Portfolio valuation**: Calculate total holdings by querying balances and prices across user's multi-chain positions * **Protocol synchronization**: Monitor and compare state across different chain deployments **Architecture benefits:** * **Single transaction cost**: One read request handles multiple chains instead of separate transactions * **Atomic aggregation**: All chain data is processed together, ensuring consistency * **Reduced complexity**: No need to manage multiple async requests or coordinate responses * **Gas efficiency**: Compute processing happens off-chain, minimizing source chain gas usage * **Failure handling**: Built-in retry and error handling across all target chains **Key implementation details:** * Array of `EVMCallRequestV1` structs, each targeting different chains/contracts * Unique `appRequestLabel` for each request to track responses during compute processing * `lzMap` processes each chain's response individually (normalization, validation) * `lzReduce` combines all mapped responses into final aggregated result * DVNs must support all target chains specified in your requests #### Contract Example Refer to the same `UniswapV3QuoteDemo` contract under [Non-View Functions](#call-non-view-functions) #### Architecture Benefits **Single Transaction Efficiency:** * One read request handles 3+ chains instead of separate transactions * Reduced gas costs and complexity compared to multiple individual requests **Atomic Consistency:** * All chain data fetched and processed together * No timing discrepancies between separate async requests **Failure Resilience:** * Built-in retry logic across all target chains * Graceful handling of individual chain failures without affecting others ### Hybrid Messaging + Read For applications that need both messaging and read capabilities: ```solidity wrap theme={null} contract HybridApp is OAppRead { uint32 constant READ_CHANNEL_THRESHOLD = 4294965694; function _lzReceive( Origin calldata _origin, bytes32 _guid, bytes calldata _message, address _executor, bytes calldata _extraData ) internal override { if (_origin.srcEid > READ_CHANNEL_THRESHOLD) { // Handle read responses _handleReadResponse(_message); } else { // Handle regular messages _handleMessage(_origin, _message); } } function _handleReadResponse(bytes calldata _message) internal { // Process read response data uint256 price = abi.decode(_message, (uint256)); // Update application state with fetched data } function _handleMessage(Origin calldata _origin, bytes calldata _message) internal { // Process regular crosschain messages string memory data = abi.decode(_message, (string)); // Handle standard messaging logic } } ``` ## Debugging lzRead introduces unique challenges compared to standard LayerZero messaging. This comprehensive debugging guide covers common pitfalls, specific error scenarios, and practical solutions to help you troubleshoot lzRead implementations effectively. #### 1. Incorrect Execution Options Type **❌ Problem:** Using standard messaging options instead of lzRead-specific options causes transaction reverts. **Root Cause:** lzRead requires `addExecutorLzReadOption` with calldata size estimation, not `addExecutorLzReceiveOption`. ```solidity wrap theme={null} // ❌ WRONG - Standard messaging options OptionsBuilder.newOptions().addExecutorLzReceiveOption(100000, 0); // ✅ CORRECT - lzRead options with size estimation OptionsBuilder.newOptions().addExecutorLzReadOption(100000, 64, 0); // gas size value ``` **Solution:** Generate the correct option for use in your `enforcedOptions` or `extraOptions`: ```solidity wrap theme={null} import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; function getReadOptions(uint256 responseSize) internal pure returns (bytes memory) { return OptionsBuilder .newOptions() .addExecutorLzReadOption( 200000, // Gas limit for response processing responseSize, // Expected response data size in bytes 0 // Native value (usually 0 for reads) ); } ``` #### 2. Target Function Reverts (DVN Fulfillment Failure) **❌ Problem:** When the target function reverts during execution, DVNs cannot fulfill the request, causing the entire read operation to fail. **Root Cause:** DVNs use `eth_call` to execute target functions. If the function reverts with the provided parameters, verification cannot complete. **Common Revert Scenarios:** * Invalid parameters passed to target function * Target contract state changes between request and execution * Insufficient target chain block confirmations * Target function has built-in parameter validation that fails ```solidity wrap theme={null} // ❌ PROBLEMATIC - Function may revert with certain parameters function riskyRead() external payable { bytes memory callData = abi.encodeWithSelector( IToken.balanceOf.selector, address(0) // Zero address may cause revert in some implementations ); // ... rest of read logic } // ✅ SAFE - Validate parameters and use defensive programming function safeRead(address tokenHolder) external payable { require(tokenHolder != address(0), "Invalid holder address"); require(tokenHolder.code.length > 0, "Not a contract"); // If targeting contracts bytes memory callData = abi.encodeWithSelector( IToken.balanceOf.selector, tokenHolder ); // ... rest of read logic } ``` **Debug Strategy:** ```solidity wrap theme={null} // Test target function locally before using in lzRead function testTargetFunction(address target, bytes memory callData) external view returns (bool success, bytes memory result) { (success, result) = target.staticcall(callData); if (!success) { // Log the revert reason for debugging if (result.length > 0) { assembly { let returndata_size := mload(result) revert(add(32, result), returndata_size) } } } } ``` **Prevention Checklist:** * ✅ Test target function calls with your exact parameters on target chain * ✅ Ensure target contract exists at the specified address * ✅ Verify function selector matches target contract interface * ✅ Check that target function doesn't have restrictive access controls * ✅ Test with realistic parameter ranges and edge cases #### 3. Nonce Ordering Issues (Sequential Verification Failure) **❌ Problem:** If the first nonce in a sequence fails, all subsequent nonces are blocked because LayerZero verification is ordered. **Root Cause:** LayerZero processes nonces sequentially. A failed or stuck nonce prevents processing of later nonces until resolved. ```solidity wrap theme={null} // ❌ PROBLEMATIC - Multiple rapid requests without nonce management function multipleReads() external payable { // These requests will be processed sequentially by nonce readData(target1, eid1); // Nonce N readData(target2, eid2); // Nonce N+1 - blocked if N fails readData(target3, eid3); // Nonce N+2 - blocked if N or N+1 fails } ``` **Error Indicators:** * Later transactions succeed but never receive responses * LayerZero scan shows "Verified" but not "Delivered" for subsequent messages * Nonce gaps in your application's message history **Recovery Strategy:** 1. **Identify the failed nonce** causing the blockage using nonce status checks 2. **Use `endpoint.skip()`** to bypass the failed nonce and unblock subsequent processing 3. **Ensure subsequent requests** are properly formatted and verifiable before sending new reads 4. **Implement prevention** by validating all parameters before sending future requests See the [Endpoint - Skip](../troubleshooting/debugging-messages#skipping-nonce) section to see how to skip a nonce. #### 4. Calldata Size Estimation Errors **❌ Problem:** Underestimating response size in `addExecutorLzReadOption` causes executor delivery failures. **Root Cause:** Executors pre-allocate gas based on your size estimate. If the actual response exceeds this size, automatic delivery fails. ```solidity wrap theme={null} // ❌ UNDERESTIMATED - Will fail if response is larger than 32 bytes OptionsBuilder.newOptions().addExecutorLzReadOption(100000, 32, 0); // But target function returns: (uint256, address, string) ≈ 100+ bytes function complexTargetFunction() external view returns (uint256, address, string memory); ``` **Size Calculation Guide:** ```solidity wrap theme={null} contract SizeEstimator { // Calculate response sizes for common types function estimateResponseSize(bytes memory sampleResponse) external pure returns (uint256) { return sampleResponse.length; } // Common type sizes (for reference): // uint256: 32 bytes // address: 32 bytes (padded) // bool: 32 bytes (padded) // bytes32: 32 bytes // string: 32 + length + padding // dynamic array: 32 + (element_size * length) // tuple: sum of all element sizes } // ✅ PROPER SIZE ESTIMATION function getOptionsWithCorrectSize(uint256 expectedStringLength) internal pure returns (bytes memory) { uint256 estimatedSize = 32 + // uint256 32 + // address 32 + // string length expectedStringLength + // string content 32; // padding buffer return OptionsBuilder .newOptions() .addExecutorLzReadOption(200000, estimatedSize, 0); } ``` #### 5. Block Number vs Timestamp on L2 Chains **❌ Problem:** Using `block.number` on L2 chains often references L1 block numbers, causing timing and finality issues. **Root Cause:** Many L2s inherit block numbers from their L1 parent chain, making `block.number` unsuitable for timing-sensitive operations. **Affected Chains:** * **Arbitrum**: `block.number` returns L1 block number, not L2 sequence numbers * **Optimism**: Similar L1 block number inheritance in some contexts ```solidity wrap theme={null} // ❌ PROBLEMATIC on L2s - May reference L1 blocks EVMCallRequestV1({ // ... isBlockNum: true, blockNumOrTimestamp: uint64(block.number), // This is L1 block number on Arbitrum! // ... }); // ✅ RECOMMENDED - Use timestamps for universal compatibility EVMCallRequestV1({ // ... isBlockNum: false, blockNumOrTimestamp: uint64(block.timestamp), // Works consistently across all chains // ... }); ``` ## Further Reading * [Read Standard Overview](../../../concepts/applications/read-standard) - Conceptual information * [Read Paths & DVNs](../../../deployments/read-contracts) - Available chains and DVNs * [Execution Options](../configuration/options) - Options configuration # Architecture Source: https://docs.layerzero.network/v2/developers/evm/multi-asset-oft/architecture Nexus OApp, NexusOFT wrappers, OFT registry, message encoding, and modular security. Multi-Asset OFT is built around the Nexus contract — a single upgradeable OApp that owns all cross-chain messaging for multiple registered tokens. Each token gets a thin `NexusOFT` wrapper that exposes the standard `IOFT` interface, while the `Nexus` contract handles burn/mint, fee collection, and module delegation. ## System Overview ``` +--------------------------------------------------------------+ | Pluggable Modules | | NexusFeeConfigModule | NexusPauseModule | NexusRateLimiter | | (independently upgradeable, admin-swappable) | +--------------------------------------------------------------+ | Nexus Hub | | OApp + OFT Registry + FeeHandler + CreditRedirect | | (burn/mint, _lzSend/_lzReceive, module delegation) | +===========================+===========================+======+ | NexusOFT (per token) | NexusOFT (per token) | ... | | IOFT -> delegates to hub | IOFT -> delegates to hub | | +===========================+===========================+======+ | NexusERC20 + NexusERC20Guard | | (shared allowlist + pause on transfer/mint/burn) | +--------------------------------------------------------------+ ``` ## Core Contracts ### Nexus (Hub) The Nexus contract is the only contract that interacts with the LayerZero endpoint. It handles OApp messaging, peer/delegate management, enforced options, fee deposit, credit redirect, messaging channel ops, and the OFT registry. It stores three mutable module pointers: ```solidity theme={null} INexusPause pauseModule; INexusFeeConfig feeConfigModule; INexusRateLimiter rateLimiterModule; ``` If a module is not set (`address(0)`), its extension is inactive — pause returns `false`, fee returns `0`, rate limiter capacity returns `type(uint256).max`. ### NexusOFT (Per-Token) A stateless contract that implements `IOFT` by forwarding all calls to Nexus: * `quoteOFT()` → `Nexus.nexusQuoteOFT()` * `quoteSend()` → `Nexus.nexusQuoteSend()` * `send()` → `Nexus.nexusSend()` On the receive path, the Nexus contract calls `NexusOFT.nexusReceive()` to emit events and forward compose messages. ### NexusERC20 + NexusERC20Guard See [NexusERC20](/v2/developers/evm/multi-asset-oft/nexus-erc20) for the token layer. ## OFT Registry Tokens are registered in the `Nexus` contract with a unique `uint32 tokenId` that maps to: * An `oftAddress` (the `NexusOFT` wrapper) * A `burnerMinterAddress` (the `NexusERC20` or a wrapper contract) Registration validates that all tokens share the same local and shared decimals, the `NexusOFT.tokenId()` matches the registered `tokenId`, and no duplicate registrations exist. ## Nexus ID and Message Encoding Cross-chain messages are encoded with a **4-byte `tokenId` prefix** via `NexusMsgCodec`, allowing Nexus to route messages for multiple tokens over a single OApp channel: ``` | tokenId (4 bytes) | sendTo (32 bytes) | amountSD (8 bytes) | [composeFrom + composeMsg] | ``` Modules identify pathways using a **composite Nexus ID**: ``` nexusId = (uint256(tokenId) << 32) | uint256(eid) ``` This encodes both the token and destination chain into a single `uint256`, enabling the 4-level priority resolution used by fee and pause modules. The rate limiter uses only the EID portion (per-destination). ## Module Delegation On an outbound `nexusSend()`, Nexus delegates to modules in this order: ``` 1. Pause → _isPaused(nexusId) — reverts if paused 2. Fee → _debitView() calculates fee via feeConfigModule 3. Rate Limiter → _outflow(nexusId, from, amountReceivedLD) 4. Burn → burns amountSentLD from sender, mints fee to feeDeposit 5. _lzSend → sends encoded message to LayerZero endpoint ``` On an inbound `_lzReceive()`: ``` 1. Rate Limiter → _inflow(nexusId, to, amountLD) 2. Credit Redirect → _redirectCredit() (escrow if configured and recipient not allowlisted) 3. Mint → mints amountLD to recipient (or escrow) 4. Compose → forwards compose message to NexusOFT if present ``` ## Alt Variants For chains where gas fees are paid via an ERC20 token (using `EndpointV2Alt`): * `NexusAlt` — Hub variant that overrides `_payNative()` to no-op (native fee pushed by `NexusOFTAlt`) * `NexusOFTAlt` — Wrapper that pushes both native ERC20 and LZ token fees to the endpoint before calling Nexus ## Upgradeability All upgradeable contracts use EIP-7201 namespaced storage. `NexusOFT` is **not upgradeable** — it is a thin stateless wrapper with only immutable state. The Nexus contract and each module can be upgraded independently through their respective proxies. Module addresses are mutable, so a new module deployment can be swapped in by `DEFAULT_ADMIN_ROLE` without upgrading the Nexus proxy. ## Next Steps * [Modules](/v2/developers/evm/multi-asset-oft/modules) for fee, pause, and rate limiter configuration * [NexusERC20](/v2/developers/evm/multi-asset-oft/nexus-erc20) for the token and guard layer * [RBAC Reference](/v2/developers/evm/multi-asset-oft/rbac-reference) for the complete role mapping # Modules Source: https://docs.layerzero.network/v2/developers/evm/multi-asset-oft/modules Pluggable fee, pause, and rate limiter modules for Multi-Asset OFT cross-chain operations. Multi-Asset OFT delegates security policies to independently upgradeable module contracts. Each module is set on Nexus via admin-only setters (`setPauseModule`, `setFeeConfigModule`, `setRateLimiterModule`). If a module is not set, its extension is inactive. The fee and pause modules share the same **4-level priority resolution** over composite Nexus IDs, giving operators fine-grained control per token, per destination, or globally. The rate limiter operates **per destination** (EID) only — all tokens on the same destination share the same bucket. ## Priority Resolution Nexus IDs encode both the token and destination: ``` nexusId = (uint256(tokenId) << 32) | uint256(eid) ``` Each module resolves configs by checking four key levels in order, from least specific to most specific: | Level | Key | Scope | | ---------------- | ------------------------------ | ------------------------------------- | | Global | `0` | All tokens, all destinations | | Destination-only | `nexusId & 0xFFFFFFFF` | All tokens for a specific destination | | Token-only | `nexusId & 0xFFFFFFFF00000000` | All destinations for a specific token | | Composite | `nexusId` | Specific token + destination pair | The config with the **highest priority wins**. On equal priority, the least specific key (checked first) wins. A config with `MAX_PRIORITY` (`type(uint128).max`) short-circuits resolution immediately. This means you can set a global default and override it for specific tokens or destinations without touching the global config. ## Fee Config Module Same BPS-based fee calculation as [Stablecoin OFT's fee extension](/v2/developers/evm/stablecoin-oft/extensions#fee-module), but with priority resolution. Fees are collected by burning `amountSentLD` from the sender and minting the fee portion to the configured `feeDeposit` address. ### Configuration Each config entry stores: | Field | Type | Description | | ---------- | --------- | --------------------------------- | | `priority` | `uint128` | Resolution priority (higher wins) | | `feeBps` | `uint16` | Fee in basis points (0–10000) | ```solidity theme={null} function setFeeBps(SetFeeBpsParam[] calldata _params) public; ``` Setting `priority = 0` and `feeBps = 0` removes the config entry. ### Role `FEE_CONFIG_MANAGER_ROLE` (authenticated via Nexus access control). ## Pause Module Controls whether outbound transfers are allowed for a given pathway. Nexus checks `_isPaused(nexusId)` before every send — if paused, the transaction reverts. ### Configuration Each config entry stores: | Field | Type | Description | | ---------- | --------- | --------------------------------- | | `priority` | `uint128` | Resolution priority (higher wins) | | `paused` | `bool` | Whether the pathway is paused | ```solidity theme={null} function setPaused(SetPausedParam[] calldata _params) public; ``` ### Role Logic The pause module enforces a nuanced role check per batch based on the **effective impact** of each config change: * `PAUSER_ROLE` — required when the net effect is pausing or strengthening a pause config * `UNPAUSER_ROLE` — required when the net effect is unpausing, weakening a pause config, or a no-op (to prevent unauthorized no-ops) * **Mixed batch** (some entries pause, some unpause) → requires both roles This ensures a compromised pauser key cannot also undo a legitimate security pause. ### Pause vs NexusERC20Guard Pause | Concern | Controlled By | Scope | | ----------------- | ----------------- | ------------------------------------------------- | | Cross-chain sends | NexusPauseModule | Per (token, destination) with priority resolution | | Local transfers | `NexusERC20Guard` | Per token (`uint160(tokenAddress)`) | Pausing a cross-chain pathway does not block local transfers. Pausing a token via the guard blocks both local and cross-chain transfers for that token. ## Rate Limiter Module Enforces token bucket rate limits on inbound and outbound transfers, shared across all tokens per destination EID. ### How It Works Rate limits are tracked **per EID** (not per Nexus ID). All tokens on the same destination share a single bucket. Token amounts are converted to a **common unit** via per-token scales before consumption, allowing heterogeneous tokens to share a meaningful limit. ### Token Scales Because all tokens on a destination share one rate limit bucket, raw token amounts aren't directly comparable — transferring 1,000 of a stablecoin is not the same as transferring 1,000 of a governance token. Token scales assign a relative price to each token so that higher-value tokens consume proportionally more of the shared limit. | Field | Type | Description | | --------- | --------- | -------------------------------------------------- | | `scale` | `uint256` | Fixed-point price multiplier (denominator: `1e18`) | | `enabled` | `bool` | Whether scaling is active for this token | A scale of `1e18` means the token's amount counts at face value (1:1). A scale of `2e18` means each token unit consumes twice as much of the bucket. Setting `enabled = true` with `scale = 0` effectively prices the token at zero, exempting it from rate limit consumption. ```solidity theme={null} function setScales(SetScaleParam[] calldata _params) public; ``` ### Configuration Same as [Stablecoin OFT's rate limiter](/v2/developers/evm/stablecoin-oft/extensions#rate-limiter-module) — token bucket with linear decay, configurable capacity and refill rate, address exemptions, net/gross accounting. The key difference is that limits operate on **scaled** amounts rather than raw token amounts. ### Role `RATE_LIMITER_MANAGER_ROLE` (authenticated via Nexus access control). Controls rate limit configs, states, address exemptions, checkpoints, and token scales. ## Module Execution Order On an outbound `nexusSend()`: ``` 1. Pause → _isPaused(nexusId) reverts if paused 2. Fee → _debitView() calculates fee, reduces amountReceivedLD 3. Rate Limiter → _outflow(nexusId) checks and updates outbound bucket 4. Token Transfer → burn amountSentLD, mint fee to feeDeposit ``` On an inbound `_lzReceive()`: ``` 1. Rate Limiter → _inflow(nexusId) checks and updates inbound bucket 2. Token Transfer → mint amountLD to recipient ``` ## Next Steps * [Architecture](/v2/developers/evm/multi-asset-oft/architecture) for system design and message flow * [RBAC Reference](/v2/developers/evm/multi-asset-oft/rbac-reference) for the complete role-to-function mapping * [Security and Compliance](/v2/developers/evm/multi-asset-oft/security-compliance) for the threat model # NexusERC20 Source: https://docs.layerzero.network/v2/developers/evm/multi-asset-oft/nexus-erc20 Upgradeable ERC20 token with guard-delegated allowlist and pause, permit, and fund recovery for Multi-Asset OFT. `NexusERC20` is an upgradeable ERC20 token designed for Multi-Asset OFT. Unlike [`ERC20Plus`](/v2/developers/evm/stablecoin-oft/erc20plus) which embeds allowlist and pause logic directly, `NexusERC20` delegates these checks to a shared `NexusERC20Guard` contract — one guard serving multiple tokens. ## Comparison with ERC20Plus | Feature | `ERC20Plus` | `NexusERC20` | | ---------------------- | --------------- | ------------------------------------------------------------ | | Allowlist (3-mode) | Inline | Delegated to `NexusERC20Guard` | | Pause | Global (inline) | Per-token via guard (`PauseByID`, keyed by `uint160(token)`) | | Fund recovery | Yes | Yes | | ERC20Permit (EIP-2612) | Yes | Yes | | RBAC mint/burn | Yes | Yes | | Guard contract | N/A | Shared across tokens | | Upgradeable | Yes (EIP-7201) | Yes (EIP-7201) | ## NexusERC20 ### Roles ```solidity theme={null} bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); ``` When used with Multi-Asset OFT, the **burner-minter address** (which can be the `NexusERC20` itself or a separate contract) must be granted both roles. Nexus calls mint/burn via configurable function selectors. ### Guard Checks Every `transfer`, `transferFrom`, `mint`, and `burn` call goes through the guard: ```solidity theme={null} guard.checkTransfer(address(this), msg.sender, _from, _to, _amount); ``` The guard enforces: 1. **Pause** — `_assertNotPaused(uint160(_token))`, keyed by the token's address 2. **Allowlist** — `_assertAllowlisted()` for caller, sender, and recipient (non-zero addresses) On the cross-chain receive path, Nexus can [redirect credits](/v2/developers/evm/multi-asset-oft/security-compliance#credit-redirect-for-non-allowlisted-recipients) to escrow when the intended recipient is not allowlisted. ### Fund Recovery Same as `ERC20Plus` — addresses holding `DEFAULT_ADMIN_ROLE` can transfer tokens from non-allowlisted addresses: ```solidity theme={null} function recoverFunds(address _from, address _to, uint256 _amount) public; ``` Reverts with `CannotRecoverFromAllowlisted` if `_from` is allowlisted. ### ERC20Permit (EIP-2612) Built-in gasless approvals, identical to `ERC20Plus`. ## NexusERC20Guard A single upgradeable guard shared by all `NexusERC20` tokens on the same chain. ### What It Does The guard combines two concerns: * **AllowlistRBACUpgradeable** — Three-mode allowlist (Open / Blacklist / Whitelist) with the same behavior as [Stablecoin OFT's allowlist](/v2/developers/evm/stablecoin-oft/extensions#allowlist) * **PauseByIDRBACUpgradeable** — Per-token pause using `uint160(tokenAddress)` as the pause ID ### Initialization ```solidity theme={null} function initialize(address _initialAdmin) public initializer; ``` Initializes with `AllowlistMode.Open` (no transfer restrictions). ### Roles | Role | Source | Used For | | -------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------ | | `DEFAULT_ADMIN_ROLE` | `AccessControl2StepUpgradeable` | Admin transfer, set allowlist mode | | `BLACKLISTER_ROLE` | `AllowlistRBACUpgradeable` | Blacklist addresses | | `WHITELISTER_ROLE` | `AllowlistRBACUpgradeable` | Whitelist addresses | | `PAUSER_ROLE` | `PauseByIDRBACUpgradeable` | `setDefaultPaused` (when pausing), `setPaused` (when effectively pausing by token address as ID) | | `UNPAUSER_ROLE` | `PauseByIDRBACUpgradeable` | `setDefaultPaused` (when unpausing), `setPaused` (when effectively unpausing or no-op) | ### Allowlist Modes | Mode | Behavior | | ------------- | ----------------------------- | | **Open** | No restrictions | | **Blacklist** | Block specific addresses | | **Whitelist** | Allow only specific addresses | Mode transitions do not clear existing lists. Both lists support paginated enumeration via `getBlacklist(offset, limit)` and `getWhitelist(offset, limit)`. ## Next Steps * [Architecture](/v2/developers/evm/multi-asset-oft/architecture) for how `NexusERC20` fits into Multi-Asset OFT * [Modules](/v2/developers/evm/multi-asset-oft/modules) for cross-chain fee, pause, and rate limiting * [RBAC Reference](/v2/developers/evm/multi-asset-oft/rbac-reference) for the complete role mapping # Multi-Asset OFT Overview Source: https://docs.layerzero.network/v2/developers/evm/multi-asset-oft/overview A multi-token OFT framework built on a single OApp with composable security modules. Multi-Asset OFT is a multi-token cross-chain hub built on LayerZero V2. Instead of deploying a separate OFT contract per token, Multi-Asset OFT registers many tokens under a single upgradeable OApp that owns all cross-chain messaging. Each token gets a thin `NexusOFT` wrapper that exposes the standard `IOFT` interface, so integrators and UIs interact with it the same way they would with any OFT. The core contracts are named `Nexus`, `NexusOFT`, `NexusERC20`, and `NexusERC20Guard` in the codebase. Throughout these docs, "Nexus" refers to the on-chain contract, while "Multi-Asset OFT" refers to the product. ## How It Differs from Stablecoin OFT [Stablecoin OFT](/v2/developers/evm/stablecoin-oft/overview) deploys one full OFT contract per token with security modules (fee, pause, rate limiter) embedded in the contract's inheritance tree. Multi-Asset OFT centralizes messaging in a hub and makes security modules independently deployable and swappable. | Aspect | Stablecoin OFT | Multi-Asset OFT | | --------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------- | | Deployment model | One OFT contract per token | One hub + one `NexusOFT` per token | | Cross-chain messaging | Inside each OFT | Centralized in `Nexus` OApp | | Multi-token per chain | Multiple full OFT deployments | Single hub, multiple registered tokens | | Security modules | Embedded in OFT inheritance | External, swappable contracts | | Fee/pause granularity | Per-destination EID | 4-level priority: global, destination, token, composite (fee and pause); per-destination (rate limiter) | | Rate limiting | Per-destination, single token | Per-destination, shared across tokens with scaling | | Token standard | `ERC20Plus` (inline allowlist + pause) | `NexusERC20` + shared `NexusERC20Guard` | ## What It Does Multi-Asset OFT gives token issuers the same enterprise controls as Stablecoin OFT — who can transfer, where tokens can move, how much can move, and what it costs — but for **multiple tokens through a single hub**: * **Token registry** — Register and deregister tokens dynamically via `TOKEN_REGISTRAR_ROLE` * **Per-destination + per-token policies** — Fee and pause modules resolve configs across four priority levels (global, destination-only, token-only, and composite). Rate limiting is per-destination, shared across tokens with scaling. * **Shared guard** — A single `NexusERC20Guard` enforces allowlist and pause on transfer, mint, and burn for all `NexusERC20` tokens * **Credit redirect** — Escrow for non-allowlisted inbound credits on the hub * **Messaging channel RBAC** — `clear` / `skip` / `burn` / `nilify` via dedicated roles * **Standard IOFT interface** — Each `NexusOFT` looks like a standard OFT to UIs, bridges, and aggregators * **Pluggable modules** — Swap fee, pause, or rate limiter modules without redeploying the hub ## Supported Transfer Model Multi-Asset OFT uses **burn/mint** exclusively. It burns tokens on the source chain and mints on the destination chain using configurable function selectors (immutable per deployment): * Default mint: `mint(address,uint256)` — selector `0x40c10f19` * Default burn: `burn(address,uint256)` — selector `0x9dc29fac` Any `(address,uint256)` signature is supported (e.g., `issue(address,uint256)`, `redeem(address,uint256)`). **Fee-on-transfer and rebasing tokens are not supported.** The OFT debit/credit accounting assumes lossless ERC20 transfers. ## Security Posture All contracts use EIP-7201 namespaced storage and OpenZeppelin's audited upgradeable libraries. Multi-Asset OFT contracts are [independently audited](/v2/resources/audits). Roles are managed through `AccessControl2StepUpgradeable` with two-step admin transfer. Module addresses are mutable (admin-only) so security modules can be upgraded independently. Fee settlement is push-based — fees are minted to a configured `feeDeposit` address during sends, with no withdrawal function. ## Next Steps * **Technical leaders:** Continue to [Architecture](/v2/developers/evm/multi-asset-oft/architecture) for system design and message flow * **Security teams:** See [Security and Compliance](/v2/developers/evm/multi-asset-oft/security-compliance) for the threat model * **Integration engineers:** See [Modules](/v2/developers/evm/multi-asset-oft/modules) for fee, pause, and rate limiting, and [`NexusERC20`](/v2/developers/evm/multi-asset-oft/nexus-erc20) for the token layer # RBAC Reference Source: https://docs.layerzero.network/v2/developers/evm/multi-asset-oft/rbac-reference Complete role definitions and function mappings across Nexus, modules, NexusERC20, and NexusERC20Guard. Multi-Asset OFT distributes roles across four contract boundaries: the Nexus OApp, the external modules, the token (`NexusERC20`), and the guard (`NexusERC20Guard`). All role management uses `AccessControl2StepUpgradeable` with two-step admin transfer. ## Nexus | Role | Source | Used For | | -------------------------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DEFAULT_ADMIN_ROLE` | `AccessControl2StepUpgradeable` | `setPeer`, `setEnforcedOptions`, `setMsgInspector`, `setFeeDeposit`, `setPauseModule`, `setFeeConfigModule`, `setRateLimiterModule`, `setCreditRedirectConfig`, delegate operations | | `TOKEN_REGISTRAR_ROLE` | `OFTRegistryRBACUpgradeable` | `registerToken`, `deregisterToken` | | `MESSAGING_CHANNEL_MANAGER_ROLE` | `OAppMessagingChannelRBACUpgradeable` | `clear`, `skip`, `burn` (non-reversible channel ops) | | `MESSAGE_NILIFIER_ROLE` | `OAppMessagingChannelRBACUpgradeable` | `nilify` (reversible; can be re-verified) | Module roles (`FEE_CONFIG_MANAGER_ROLE`, `PAUSER_ROLE`, `UNPAUSER_ROLE`, `RATE_LIMITER_MANAGER_ROLE`) are also granted on Nexus but consumed by the modules via `onlyNexusRole` / `_checkRole`. `DEFAULT_ADMIN_ROLE` is synchronized with `delegate`. The `setDelegate` function always reverts. ## NexusOFT **No roles.** Access is restricted by the `onlyNexus` modifier — only the Nexus contract can call `nexusReceive`. ## Nexus Fee Config Module | Role | Source | Used For | | ------------------------- | ------------------------------------------------------ | ----------- | | `FEE_CONFIG_MANAGER_ROLE` | Declared locally, checked on Nexus via `onlyNexusRole` | `setFeeBps` | ## Nexus Pause Module | Role | Source | Used For | | --------------- | --------------------------------------------------- | ---------------------------------------------------------- | | `PAUSER_ROLE` | Declared locally, checked on Nexus via `_checkRole` | `setPaused` (when pausing or strengthening a pause config) | | `UNPAUSER_ROLE` | Declared locally, checked on Nexus via `_checkRole` | `setPaused` (when unpausing, weakening, or no-op) | ## Nexus Rate Limiter Module | Role | Source | Used For | | --------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | | `RATE_LIMITER_MANAGER_ROLE` | Declared locally, checked on Nexus via `onlyNexusRole` | `setRateLimitGlobalConfig`, `setRateLimitConfigs`, `setRateLimitStates`, `setRateLimitAddressExemptions`, `checkpointRateLimits`, `setScales` | ## NexusERC20 | Role | Source | Used For | | -------------------- | ------------------------------- | -------------------------- | | `DEFAULT_ADMIN_ROLE` | `AccessControl2StepUpgradeable` | `setGuard`, `recoverFunds` | | `MINTER_ROLE` | Declared locally | `mint` | | `BURNER_ROLE` | Declared locally | `burn` | ## NexusERC20Guard | Role | Source | Used For | | -------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------ | | `DEFAULT_ADMIN_ROLE` | `AccessControl2StepUpgradeable` | Admin transfer, set allowlist mode | | `BLACKLISTER_ROLE` | `AllowlistRBACUpgradeable` | Blacklist addresses | | `WHITELISTER_ROLE` | `AllowlistRBACUpgradeable` | Whitelist addresses | | `PAUSER_ROLE` | `PauseByIDRBACUpgradeable` | `setDefaultPaused` (when pausing), `setPaused` (when effectively pausing by token address as ID) | | `UNPAUSER_ROLE` | `PauseByIDRBACUpgradeable` | `setDefaultPaused` (when unpausing), `setPaused` (when effectively unpausing or no-op) | ## Role Separation Principles * **Module swapping vs module configuration** — Only `DEFAULT_ADMIN_ROLE` can change which module contract is active. Module-specific roles (`FEE_CONFIG_MANAGER_ROLE`, `PAUSER_ROLE`, etc.) can only configure the current module. * **Pause / Unpause** — Split across two roles. A compromised pauser key can halt transfers (disruptive, but funds remain safe) but cannot re-enable them. * **Token registration** — Separate from admin. `TOKEN_REGISTRAR_ROLE` cannot change module addresses or grant other roles. * **Nexus roles vs guard roles** — Module roles live on the Nexus contract. Guard roles (allowlist, token-level pause) live on the guard contract. These are independent access control hierarchies. * **Fee configuration vs fee collection** — `FEE_CONFIG_MANAGER_ROLE` sets BPS rates. Fees are pushed to `feeDeposit` automatically — there is no withdrawal function. * **Messaging channel manager / Message nilifier** — `clear`, `skip`, and `burn` are permanent; `nilify` can be reversed by re-verification. Lets operational wallets unblock or ignore messages without sharing the admin delegate. ## Next Steps * [NexusERC20](/v2/developers/evm/multi-asset-oft/nexus-erc20) for token-level behavior and guard details * [Modules](/v2/developers/evm/multi-asset-oft/modules) for fee, pause, and rate limiter configuration * [Security and Compliance](/v2/developers/evm/multi-asset-oft/security-compliance) for operational security recommendations # Security and Compliance Source: https://docs.layerzero.network/v2/developers/evm/multi-asset-oft/security-compliance Threat model, operational security, and compliance controls for Multi-Asset OFT multi-token hub deployments. Multi-Asset OFT shares the same security foundations as [Stablecoin OFT](/v2/developers/evm/stablecoin-oft/security-compliance) — OpenZeppelin audited upgradeable libraries, EIP-7201 namespaced storage, two-step admin transfer, and push-based fee settlement. Multi-Asset OFT contracts are [independently audited](/v2/resources/audits). This page focuses on threats and mitigations **specific to the Multi-Asset OFT architecture**. ## Multi-Asset OFT Threat Model | Threat | Impact | Mitigation | | --------------------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Module swap to malicious contract** | Attacker-controlled module could disable pause, zero out fees, or remove rate limits | Only `DEFAULT_ADMIN_ROLE` can call `setPauseModule` / `setFeeConfigModule` / `setRateLimiterModule`. Use governance multisig for admin. | | **Shared rate limit exhaustion** | One high-volume token could exhaust the rate limit bucket for all tokens on a destination | Token scales convert amounts to a common unit. Set scales appropriately. Monitor bucket utilization per EID. | | **Token registration of malicious OFT** | Rogue OFT could execute mint calls for its token in different chains | Token registration is the Multi-Asset OFT equivalent of peer setting in OFTs — a critical trust boundary. `TOKEN_REGISTRAR_ROLE` should be held by a trusted operator, not a hot wallet. | | **Deregistered token with inflight messages** | Inbound messages for a deregistered token will revert, blocking the channel | Pause outbound transfers for the token via the NexusPauseModule, wait until all inflight messages have been delivered and processed, then deregister. | | **Blocked inbound nonce** | Receives on that path pause until the nonce is handled (affects all tokens on the hub path) | Assign messaging channel roles to ops wallets so they can `skip` / `nilify` / `clear` / `burn` without the admin delegate. | ## Shared Threat Model The following threats and mitigations are shared with Stablecoin OFT: * **Pauser / Unpauser key compromise** — Same split-role mitigation. See [Stablecoin OFT Security](/v2/developers/evm/stablecoin-oft/security-compliance). * **Fee deposit address compromise** — Same push-based model. Attacker controlling `feeDeposit` can only receive fees, not extract principal. * **Supply inflation via misconfigured deployment** — Ensure each `NexusERC20` grants `MINTER_ROLE`/`BURNER_ROLE` only to the intended burner-minter address. * **Fund recovery abuse** — Same `recoverFunds` restriction: only from non-allowlisted addresses. * **Non-atomic proxy deployment** — Same risk. Deploy proxy and call `initialize` atomically. ## Compliance Controls ### Allowlist (via NexusERC20Guard) The shared guard enforces allowlist and pause checks on `transfer`, `transferFrom`, `mint`, and `burn` for all registered `NexusERC20` tokens. Mode switches (Open → Blacklist → Whitelist) are instant and do not clear existing lists. ### Credit Redirect for Non-Allowlisted Recipients `setCreditRedirectConfig` on Nexus redirects inbound credits for non-allowlisted recipients to an escrow address and emits `CreditRedirected`. When redirected, the credited amount reported to `NexusOFT` / compose is `0`. When redirect is disabled, those credits follow normal guard allowlist and pause rules on mint. Messaging channel roles (`nilify` / `skip`) can clear paths that fail or should be ignored. ### Per-Token Pause (via NexusERC20Guard) Each `NexusERC20` can be paused independently using `uint160(tokenAddress)` as the pause ID. This allows freezing a specific token's local transfers without affecting other tokens. ### Per-Pathway Pause (via NexusPauseModule) Cross-chain sends can be paused at four levels: globally, per destination, per token, or per (token, destination) pair. Priority resolution determines the effective state. ### Fund Recovery Same mechanism as Stablecoin OFT — admin can transfer tokens from non-allowlisted addresses for compliance seizures. ## Monitoring Events to monitor across the Multi-Asset OFT deployment: | Event | Source | Indicates | | ---------------------------------------------------------------- | ------------------- | ------------------------------------------------- | | `RoleGranted` / `RoleRevoked` | All contracts | Permission changes | | `DefaultAdminTransferScheduled` | All with 2-step | Admin transfer initiated | | `PauseModuleSet` / `FeeConfigModuleSet` / `RateLimiterModuleSet` | Nexus | Module swap (high severity) | | `TokenRegistered` / `TokenDeregistered` | Nexus | Token registry changes | | `FeeConfigSet` | Fee Module | Fee rate changes | | `PauseConfigSet` | Pause Module | Pathway pause state changes | | `RateLimitConfigSet` / `RateLimitStateSet` | Rate Limiter Module | Rate limit config changes | | `OFTSent` / `OFTReceived` | `NexusOFT` | Cross-chain transfers (alert on large amounts) | | `GuardSet` | `NexusERC20` | Guard contract swap (high severity) | | `AllowlistModeChanged` | Guard | Allowlist mode transitions | | `BlacklistUpdated` / `WhitelistUpdated` | Guard | Address list changes | | `CreditRedirectConfigSet` / `CreditRedirected` | Nexus | Escrow redirect config or inbound credit redirect | ## Next Steps * [RBAC Reference](/v2/developers/evm/multi-asset-oft/rbac-reference) for the complete role-to-function matrix * [Architecture](/v2/developers/evm/multi-asset-oft/architecture) for the system design overview # LayerZero V2 OApp Quickstart Source: https://docs.layerzero.network/v2/developers/evm/oapp/overview Overview of OApp Quickstart on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure crosschain messaging. The **OApp standard** lets your contract send and receive arbitrary *messages* across chains. With OApp, you can update onchain state on one network and trigger custom business logic on another. Diagram showing crosschain messaging between Network A and Network B, with an arrow indicating the message flow via LayerZero Send and Receive Diagram showing crosschain messaging between Network A and Network B, with an arrow indicating the message flow via LayerZero Send and Receive `OApp.sol` implements the core interface for calling LayerZero's Endpoint V2 on EVM chains. It also provides hookable `_lzSend` and `_lzReceive` methods so you can inject your own business logic: Class inheritance diagram showing OApp.sol implementing the core interface for LayerZero Endpoint V2, with hookable _lzSend and _lzReceive methods for custom business logic Class inheritance diagram showing OApp.sol implementing the core interface for LayerZero Endpoint V2, with hookable _lzSend and _lzReceive methods for custom business logic If your use case only involves crosschain token transfers, consider inheriting the [**OFT Standard**](../oft/quickstart) instead of OApp. ## Installation To start using LayerZero contracts in a new project, use the LayerZero CLI tool, [**create-lz-oapp**](../../../get-started/create-lz-oapp/start). The CLI tool is an npx package that allows developers to create any omnichain application in \<4 minutes! Get started by running the following from your command line: ```bash wrap theme={null} npx create-lz-oapp@latest --example oapp ``` This will create an example repository containing both the Hardhat and Foundry frameworks, LayerZero development utilities, as well as the **OApp contract package** pre-installed. To use LayerZero contracts in an existing project, you can install the **OApp package** directly: ```bash wrap theme={null} npm install @layerzerolabs/oapp-evm ``` ```bash wrap theme={null} yarn add @layerzerolabs/oapp-evm ``` ```bash wrap theme={null} pnpm add @layerzerolabs/oapp-evm ``` ```bash wrap theme={null} forge init ``` ```bash wrap theme={null} forge install layerzero-labs/devtools forge install layerzero-labs/LayerZero-v2 forge install OpenZeppelin/openzeppelin-contracts git submodule add https://github.com/GNSPS/solidity-bytes-utils.git lib/solidity-bytes-utils ``` Then add to your `foundry.toml` under `[profile.default]`: ```toml wrap theme={null} [profile.default] src = "src" out = "out" libs = ["lib"] remappings = [ '@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/', '@layerzerolabs/lz-evm-protocol-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/protocol', '@layerzerolabs/lz-evm-messagelib-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/messagelib', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', 'solidity-bytes-utils/=lib/solidity-bytes-utils/', ] ``` LayerZero contracts work with both [**OpenZeppelin V5**](https://docs.openzeppelin.com/contracts/5.x/access-control#ownership-and-ownable) and V4 contracts. Specify your desired version in your project's package.json: ```typescript wrap theme={null} "resolutions": { "@openzeppelin/contracts": "^5.0.1", } ``` ## Custom OApp Contract To build your own crosschain application, inherit from `OApp.sol` and implement two key pieces: 1. **Send business logic**: how you encode and dispatch a custom `_message` on the source 2. **Receive business logic**: how you decode and apply an incoming `_message` on the destination Below is a complete example skeleton structure showing: * A constructor wiring in the local Endpoint and owner * A `sendString(...)` function that updates state, encodes a string, and calls `_lzSend(...)` * An override of `_lzReceive(...)` that decodes the string and applies business logic * (Optional) a `quoteSendString(...)` function to query the fee details needed to call `sendString(...)` ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { OApp, Origin, MessagingFee } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; contract MyOApp is OApp, OAppOptionsType3 { /// @notice Last string received from any remote chain string public lastMessage; /// @notice Msg type for sending a string, for use in OAppOptionsType3 as an enforced option uint16 public constant SEND = 1; /// @notice Initialize with Endpoint V2 and owner address /// @param _endpoint The local chain's LayerZero Endpoint V2 address /// @param _owner The address permitted to configure this OApp constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) Ownable(_owner) {} // ────────────────────────────────────────────────────────────────────────────── // 0. (Optional) Quote business logic // // Example: Get a quote from the Endpoint for a cost estimate of sending a message. // Replace this to mirror your own send business logic. // ────────────────────────────────────────────────────────────────────────────── /** * @notice Quotes the gas needed to pay for the full omnichain transaction in native gas or ZRO token. * @param _dstEid Destination chain's endpoint ID. * @param _string The string to send. * @param _options Message execution options (e.g., for sending gas to destination). * @param _payInLzToken Whether to return fee in ZRO token. * @return fee A `MessagingFee` struct containing the calculated gas fee in either the native token or ZRO token. */ function quoteSendString( uint32 _dstEid, string calldata _string, bytes calldata _options, bool _payInLzToken ) public view returns (MessagingFee memory fee) { bytes memory _message = abi.encode(_string); // combineOptions (from OAppOptionsType3) merges enforced options set by the contract owner // with any additional execution options provided by the caller fee = _quote(_dstEid, _message, combineOptions(_dstEid, SEND, _options), _payInLzToken); } // ────────────────────────────────────────────────────────────────────────────── // 1. Send business logic // // Example: send a simple string to a remote chain. Replace this with your // own state-update logic, then encode whatever data your application needs. // ────────────────────────────────────────────────────────────────────────────── /// @notice Send a string to a remote OApp on another chain /// @param _dstEid Destination Endpoint ID (uint32) /// @param _string The string to send /// @param _options Execution options for gas on the destination (bytes) function sendString(uint32 _dstEid, string calldata _string, bytes calldata _options) external payable { // 1. (Optional) Update any local state here. // e.g., record that a message was "sent": // sentCount += 1; // 2. Encode any data structures you wish to send into bytes // You can use abi.encode, abi.encodePacked, or directly splice bytes // if you know the format of your data structures bytes memory _message = abi.encode(_string); // 3. Call OAppSender._lzSend to package and dispatch the crosschain message // - _dstEid: remote chain's Endpoint ID // - _message: ABI-encoded string // - _options: combined execution options (enforced + caller-provided) // - MessagingFee(msg.value, 0): pay all gas as native token; no ZRO // - payable(msg.sender): refund excess gas to caller // // combineOptions (from OAppOptionsType3) merges enforced options set by the contract owner // with any additional execution options provided by the caller _lzSend( _dstEid, _message, combineOptions(_dstEid, SEND, _options), MessagingFee(msg.value, 0), payable(msg.sender) ); } // ────────────────────────────────────────────────────────────────────────────── // 2. Receive business logic // // Override _lzReceive to decode the incoming bytes and apply your logic. // The base OAppReceiver.lzReceive ensures: // • Only the LayerZero Endpoint can call this method // • The sender is a registered peer (peers[srcEid] == origin.sender) // ────────────────────────────────────────────────────────────────────────────── /// @notice Invoked by OAppReceiver when EndpointV2.lzReceive is called /// @dev _origin Metadata (source chain, sender address, nonce) /// @dev _guid Global unique ID for tracking this message /// @param _message ABI-encoded bytes (the string we sent earlier) /// @dev _executor Executor address that delivered the message /// @dev _extraData Additional data from the Executor (unused here) function _lzReceive( Origin calldata /*_origin*/, bytes32 /*_guid*/, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { // 1. Decode the incoming bytes into a string // You can use abi.decode, abi.decodePacked, or directly splice bytes // if you know the format of your data structures string memory _string = abi.decode(_message, (string)); // 2. Apply your custom logic. In this example, store it in `lastMessage`. lastMessage = _string; // 3. (Optional) Trigger further onchain actions. // e.g., emit an event, mint tokens, call another contract, etc. // emit MessageReceived(_origin.srcEid, _string); } } ``` ### Constructor * Pass the Endpoint V2 address and owner address into the base contracts. * `OApp(_endpoint, _owner)` binds your contract to the local LayerZero Endpoint V2 and registers the owner as the delegate, making it the only address that can change configurations (such as libraries, DVNs, and Executors. * `Ownable(_owner)` makes `_owner` the only address that can change configurations (such as peers, enforced options, and delegate). * After deployment, the owner can call: * `setConfig(...)` to adjust library or DVN parameters * `setSendLibrary(...)` and `setReceiveLibrary(...)` to override default libraries * `setPeer(...)` to whitelist remote OApp addresses * `setDelegate(...)` to assign a different delegate address A full overview of how to use these adminstrative functions can be found below under [**Deployment & Wiring**](#deployment-and-wiring). ### sendString(...) 1. **Update local state (optional)** * Before sending, you might update a counter, lock tokens, or perform any onchain action specific to your app. 2. **Encode the message** * Use `abi.encode(_message)`, `abi.encodePacked(_message)`, or manual byte shifting/offsets to turn the string into a `bytes` array. LayerZero [packets](../../../concepts/protocol/packet#packet-endpoint) carry raw `bytes`, so you must encode any data type into bytes first. 3. **Call `_lzSend(...)`** * `_dstEid` is the destination chain's [Endpoint ID](/v2/concepts/glossary#endpoint-id). LayerZero uses numeric IDs (e.g., `30101` for Ethereum, `30168` for Solana). * `_message` is the ABI-encoded string (`bytes memory`). * `_options` is a `bytes` array specifying gas or executor instructions for the destination. For example, an `ExecutorLzReceiveOption` tells the destination how much gas to allocate to your receive call. * `MessagingFee(msg.value, 0)` pays fees in native gas. If you wanted to pay in ZRO tokens, set the second field instead. * `payable(msg.sender)` specifies the refund address for any unused gas. This can be any address (EOA or contract), but if it's a contract, the contract must have a fallback function to receive the refund. ### \_lzReceive(...) 1. **Endpoint verification** * Only the LayerZero Endpoint V2 contract can invoke this function. The base `OAppReceiver` enforces that. * The call succeeds only if `_origin.sender == peers[_origin.srcEid]`. In other words, the sender's address must match the registered peer for that source chain. 2. **Decode the incoming bytes** * Use `abi.decode(_message, (string))` to extract the original string. If you sent a different data type (e.g., a struct), decode with the matching types. * Alternatively, you can use `abi.decodePacked()` for packed encoding, or manually splice bytes from specific offsets if you know the exact format of your data structures. 3. **Apply your business logic** * In this example, we store the decoded string in `lastMessage`. * You could instead: * Emit an event (e.g., `emit MessageReceived(_origin.srcEid, decoded)`) * Mint or unlock tokens based on the message * Call another contract to trigger a downstream workflow Always include all five parameters (`_origin`, `_guid`, `_message`, `_executor`, `_extraData`) in your override. Even if you only use `_message`, matching the function signature ensures the Endpoint can call your method correctly. ### (Optional) quoteSendString(...) You can optionally call the internal `OAppSender._quote(...)` method in a public function to provide accurate estimation for the gas cost of calling `MyOApp.sendString(...)`. The internal `_quote` method queries the send library selected by the OApp and asks the workers (DVNs and Executor) for fee details for the given encoded message: 1. **Fee estimation before sending** * Before calling `sendString(...)`, you need to know how much native gas (or ZRO tokens) to send with your transaction. The `quoteSendString(...)` function provides this cost estimate. 2. **Mirrors send logic** * The quote function uses the same message encoding (`abi.encode(_string)`) and option handling (`combineOptions(_dstEid, SEND, _options)`) as the actual send function, ensuring accurate fee estimates. 3. **Enforced options integration** * By inheriting `OAppOptionsType3` and using `combineOptions(...)`, the quote function automatically includes any enforced options that the contract owner has configured for the `SEND` message type, plus any additional options provided by the caller. 4. **Flexible payment options** * The `_payInLzToken` parameter lets you choose whether to pay fees in the native gas token of the source chain or in ZRO tokens. **Example usage:** ```solidity wrap theme={null} // Get fee estimate first MessagingFee memory fee = myOApp.quoteSendString( dstEid, "Hello World", "0x", // no additional options false // pay in native gas ); // Then send with the estimated fee myOApp.sendString{value: fee.nativeFee}( dstEid, "Hello World", "0x" ); ``` *** This section shows you exactly: * **Where** to update or check local state before sending * **How** to encode and send your application data over LayerZero * **Where** to decode incoming data and execute your custom logic Replace the `string` examples with whatever data structures and state changes your application requires. ## Deployment and Wiring After you finish writing and testing your `MyOApp` contract, follow these steps to deploy it on each network and wire up the messaging stack. We **strongly recommend** using the LayerZero CLI tool to manage your configurations. Our config generator simplifies access to all available deployments across networks and is the preferred method for crosschain messaging. See the [**CLI Guide**](../../../get-started/create-lz-oapp/start) for examples and how to use it in your project. ### 1. Deploy Your OApp Contract Deploy `MyOApp` on each chain using either the LayerZero CLI (recommended) or manual deployment scripts. After running `pnpm compile` at the root level of your example repo, you can deploy your contracts. #### Network Configuration Before using the CLI, you'll need to configure your networks in `hardhat.config.ts` with LayerZero Endpoint IDs and declare an RPC URL in your `.env` or directly in the config file: ```typescript wrap theme={null} // hardhat.config.ts import { EndpointId } from '@layerzerolabs/lz-definitions' // ... rest of hardhat config omitted for brevity networks: { 'optimism-sepolia-testnet': { // highlight-next-line eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, 'avalanche-fuji-testnet': { // highlight-next-line eid: EndpointId.AVALANCHE_V2_TESTNET, url: process.env.RPC_URL_FUJI || 'https://avalanche-fuji.drpc.org', accounts, }, 'arbitrum-sepolia-testnet': { // highlight-next-line eid: EndpointId.ARBSEP_V2_TESTNET, url: process.env.RPC_URL_ARB_SEPOLIA || 'https://arbitrum-sepolia.gateway.tenderly.co', accounts, }, } ``` The key addition to a standard `hardhat.config.ts` is the inclusion of LayerZero Endpoint IDs (`eid`) for each network. Check the [Deployments](../../../deployments/deployed-contracts) section for all available endpoint IDs. The LayerZero CLI provides automated deployment with built-in endpoint detection based on your `hardhat.config.ts` networks object: ```bash wrap theme={null} # Deploy using interactive prompts npx hardhat lz:deploy ``` The CLI will prompt you to: 1. **Select chains to deploy to:** ```bash wrap theme={null} ? Which networks would you like to deploy? › ◉ fuji ◉ amoy ◉ sepolia ``` 2. **Choose deploy script tags:** ```bash wrap theme={null} ? Which deploy script tags would you like to use? › MyOApp ``` 3. **Confirm deployment:** ```bash wrap theme={null} ✔ Do you want to continue? … yes Network: amoy Deployer: 0x0000000000000000000000000000000000000000 Network: sepolia Deployer: 0x0000000000000000000000000000000000000000 Deployed contract: MyOApp, network: amoy, address: 0x0000000000000000000000000000000000000000 Deployed contract: MyOApp, network: sepolia, address: 0x0000000000000000000000000000000000000000 ``` The CLI automatically: * Detects the correct LayerZero Endpoint V2 address for each chain * Deploys your OApp contract with proper constructor arguments * Generates deployment artifacts in `./deployments/` folder * Creates network-specific deployment files (e.g., `deployments/sepolia/MyOApp.json`) For manual deployment using Foundry, create a deployment script that handles endpoint addresses: ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { MyOApp } from "../contracts/MyOApp.sol"; contract DeployOApp is Script { function run() external { // Replace these env vars with your own values address endpoint = vm.envAddress("ENDPOINT_ADDRESS"); address owner = vm.envAddress("OWNER_ADDRESS"); vm.startBroadcast(vm.envUint("PRIVATE_KEY")); MyOApp oapp = new MyOApp(endpoint, owner); vm.stopBroadcast(); console.log("MyOApp deployed to:", address(oapp)); } } ``` Run the deployment script: ```bash wrap theme={null} # Deploy to testnet forge script script/DeployOApp.s.sol --rpc-url $RPC_URL --broadcast --verify # Deploy to multiple chains forge script script/DeployOApp.s.sol --rpc-url $ETHEREUM_RPC --broadcast --verify forge script script/DeployOApp.s.sol --rpc-url $POLYGON_RPC --broadcast --verify ``` You'll need to set the correct LayerZero Endpoint V2 addresses for each chain in your environment variables. Check the [Deployments](../../../deployments/deployed-contracts) section for endpoint addresses. ### 2. Wire Messaging Libraries and Configurations Once your contracts are onchain, you must set up send/receive libraries and DVN/Executor settings so crosschain messages flow correctly. **Production deployments should use multiple required DVNs from independent operators.** A single-DVN configuration means a compromise of that one verifier results in unrestricted forged messages on the pathway. See the [Integration Checklist](/v2/tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway) for production DVN guidance. The LayerZero CLI automatically handles all wiring via a single configuration file and command: #### Configuration File In your project root, you can find a `layerzero.config.ts` file: ```typescript wrap theme={null} import {EndpointId} from '@layerzerolabs/lz-definitions'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; import {OAppEnforcedOption, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; // This contract object defines the OApp deployment on Optimism Sepolia testnet // The config references the contract deployment from your ./deployments folder const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOApp', }; const avalancheContract: OmniPointHardhat = { eid: EndpointId.AVALANCHE_V2_TESTNET, contractName: 'MyOApp', }; const arbitrumContract: OmniPointHardhat = { eid: EndpointId.ARBSEP_V2_TESTNET, contractName: 'MyOApp', }; // For this example's simplicity, we will use the same enforced options values for sending to all chains // For production, you should ensure `gas` is set to the correct value through profiling the gas usage of calling OApp._lzReceive(...) on the destination chain // To learn more, read https://docs.layerzero.network/v2/concepts/applications/oapp-standard#execution-options-and-enforced-settings const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 80000, value: 0, }, ]; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> Avalanche // Optimism <-> Arbitrum // Avalanche <-> Arbitrum // With the config generator, pathways declared are automatically bidirectional // i.e. if you declare A,B there's no need to declare B,A // Replace with a non-LayerZero-Labs DVN provider for each pathway. // See /v2/deployments/dvn-addresses for the providers available on each chain. // Production deployments should use multiple required DVNs from independent operators — // a single-DVN configuration means a compromise of that one verifier results in // unrestricted forged messages on the pathway. const pathways: TwoWayConfig[] = [ [ optimismContract, // Chain A contract avalancheContract, // Chain B contract [['LayerZero Labs', ''], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [1, 1], // [A to B confirmations, B to A confirmations] — adjust per pathway; production deployments typically use larger values [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions ], [ optimismContract, // Chain A contract arbitrumContract, // Chain C contract [['LayerZero Labs', ''], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [1, 1], // [A to B confirmations, B to A confirmations] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain C enforcedOptions, Chain A enforcedOptions ], [ avalancheContract, // Chain B contract arbitrumContract, // Chain C contract [['LayerZero Labs', ''], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [1, 1], // [A to B confirmations, B to A confirmations] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain C enforcedOptions, Chain B enforcedOptions ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [ {contract: optimismContract}, {contract: avalancheContract}, {contract: arbitrumContract}, ], connections, }; } ``` Make sure your contract object's `contractName` matches the named deployment file for the network under `./deployments/`. #### Wire Everything Run a single command to configure all pathways: ```bash wrap theme={null} npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` This automatically handles: * Fetching the necessary contract addresses for each network from metadata * Setting send and receive libraries * Configuring DVNs and Executors * Setting up peers between contracts * Applying enforced options * All bidirectional pathways in your config For manual configuration using Foundry scripts, follow these steps: #### Environment Setup Here's a comprehensive `.env.example` file showing all the environment variables needed for the different configuration scripts: ```bash wrap theme={null} # Common variables used across scripts ENDPOINT_ADDRESS=0x... # LayerZero Endpoint V2 address OAPP_ADDRESS=0x... # Your OApp contract address SIGNER=0x... # Address with permissions to configure/send # Library Configuration (SetLibraries.s.sol) SEND_LIB_ADDRESS=0x... # SendUln302 address RECEIVE_LIB_ADDRESS=0x... # ReceiveUln302 address DST_EID=30101 # Destination chain EID SRC_EID=30110 # Source chain EID GRACE_PERIOD=0 # Grace period for library switch (0 for immediate) # Send Config (SetSendConfig.s.sol) SOURCE_ENDPOINT_ADDRESS=0x... # Chain A Endpoint address SENDER_OAPP_ADDRESS=0x... # OApp on Chain A REMOTE_EID=30101 # Endpoint ID for Chain B # Peer Configuration (SetPeers.s.sol) CHAIN1_EID=30101 # First chain EID CHAIN1_PEER=0x... # OApp address on first chain CHAIN2_EID=30110 # Second chain EID CHAIN2_PEER=0x... # OApp address on second chain CHAIN3_EID=30111 # Third chain EID CHAIN3_PEER=0x... # OApp address on third chain # Message Sending (SendMessage.s.sol) MESSAGE="Hello World" # Message to send crosschain ``` #### 2.1 Set Send and Receive Libraries 1. **Choose your libraries** (addresses of deployed MessageLib contracts). For standard crosschain messaging, you should use `SendUln302.sol` for `setSendLibrary(...)` and `ReceiveUln302.sol` for `setReceiveLibrary(...)`. You can find the deployments for these contracts under the [Deployments](../../../deployments/deployed-contracts) section. 2. Call `setSendLibrary(oappAddress, dstEid, sendLibAddress)` on the Endpoint. 3. Call `setReceiveLibrary(oappAddress, srcEid, receiveLibAddress, gracePeriod)` on the Endpoint. ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; /// @title LayerZero Library Configuration Script /// @notice Sets up send and receive libraries for OApp messaging contract SetLibraries is Script { function run() external { // Load environment variables address endpoint = vm.envAddress("ENDPOINT_ADDRESS"); // LayerZero Endpoint address address oapp = vm.envAddress("OAPP_ADDRESS"); // Your OApp contract address address signer = vm.envAddress("SIGNER"); // Address with permissions to configure // Library addresses address sendLib = vm.envAddress("SEND_LIB_ADDRESS"); // SendUln302 address address receiveLib = vm.envAddress("RECEIVE_LIB_ADDRESS"); // ReceiveUln302 address // Chain configurations uint32 dstEid = uint32(vm.envUint("DST_EID")); // Destination chain EID uint32 srcEid = uint32(vm.envUint("SRC_EID")); // Source chain EID uint32 gracePeriod = uint32(vm.envUint("GRACE_PERIOD")); // Grace period for library switch vm.startBroadcast(signer); // Set send library for outbound messages ILayerZeroEndpointV2(endpoint).setSendLibrary( oapp, // OApp address dstEid, // Destination chain EID sendLib // SendUln302 address ); // Set receive library for inbound messages ILayerZeroEndpointV2(endpoint).setReceiveLibrary( oapp, // OApp address srcEid, // Source chain EID receiveLib, // ReceiveUln302 address gracePeriod // Grace period for library switch ); vm.stopBroadcast(); } } ``` You would need to set up your `.env` file with the appropriate values: ```env wrap theme={null} ENDPOINT_ADDRESS=0x... OAPP_ADDRESS=0x... SIGNER=0x... SEND_LIB_ADDRESS=0x... # SendUln302 address RECEIVE_LIB_ADDRESS=0x... # ReceiveUln302 address DST_EID=30101 SRC_EID=30110 GRACE_PERIOD=0 # Set to 0 for immediate switch, or block number for gradual migration ``` #### 2.2 Set Send Config and Receive Config If you need non-default DVN or Executor settings (block confirmations, required DVNs, max message size, etc.), call `setConfig(...)` next. To see defaults, use `getConfig(...)`. **Send Config (A → B):** The send config is set on the source chain (Chain A) and applies to messages being sent from Chain A to Chain B. This config determines the DVN and Executor settings for outbound messages leaving Chain A and destined for Chain B. You must call `setConfig` on the Endpoint contract on Chain A, specifying the remote Endpoint ID for Chain B and the appropriate SendLib address for the A → B pathway. ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { ILayerZeroEndpointV2, SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; import { ExecutorConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBase.sol"; /// @title LayerZero Send Configuration Script (A → B) /// @notice Defines and applies ULN (DVN) + Executor configs for cross‑chain messages sent from Chain A to Chain B via LayerZero Endpoint V2. contract SetSendConfig is Script { uint32 constant EXECUTOR_CONFIG_TYPE = 1; uint32 constant ULN_CONFIG_TYPE = 2; /// @notice Broadcasts transactions to set both Send ULN and Executor configurations for messages sent from Chain A to Chain B function run() external { address endpoint = vm.envAddress("SOURCE_ENDPOINT_ADDRESS"); // Chain A Endpoint address oapp = vm.envAddress("SENDER_OAPP_ADDRESS"); // OApp on Chain A uint32 eid = uint32(vm.envUint("REMOTE_EID")); // Endpoint ID for Chain B address sendLib = vm.envAddress("SEND_LIB_ADDRESS"); // SendLib for A → B address signer = vm.envAddress("SIGNER"); /// @notice ULNConfig defines security parameters (DVNs + confirmation threshold) for A → B /// @notice Send config requests these settings to be applied to the DVNs and Executor for messages sent from A to B /// @dev 0 values will be interpretted as defaults, so to apply NIL settings, use: /// @dev uint8 internal constant NIL_DVN_COUNT = type(uint8).max; /// @dev uint64 internal constant NIL_CONFIRMATIONS = type(uint64).max; UlnConfig memory uln = UlnConfig({ confirmations: 15, // minimum block confirmations required on A before sending to B requiredDVNCount: 2, // number of DVNs required optionalDVNCount: type(uint8).max, // optional DVNs count, uint8 optionalDVNThreshold: 0, // optional DVN threshold requiredDVNs: [address(0x1111...), address(0x2222...)], // sorted list of required DVN addresses optionalDVNs: [] // sorted list of optional DVNs }); /// @notice ExecutorConfig sets message size limit + fee‑paying executor for A → B ExecutorConfig memory exec = ExecutorConfig({ maxMessageSize: 10000, // max bytes per crosschain message executor: address(0x3333...) // address that pays destination execution fees on B }); bytes memory encodedUln = abi.encode(uln); bytes memory encodedExec = abi.encode(exec); SetConfigParam[] memory params = new SetConfigParam[](2); params[0] = SetConfigParam(eid, EXECUTOR_CONFIG_TYPE, encodedExec); params[1] = SetConfigParam(eid, ULN_CONFIG_TYPE, encodedUln); vm.startBroadcast(signer); ILayerZeroEndpointV2(endpoint).setConfig(oapp, sendLib, params); // Set config for messages sent from A to B vm.stopBroadcast(); } } ``` **Receive Config (B ← A):** The receive config is set on the destination chain (Chain B) and applies to messages being received on Chain B from Chain A. This config determines the DVN settings for inbound messages arriving from Chain A. You must call `setConfig` on the Endpoint contract on Chain B, specifying the remote Endpoint ID for Chain A and the appropriate ReceiveLib address for the B ← A pathway. ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { ILayerZeroEndpointV2, SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; /// @title LayerZero Receive Configuration Script (B ← A) /// @notice Defines and applies ULN (DVN) config for inbound message verification on Chain B for messages received from Chain A via LayerZero Endpoint V2. contract SetReceiveConfig is Script { uint32 constant RECEIVE_CONFIG_TYPE = 2; function run() external { address endpoint = vm.envAddress("ENDPOINT_ADDRESS"); // Chain B Endpoint address oapp = vm.envAddress("OAPP_ADDRESS"); // OApp on Chain B uint32 eid = uint32(vm.envUint("REMOTE_EID")); // Endpoint ID for Chain A address receiveLib= vm.envAddress("RECEIVE_LIB_ADDRESS"); // ReceiveLib for B ← A address signer = vm.envAddress("SIGNER"); /// @notice UlnConfig controls verification threshold for incoming messages from A to B /// @notice Receive config enforces these settings have been applied to the DVNs for messages received from A /// @dev 0 values will be interpretted as defaults, so to apply NIL settings, use: /// @dev uint8 internal constant NIL_DVN_COUNT = type(uint8).max; /// @dev uint64 internal constant NIL_CONFIRMATIONS = type(uint64).max; UlnConfig memory uln = UlnConfig({ confirmations: 15, // min block confirmations from source (A) requiredDVNCount: 2, // required DVNs for message acceptance optionalDVNCount: type(uint8).max, // optional DVNs count optionalDVNThreshold: 0, // optional DVN threshold requiredDVNs: [address(0x1111...), address(0x2222...)], // sorted required DVNs optionalDVNs: [] // no optional DVNs }); bytes memory encodedUln = abi.encode(uln); SetConfigParam[] memory params = new SetConfigParam[](1); params[0] = SetConfigParam(eid, RECEIVE_CONFIG_TYPE, encodedUln); vm.startBroadcast(signer); ILayerZeroEndpointV2(endpoint).setConfig(oapp, receiveLib, params); // Set config for messages received on B from A vm.stopBroadcast(); } } ``` #### 2.3 Set Peers Once you've finished your **OApp Configuration** you can open the messaging channel and connect your OApp deployments by calling `setPeer`. A peer is required to be set for each EID (or network). Ideally an OApp (or OFT) will have multiple peers set where one and only one peer exists for one EID. The function takes 2 arguments: `_eid`, the destination endpoint ID for the chain our other OApp contract lives on, and `_peer`, the destination OApp contract address in `bytes32` format. ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { MyOApp } from "../contracts/MyOApp.sol"; /// @title LayerZero OApp Peer Configuration Script /// @notice Sets up peer connections between OApp deployments on different chains contract SetPeers is Script { function run() external { // Load environment variables address oapp = vm.envAddress("OAPP_ADDRESS"); // Your OApp contract address address signer = vm.envAddress("SIGNER"); // Address with owner permissions // Example: Set peers for different chains // Format: (chain EID, peer address in bytes32) (uint32 eid1, bytes32 peer1) = (uint32(vm.envUint("CHAIN1_EID")), bytes32(uint256(uint160(vm.envAddress("CHAIN1_PEER"))))); (uint32 eid2, bytes32 peer2) = (uint32(vm.envUint("CHAIN2_EID")), bytes32(uint256(uint160(vm.envAddress("CHAIN2_PEER"))))); (uint32 eid3, bytes32 peer3) = (uint32(vm.envUint("CHAIN3_EID")), bytes32(uint256(uint160(vm.envAddress("CHAIN3_PEER"))))); vm.startBroadcast(signer); // Set peers for each chain MyOApp(oapp).setPeer(eid1, peer1); MyOApp(oapp).setPeer(eid2, peer2); MyOApp(oapp).setPeer(eid3, peer3); vm.stopBroadcast(); } } ``` This function opens your OApp to start receiving messages from the messaging channel, meaning you should configure any application settings you intend on changing prior to calling `setPeer`. OApps need `setPeer` to be called correctly on both contracts to send messages. The peer address uses `bytes32` for handling non-EVM destination chains. If the peer has been set to an incorrect destination address, your messages will not be delivered and handled properly. If not resolved, users can potentially pay gas on source without any corresponding action on destination. You can confirm the peer address is the expected destination OApp address by viewing the `peers` mapping directly. #### 2.4 Set Enforced Options Enforced options allow the OApp owner to set mandatory execution parameters that will be applied to all messages of a specific type sent to a destination chain. These options are automatically combined with any caller-provided options when using `OAppOptionsType3`. **Why use enforced options?** * Ensure sufficient gas is always allocated for message execution on the destination * Enforce payment for additional services like PreCrime verification * Set consistent execution parameters across all users of your OApp * Prevent failed deliveries due to insufficient gas ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { MyOApp } from "../contracts/MyOApp.sol"; import { EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; /// @title LayerZero OApp Enforced Options Configuration Script /// @notice Sets enforced execution options for specific message types and destinations contract SetEnforcedOptions is Script { using OptionsBuilder for bytes; function run() external { // Load environment variables address oapp = vm.envAddress("OAPP_ADDRESS"); // Your OApp contract address address signer = vm.envAddress("SIGNER"); // Address with owner permissions // Destination chain configurations uint32 dstEid1 = uint32(vm.envUint("DST_EID_1")); // First destination EID uint32 dstEid2 = uint32(vm.envUint("DST_EID_2")); // Second destination EID // Message type (should match your contract's constant) uint16 SEND = 1; // Message type for sendString function // Build options using OptionsBuilder bytes memory options1 = OptionsBuilder.newOptions().addExecutorLzReceiveOption(80000, 0); bytes memory options2 = OptionsBuilder.newOptions().addExecutorLzReceiveOption(100000, 0); // Create enforced options array EnforcedOptionParam[] memory enforcedOptions = new EnforcedOptionParam[](2); // Set enforced options for first destination enforcedOptions[0] = EnforcedOptionParam({ eid: dstEid1, msgType: SEND, options: options1 }); // Set enforced options for second destination enforcedOptions[1] = EnforcedOptionParam({ eid: dstEid2, msgType: SEND, options: options2 }); vm.startBroadcast(signer); // Set enforced options on the OApp MyOApp(oapp).setEnforcedOptions(enforcedOptions); vm.stopBroadcast(); console.log("Enforced options set successfully!"); console.log("Destination 1 EID:", dstEid1, "Gas:", 80000); console.log("Destination 2 EID:", dstEid2, "Gas:", 100000); } } ``` **Environment variables needed:** ```env wrap theme={null} OAPP_ADDRESS=0x... # Your deployed MyOApp address SIGNER=0x... # Address with owner permissions DST_EID_1=30101 # First destination endpoint ID DST_EID_2=30110 # Second destination endpoint ID ``` **Run the script:** ```bash wrap theme={null} forge script script/SetEnforcedOptions.s.sol --rpc-url $RPC_URL --broadcast ``` Once set, these enforced options will be automatically applied when using `combineOptions()` in your send functions, ensuring consistent execution parameters across all messages.
## Usage Once deployed and wired, you can begin sending crosschain messages. ### Calling `send` The LayerZero CLI provides a convenient task for sending messages that automatically handles fee estimation and transaction execution. #### Using the Send Task The CLI includes a built-in `lz:oapp:send` task that: 1. Quotes the gas cost using your OApp's `quoteSendString()` function 2. Sends the message with the correct fee 3. Waits for confirmation and provides tracking links **Basic usage:** ```bash wrap theme={null} npx hardhat lz:oapp:send --dst-eid 30101 --string "Hello ethereum" --network arbitrum-sepolia-testnet ``` **Parameters:** * `--dst-eid`: Destination endpoint ID (required) * `--string`: Message to send (required) * `--network`: Source network name from your hardhat config (required) * `--options`: Execution options in hex format (optional, defaults to `0x`) **Example output:** ```bash wrap theme={null} Initiating string send from arbitrum-sepolia-testnet to ethereum-sepolia-testnet String to send: "Hello ethereum" Destination EID: 30101 Using signer: 0x1234567890123456789012345678901234567890 MyOApp contract found at: 0xabcdefabcdefabcdefabcdefabcdefabcdefabcd Execution options: 0x Quoting gas cost for the send transaction... Native fee: 0.001234567890123456 ETH LZ token fee: 0 LZ Sending the string transaction... Transaction hash: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef Waiting for transaction confirmation... Gas used: 123456 Block number: 1234567 ✅ SENT_VIA_OAPP: Successfully sent "Hello ethereum" from arbitrum-sepolia-testnet to ethereum-sepolia-testnet ✅ TX_HASH: Block explorer link for source chain arbitrum-sepolia-testnet: https://sepolia.arbiscan.io/tx/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef ✅ EXPLORER_LINK: LayerZero Scan link for tracking crosschain delivery: https://testnet.layerzeroscan.com/tx/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef ``` The task automatically: * Finds your deployed `MyOApp` contract * Quotes the exact gas fee needed * Sends the transaction with proper gas estimation * Provides block explorer and LayerZero Scan links for tracking For manual message sending using Foundry, create a script that handles fee estimation and message transmission: ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { MyOApp } from "../contracts/MyOApp.sol"; import { MessagingFee } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; /// @title LayerZero OApp Message Sending Script /// @notice Demonstrates how to send messages between OApp deployments contract SendMessage is Script { function run() external { // Load environment variables address oapp = vm.envAddress("OAPP_ADDRESS"); // Your OApp contract address address signer = vm.envAddress("SIGNER"); // Address with permissions to send // Destination chain configuration uint32 dstEid = uint32(vm.envUint("DST_EID")); // Destination chain EID // Message to send string memory message = vm.envString("MESSAGE"); // Your crosschain message bytes memory options = vm.envBytes("OPTIONS"); // Execution options (or use "0x" for default) // Get the MyOApp contract instance MyOApp myOApp = MyOApp(oapp); // 1. Quote the gas cost first MessagingFee memory fee = myOApp.quoteSendString( dstEid, message, options, false // Pay in native gas, not ZRO tokens ); console.log("Estimated native fee:", fee.nativeFee); console.log("Estimated LZ token fee:", fee.lzTokenFee); // 2. Send the message with the quoted fee vm.startBroadcast(signer); myOApp.sendString{value: fee.nativeFee}( dstEid, message, options ); vm.stopBroadcast(); console.log("Message sent successfully!"); } } ``` **Environment variables needed:** ```env wrap theme={null} OAPP_ADDRESS=0x... # Your deployed MyOApp address SIGNER=0x... # Private key or address with permissions DST_EID=30101 # Destination endpoint ID MESSAGE="Hello World" # Message to send OPTIONS=0x # Execution options (0x for default) ``` **Run the script:** ```bash wrap theme={null} forge script script/SendMessage.s.sol --rpc-url $RPC_URL --broadcast ``` ## Extensions The OApp Standard can be extended with various messaging patterns to support complex crosschain applications. Each pattern functions as a distinct omnichain building block, capable of being used independently or in combination. ### ABA (Ping-Pong) Pattern The **ABA** pattern enables nested messaging where a message sent from Chain A to Chain B triggers another message back to Chain A (`A` → `B` → `A`). This is useful for crosschain authentication, data feeds, or conditional contract execution. Diagram showing ABA messaging pattern: a ping-pong style call where Chain A sends to Chain B, which then sends back to Chain A (A → B → A) Diagram showing ABA messaging pattern: a ping-pong style call where Chain A sends to Chain B, which then sends back to Chain A (A → B → A) #### Implementation The key is to nest an `_lzSend` call within your `_lzReceive` function: ```solidity wrap theme={null} function _lzReceive( Origin calldata _origin, bytes32 /*_guid*/, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { // Decode the incoming message (string memory data, uint16 msgType, bytes memory returnOptions) = abi.decode(_message, (string, uint16, bytes)); // Process the message lastMessage = data; if (msgType == SEND_ABA) { // Send response back to origin chain _lzSend( _origin.srcEid, abi.encode("Response from Chain B", SEND), returnOptions, MessagingFee(msg.value, 0), payable(address(this)) ); } } ``` **ABA Pattern Gas Planning**: When implementing the ABA pattern, consider these important factors: 1. **Encode return options in your message**: Include the `_options` parameter for the B→A transaction within your A→B message encoding, as shown in the example above with `returnOptions`. 2. **Calculate total gas costs upfront**: The source OApp (A) needs to know the full transaction cost for the entire A→B→A flow. You should: * Quote the cost of the B→A transaction beforehand * Include this cost in your `lzReceiveOption` gas allocation for the A→B transaction * Ensure sufficient `msg.value` is forwarded to cover both legs of the journey 3. **Example gas calculation**: ```solidity wrap theme={null} // Quote B→A cost first MessagingFee memory returnFee = quoteBtoA(returnOptions); // Include return fee in A→B options bytes memory abaOptions = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(baseGas + returnGas, returnFee.nativeFee); ``` This ensures your ABA transaction has sufficient gas to complete the full round trip. ### Batch Send **Batch Send** allows a single transaction to initiate multiple `_lzSend` calls to various destination chains, reducing operational overhead for multi-chain operations. Diagram showing Batch Send pattern: a single transaction from Chain A initiating multiple _lzSend calls to Chains B, C, and D simultaneously Diagram showing Batch Send pattern: a single transaction from Chain A initiating multiple _lzSend calls to Chains B, C, and D simultaneously #### Key Implementation Points The batch send pattern includes several important design decisions: 1. **Fee Validation**: Override `_payNative` to change fee check from equivalency to `<` since batch fees are cumulative 2. **Consistent Loop Pattern**: Both `quote` and `send` functions use identical for loops to iterate through destinations for predictable behavior #### Implementation ```solidity wrap theme={null} // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import { OApp, MessagingFee, Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; /** * @title BatchSendMock contract for demonstrating multiple outbound crosschain calls using LayerZero. * @notice THIS IS AN EXAMPLE CONTRACT. DO NOT USE THIS CODE IN PRODUCTION. * @dev This contract showcases how to send multiple crosschain calls with one source function call using LayerZero's OApp Standard. */ contract BatchSendMock is OApp, OAppOptionsType3 { /// @notice Last received message data. string public data = "Nothing received yet"; /// @notice Message types that are used to identify the various OApp operations. /// @dev These values are used in things like combineOptions() in OAppOptionsType3 (enforcedOptions). uint16 public constant SEND = 1; /// @notice Emitted when a message is received from another chain. event MessageReceived(string message, uint32 senderEid, bytes32 sender); /// @notice Emitted when a message is sent to another chain (A -> B). event MessageSent(string message, uint32 dstEid); /// @dev Revert with this error when an invalid message type is used. error InvalidMsgType(); /** * @dev Constructs a new BatchSend contract instance. * @param _endpoint The LayerZero endpoint for this contract to interact with. * @param _owner The owner address that will be set as the owner of the contract. */ constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) Ownable(msg.sender) {} // Override to change fee check from equivalency to < since batch fees are cumulative function _payNative(uint256 _nativeFee) internal override returns (uint256 nativeFee) { if (msg.value < _nativeFee) revert NotEnoughNative(msg.value); return _nativeFee; } /** * @notice Returns the estimated messaging fee for a given message. * @param _dstEids Destination endpoint ID array where the message will be batch sent. * @param _msgType The type of message being sent. * @param _message The message content. * @param _extraSendOptions Extra gas options for receiving the send call (A -> B). * Will be summed with enforcedOptions, even if no enforcedOptions are set. * @param _payInLzToken Boolean flag indicating whether to pay in LZ token. * @return totalFee The estimated messaging fee for sending to all pathways. */ function quote( uint32[] memory _dstEids, uint16 _msgType, string memory _message, // Semantic naming for message content bytes calldata _extraSendOptions, bool _payInLzToken ) public view returns (MessagingFee memory totalFee) { bytes memory encodedMessage = abi.encode(_message); // Clear distinction: input vs processed for (uint i = 0; i < _dstEids.length; i++) { bytes memory options = combineOptions(_dstEids[i], _msgType, _extraSendOptions); MessagingFee memory fee = _quote(_dstEids[i], encodedMessage, options, _payInLzToken); totalFee.nativeFee += fee.nativeFee; totalFee.lzTokenFee += fee.lzTokenFee; } } function send( uint32[] memory _dstEids, uint16 _msgType, string memory _message, bytes calldata _extraSendOptions // gas settings for A -> B ) external payable { // Message type validation for security and extensibility if (_msgType != SEND) { revert InvalidMsgType(); } // Gas efficiency: calculate total fees upfront (fail-fast pattern) MessagingFee memory totalFee = quote(_dstEids, _msgType, _message, _extraSendOptions, false); require(msg.value >= totalFee.nativeFee, "Insufficient fee provided"); // Encodes the message before invoking _lzSend. bytes memory _encodedMessage = abi.encode(_message); uint256 totalNativeFeeUsed = 0; uint256 remainingValue = msg.value; for (uint i = 0; i < _dstEids.length; i++) { bytes memory options = combineOptions(_dstEids[i], _msgType, _extraSendOptions); MessagingFee memory fee = _quote(_dstEids[i], _encodedMessage, options, false); totalNativeFeeUsed += fee.nativeFee; remainingValue -= fee.nativeFee; // Granular fee tracking per destination require(remainingValue >= 0, "Insufficient fee for this destination"); _lzSend( _dstEids[i], _encodedMessage, options, fee, payable(msg.sender) ); emit MessageSent(_message, _dstEids[i]); // Event emission for tracking } } /** * @notice Internal function to handle receiving messages from another chain. * @dev Decodes and processes the received message based on its type. * @param _origin Data about the origin of the received message. * @param message The received message content. */ function _lzReceive( Origin calldata _origin, bytes32 /*guid*/, bytes calldata message, address, // Executor address as specified by the OApp. bytes calldata // Any extra data or options to trigger on receipt. ) internal override { string memory _data = abi.decode(message, (string)); data = _data; emit MessageReceived(data, _origin.srcEid, _origin.sender); } } ``` This pattern is particularly useful for **mass updating state from a single call** - allowing you to push data from one chain to many chains efficiently. Common use cases include configuration updates, price feeds, or state synchronization across multiple destination chains. ### Call Composer **Composed** messaging enables **horizontal composability** where a message triggers external contract calls on the destination chain through `lzCompose`. Unlike vertical composability (multiple calls in a single transaction), horizontal composability processes operations as separate, containerized message packets. Diagram showing horizontal composability: OApp receives message via lzReceive, then calls sendCompose to deliver a separate composed message to an external contract via lzCompose (A → B1 → B2) Diagram showing horizontal composability: OApp receives message via lzReceive, then calls sendCompose to deliver a separate composed message to an external contract via lzCompose (A → B1 → B2) #### Benefits of Horizontal Composability * **Fault Isolation**: If a composed call fails, it doesn't revert the main token transfer or message * **Gas Efficiency**: Each step can have independent gas limits and execution options * **Flexible Workflows**: Complex multi-step operations can be broken into manageable pieces #### Sending Side ```solidity wrap theme={null} function sendStringToComposer( uint32 _dstEid, string memory _string, address _composer, bytes calldata _extraOptions ) external payable { // Include both lzReceive and lzCompose options in enforcedOptions or extraOptions bytes memory composedOptions = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(65000, 0) // For the main receive .addExecutorLzComposeOption(0, 50000, 0); // For the compose call bytes memory _message = abi.encode(_string, _composer); _lzSend( _dstEid, _message, composedOptions, MessagingFee(msg.value, 0), payable(msg.sender) ); } ``` #### Receiving Side ```solidity wrap theme={null} function _lzReceive( Origin calldata _origin, bytes32 _guid, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { (string memory _string, address composer) = abi.decode(_message, (string, address)); // Store the message and perform primary logic lastMessage = _string; // Send composed message to external contract as separate message packet endpoint.sendCompose(composer, _guid, 0, _message); } ``` #### Composer Contract ```solidity wrap theme={null} import { IOAppComposer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol"; contract Composer is IOAppComposer { address public immutable endpoint; address public immutable trustedOApp; constructor(address _endpoint, address _trustedOApp) { endpoint = _endpoint; trustedOApp = _trustedOApp; } function lzCompose( address _oApp, bytes32 /*_guid*/, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) external payable override { // Security checks require(msg.sender == endpoint, "!endpoint"); require(_oApp == trustedOApp, "!oApp"); // Decode the message payload (string memory _string, ) = abi.decode(_message, (string, address)); // Execute custom business logic performCustomAction(_string); } function performCustomAction(string memory message) internal { // Your custom logic here (swap, stake, mint, etc.) } } ``` **Execution Options for Composed Messages**: You must provide gas for both the main `lzReceive` call and the `lzCompose` call: ```solidity wrap theme={null} bytes memory options = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(baseGas, 0) // Main message processing .addExecutorLzComposeOption(0, composeGas, value); // Composed call (index 0) ``` The `_index` parameter allows multiple composed calls with different gas allocations. ### Message Ordering LayerZero supports both **unordered** (default) and **ordered** delivery patterns. #### Ordered Delivery Implementation ```solidity wrap theme={null} pragma solidity ^0.8.22; import { OApp, Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; /** * @title OmniChain Nonce Ordered Enforcement Example * @dev Implements nonce ordered enforcement for your OApp. */ contract OrderedOApp is OApp { // Mapping to track the maximum received nonce for each source endpoint and sender mapping(uint32 eid => mapping(bytes32 sender => uint64 nonce)) private receivedNonce; constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) {} /** * @dev Public function to get the next expected nonce for a given source endpoint and sender. * @param _srcEid Source endpoint ID. * @param _sender Sender's address in bytes32 format. * @return uint64 Next expected nonce. */ function nextNonce(uint32 _srcEid, bytes32 _sender) public view virtual override returns (uint64) { return receivedNonce[_srcEid][_sender] + 1; } /** * @dev Internal function to accept nonce from the specified source endpoint and sender. * @param _srcEid Source endpoint ID. * @param _sender Sender's address in bytes32 format. * @param _nonce The nonce to be accepted. */ function _acceptNonce(uint32 _srcEid, bytes32 _sender, uint64 _nonce) internal virtual override { uint64 expectedNonce = receivedNonce[_srcEid][_sender] + 1; require(_nonce == expectedNonce, "OApp: invalid nonce"); receivedNonce[_srcEid][_sender] = _nonce; // Update to the accepted nonce } /** * @dev Override receive function to enforce strict nonce enforcement. * @dev This function is internal and should not be public. */ function _lzReceive( Origin calldata _origin, bytes32 _guid, bytes calldata _message, address _executor, bytes calldata _extraData ) internal override { // Enforce nonce ordering before processing the message _acceptNonce(_origin.srcEid, _origin.sender, _origin.nonce); // Process your message logic here // Example: string memory receivedMessage = abi.decode(_message, (string)); } // Must include ExecutorOrderedExecutionOption in your send options function sendOrdered(uint32 _dstEid, string memory _message) external payable { bytes memory options = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(200000, 0) .addExecutorOrderedExecutionOption(); // Required for ordered execution _lzSend(_dstEid, abi.encode(_message), options, MessagingFee(msg.value, 0), payable(msg.sender)); } } ``` #### Important Nonce Management Considerations When implementing ordered delivery, be aware of these critical nonce synchronization issues: 1. **Nonce Validation**: The `_acceptNonce` function must be called in `_lzReceive` to verify the incoming nonce matches the expected sequence before processing any message. 2. **Protocol vs Local Nonce Mismatch**: Functions like `skip()`, `burn()`, and `clear()` advance the protocol's nonce but **do not** automatically update your OApp's local nonce mapping. This creates a dangerous mismatch where: * Protocol nonce: 15 (after skipping message 15) * OApp mapping: 14 (still expecting message 15) * Result: All future messages will be rejected 3. **Solution**: If your OApp needs to use `skip()`, `burn()`, or `clear()`, you must **manually increment your local nonce** to stay synchronized: ```solidity wrap theme={null} // When skipping a message, update your local tracking function skipMessage(uint32 _srcEid, bytes32 _sender, uint64 _nonce) external onlyOwner { // Skip the message at protocol level endpoint.skip(this, _srcEid, _sender, _nonce); // Critical: Update local nonce to match protocol receivedNonce[_srcEid][_sender] = _nonce; } ``` **Best Practice**: Only call these recovery functions from within your OApp contract, never externally, to ensure nonce synchronization is maintained. ### Rate Limiting Control message frequency to prevent spam and ensure controlled crosschain interactions: ```solidity wrap theme={null} contract RateLimitedOApp is OApp, RateLimiter { constructor( address _endpoint, address _owner, RateLimitConfig[] memory _rateLimitConfigs ) OApp(_endpoint, _owner) { _setRateLimits(_rateLimitConfigs); } function sendWithRateLimit( uint32 _dstEid, string memory _message, bytes calldata _options ) external payable { // Check rate limit before sending _outflow(_dstEid, 1); // 1 message _lzSend( _dstEid, abi.encode(_message), _options, MessagingFee(msg.value, 0), payable(msg.sender) ); } } ``` ### Further Reading For detailed implementations and advanced patterns, see: * [Message Execution Options](../configuration/options) - Options configuration * [OApp Technical Reference](../../../concepts/technical-reference/oapp-reference) - Deep dive into OApp mechanics * [Integration Checklist](../../../tools/integration-checklist) - Security considerations and best practices ### Tracing and Troubleshooting You can follow your testnet and mainnet transaction statuses using [LayerZero Scan](https://layerzeroscan.com/). Refer to [Debugging Messages](../troubleshooting/debugging-messages) for any unexpected complications when sending a message. You can also ask for help or follow development in the [Discord](https://discord.com/invite/ktbvm8Nkcr). # Sending Tokenized Assets Source: https://docs.layerzero.network/v2/developers/evm/oft/native-transfer Step-by-step guide to sending tokenized assets using LayerZero V2. Build and deploy omnichain applications with crosschain messaging. Follow step-by-step de... To transfer tokens to different blockchain networks using LayerZero, you have 3 options: * **Build your own Omnichain Token** using LayerZero contract standards. * **Send native gas tokens** as part of your message's execution options. * **Utilize a native bridge** built on top of LayerZero (e.g., Stargate). ## Building Your Own Omnichain Token The **Omnichain Fungible Token (OFT) Standard** and **Omnichain Non-Fungible Token (ONFT) Standard** are ideal for creating tokens that exist on multiple chains. These standards allow tokens to be transferred across multiple blockchains without asset wrapping or middlechains, ensuring consistency and interoperability for holders. For new tokens, inherit from `OFT` or `ONFT`. For existing tokens, use `OFTAdapter` or `ONFTAdapter`. To build a token using `OFT` or `ONFT`, you need to deploy the standard contracts on each chain where the token you own will or currently exists. Read the [OFT Quickstart](../oft/quickstart) and the [ONFT Quickstart](../onft/quickstart) to learn more. ## Sending Small Amounts of Native Gas Depending on your destination application's logic, you may want to transfer small amounts of native gas tokens for the destination chain's transaction fees or to help users onboard to the new blockchain. LayerZero [Message Execution Options](../configuration/options) enable you to send small amounts of native gas as part of your crosschain call or to a specific address on the destination chain: * **`lzReceive`**: Send `gasLimit` AND / OR `msg.value` as part of the destination `EndpointV2.lzReceive` call. * **`lzCompose`**: Send `gasLimit` AND / OR `msg.value` as part of the destination `EndpointV2.lzCompose` call. * **`lzNativeDrop`**: Send an `_amount` of native gas in wei to a specific `_receiver` address. These gas amounts will be paid for on the source chain by the caller of `EndpointV2.send` within your application, abstracting gas management from your users. For more information, see [Transaction Pricing](../../../concepts/protocol/transaction-pricing). ## Moving Native Assets (e.g., wETH, USDC, USDT) To move native assets that have already been deployed by another contract owner, two methods exist to help your development: ### Option 1: Protocols or Native Bridges Built on LayerZero Utilize a protocol, decentralized exchange (DEX), or native asset bridge built on LayerZero (e.g., Stargate) for transferring native assets between chains. **Functionality:** Stargate and similar platforms handle the creation of asset pools, facilitating the easy movement of native assets across multiple chains. **Advantages:** This option enables you to utilize existing liquidity and composability with your smart contracts without the need for deploying the OFT Standards directly. Read the [Stargate Docs](https://stargateprotocol.gitbook.io/stargate/v2-developer-docs) for how to transfer and swap crosschain assets in your smart contracts. ### Option 2: Wrapped Asset Bridges If you run your own blockchain, you can [Contact LayerZero Labs](https://layerzeronetwork.typeform.com/to/U9hMgxf1) to deploy a [LayerZero Endpoint](../../../concepts/protocol/layerzero-endpoint) contract on your network. This enables the creation of a wrapped asset bridge to easily move existing assets to your chain. **Wrapped Asset Bridge:** The bridge locks tokens on the source chain and mints equivalent tokens on the destination chain using the OFT Standard. This method is not advisable if this bridge will not be endorsed by the chain, as it requires acceptance and liquidity to be provided for the new token standard (e.g., "yourUSDC") by DeFi applications. Established tokens or those endorsed by the chain will have better composability and usability. # LayerZero V2 OFT Quickstart Source: https://docs.layerzero.network/v2/developers/evm/oft/quickstart Get started with OFT Quickstart. Step-by-step tutorial for building omnichain applications on LayerZero V2. LayerZero enables secure crosschain messaging. The **Omnichain Fungible Token (OFT) Standard** enables fungible tokens to exist across multiple blockchains while maintaining a unified supply. The OFT standard works by **debiting** an amount of tokens from a sender on the source chain and **crediting** the same amount of tokens to a receiver on the destination chain. ### OFT The `_debit` function in `OFT.sol` burns an amount of an ERC20 token, while `_credit` mints ERC20 tokens on the destination chain. Diagram showing OFT burn-and-mint mechanism: tokens are burned (subtracted) on Network A and minted (added) on Network B, connected by an arrow representing the crosschain transfer Diagram showing OFT burn-and-mint mechanism: tokens are burned (subtracted) on Network A and minted (added) on Network B, connected by an arrow representing the crosschain transfer `OFT.sol` extends the base `OApp.sol` and inherits `ERC20`, providing both crosschain messaging and standard token functionality: Class inheritance diagram showing OFT.sol extending OApp.sol for crosschain messaging and inheriting ERC20 for standard token functionality Class inheritance diagram showing OFT.sol extending OApp.sol for crosschain messaging and inheriting ERC20 for standard token functionality ### OFT Adapter `OFTAdapter.sol` can be used for already deployed ERC20 tokens who lack mint capabilities, so that the `_debit` function calls `safeERC20.transferFrom` from a sender, while `_credit` calls `safeERC20.transfer` to a receiver. Diagram showing OFT Adapter lock-and-mint mechanism: ERC20 tokens are locked in an escrow contract on Network A, and equivalent OFT tokens are minted on Network B Diagram showing OFT Adapter lock-and-mint mechanism: ERC20 tokens are locked in an escrow contract on Network A, and equivalent OFT tokens are minted on Network B `OFTAdapter.sol` provides token bridging without modifying the original ERC20 token contract: Class inheritance diagram showing OFTAdapter.sol extending OApp.sol for crosschain messaging while wrapping an existing ERC20 token contract Class inheritance diagram showing OFTAdapter.sol extending OApp.sol for crosschain messaging while wrapping an existing ERC20 token contract If your use case involves crosschain messaging beyond token transfers, consider using the [**OApp Standard**](../oapp/overview) for maximum flexibility. For detailed technical information about transfer flows, decimal handling, and architecture patterns, see the [**OFT Technical Reference**](../../../concepts/technical-reference/oft-reference). ### Explore Deployed OFTs Browse production OFT deployments from various asset issuers, including Stargate-managed assets, on the [**OFT Ecosystem & Stargate Assets**](../../../deployments/oft-ecosystem-stargate-assets) page. See which tokens are available for crosschain transfers across LayerZero-supported chains. ## Find your chain LayerZero supports OFT deployment on every EVM chain in the table below. Find your chain's Endpoint ID (EID), `EndpointId` enum token, native chain ID, and a default RPC URL, then use those values wherever this guide references a chain (for example in your `hardhat.config.ts` networks block and your LayerZero config). The deployment flow is identical for every chain; only these identifiers change. | Chain | Stage | EID | EndpointId | Native chain ID | RPC URL | | ------------------------------ | ------- | ----- | ---------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 0G Mainnet | mainnet | 30388 | OG\_V2\_MAINNET | 16661 | [https://evmrpc.0g.ai](https://evmrpc.0g.ai) | | Abstract Mainnet | mainnet | 30324 | ABSTRACT\_V2\_MAINNET | 2741 | [https://api.mainnet.abs.xyz](https://api.mainnet.abs.xyz) | | Animechain Mainnet | mainnet | 30372 | ANIMECHAIN\_V2\_MAINNET | 69000 | (add your RPC) | | Ape Mainnet | mainnet | 30312 | APE\_V2\_MAINNET | 33139 | [https://rpc.apechain.com](https://rpc.apechain.com) | | Apex Fusion Nexus Mainnet | mainnet | 30384 | APEXFUSIONNEXUS\_V2\_MAINNET | 9069 | [https://rpc.nexus.mainnet.apexfusion.org](https://rpc.nexus.mainnet.apexfusion.org) | | Arbitrum Mainnet | mainnet | 30110 | ARBITRUM\_V2\_MAINNET | 42161 | [https://arb1.arbitrum.io/rpc](https://arb1.arbitrum.io/rpc) | | Arbitrum Nova Mainnet | mainnet | 30175 | NOVA\_V2\_MAINNET | 42170 | [https://arbitrum-nova.gateway.tenderly.co](https://arbitrum-nova.gateway.tenderly.co) | | Arc Mainnet | mainnet | 30417 | ARC\_V2\_MAINNET | 5042 | (add your RPC) | | Astar Mainnet | mainnet | 30210 | ASTAR\_V2\_MAINNET | 592 | [https://astar.api.onfinality.io/public](https://astar.api.onfinality.io/public) | | Astar zkEVM Mainnet | mainnet | 30257 | ZKATANA\_V2\_MAINNET | 3776 | [https://rpc.startale.com/astar-zkevm](https://rpc.startale.com/astar-zkevm) | | AULT Mainnet | mainnet | 30413 | AULT\_V2\_MAINNET | 904 | (add your RPC) | | Avalanche Mainnet | mainnet | 30106 | AVALANCHE\_V2\_MAINNET | 43114 | [https://api.avax.network/ext/bc/C/rpc](https://api.avax.network/ext/bc/C/rpc) | | Base Mainnet | mainnet | 30184 | BASE\_V2\_MAINNET | 8453 | [https://mainnet.base.org](https://mainnet.base.org) | | Beam Mainnet | mainnet | 30198 | MERITCIRCLE\_V2\_MAINNET | 4337 | [https://subnets.avax.network/beam/mainnet/rpc](https://subnets.avax.network/beam/mainnet/rpc) | | Berachain Mainnet | mainnet | 30362 | BERA\_V2\_MAINNET | 80094 | [https://rpc.berachain.com](https://rpc.berachain.com) | | Bitlayer Mainnet | mainnet | 30314 | BITLAYER\_V2\_MAINNET | 200901 | [https://rpc.bitlayer.org](https://rpc.bitlayer.org) | | Blast Mainnet | mainnet | 30243 | BLAST\_V2\_MAINNET | 81457 | [https://rpc.blast.io](https://rpc.blast.io) | | BNB Smart Chain (BSC) Mainnet | mainnet | 30102 | BSC\_V2\_MAINNET | 56 | [https://bsc.drpc.org](https://bsc.drpc.org) | | BOB Mainnet | mainnet | 30279 | BOB\_V2\_MAINNET | 60808 | [https://rpc.gobob.xyz](https://rpc.gobob.xyz) | | Botanix | mainnet | 30376 | BOTANIX\_V2\_MAINNET | 3637 | [https://rpc.botanixlabs.com](https://rpc.botanixlabs.com) | | Bouncebit Mainnet | mainnet | 30293 | BOUNCEBIT\_V2\_MAINNET | 6001 | [https://fullnode-mainnet.bouncebitapi.com](https://fullnode-mainnet.bouncebitapi.com) | | Camp Mainnet | mainnet | 30381 | CAMP\_V2\_MAINNET | 484 | [https://rpc.camp.raas.gelato.cloud](https://rpc.camp.raas.gelato.cloud) | | Canto Mainnet | mainnet | 30159 | CANTO\_V2\_MAINNET | 7700 | [https://canto-rpc.ansybl.io](https://canto-rpc.ansybl.io) | | Celo Mainnet | mainnet | 30125 | CELO\_V2\_MAINNET | 42220 | [https://forno.celo.org](https://forno.celo.org) | | Chiliz Mainnet | mainnet | 30409 | CHILIZ\_V2\_MAINNET | 88888 | [https://chiliz-mainnet.gateway.tatum.io](https://chiliz-mainnet.gateway.tatum.io) | | Citrea Mainnet | mainnet | 30403 | CITREA\_V2\_MAINNET | 4114 | [https://rpc.mainnet.citrea.xyz](https://rpc.mainnet.citrea.xyz) | | Codex Mainnet | mainnet | 30323 | CODEX\_V2\_MAINNET | 81224 | [https://rpc.codex.xyz](https://rpc.codex.xyz) | | Concrete | mainnet | 30366 | CONCRETE\_V2\_MAINNET | 12739 | (add your RPC) | | Conflux eSpace Mainnet | mainnet | 30212 | CONFLUX\_V2\_MAINNET | 1030 | [https://evm.confluxrpc.com](https://evm.confluxrpc.com) | | Core Mainnet | mainnet | 30153 | COREDAO\_V2\_MAINNET | 1116 | [https://rpc.coredao.org](https://rpc.coredao.org) | | Cronos EVM Mainnet | mainnet | 30359 | CRONOSEVM\_V2\_MAINNET | 25 | [https://evm.cronos.org](https://evm.cronos.org) | | Cronos zkEVM Mainnet | mainnet | 30360 | CRONOSZKEVM\_V2\_MAINNET | 388 | [https://mainnet.zkevm.cronos.org](https://mainnet.zkevm.cronos.org) | | Cyber Mainnet | mainnet | 30283 | CYBER\_V2\_MAINNET | 7560 | [https://rpc.cyber.co](https://rpc.cyber.co) | | Degen Mainnet | mainnet | 30267 | DEGEN\_V2\_MAINNET | 666666666 | [https://rpc.degen.tips](https://rpc.degen.tips) | | Derive Mainnet | mainnet | 30311 | LYRA\_V2\_MAINNET | 957 | [https://rpc.lyra.finance](https://rpc.lyra.finance) | | Dexalot Subnet Mainnet | mainnet | 30118 | DEXALOT\_V2\_MAINNET | 432204 | [https://subnets.avax.network/dexalot/mainnet/rpc](https://subnets.avax.network/dexalot/mainnet/rpc) | | DFK Chain | mainnet | 30115 | DFK\_V2\_MAINNET | 53935 | [https://subnets.avax.network/defi-kingdoms/dfk-chain/rpc](https://subnets.avax.network/defi-kingdoms/dfk-chain/rpc) | | Dinari Mainnet | mainnet | 30385 | DINARI\_V2\_MAINNET | 202110 | [https://subnets.avax.network/dinari/mainnet/rpc](https://subnets.avax.network/dinari/mainnet/rpc) | | DM2 Verse Mainnet | mainnet | 30315 | DM2VERSE\_V2\_MAINNET | 68770 | [https://rpc.dm2verse.dmm.com](https://rpc.dm2verse.dmm.com) | | Doma Mainnet | mainnet | 30393 | DOMA\_V2\_MAINNET | 97477 | [https://doma.drpc.org](https://doma.drpc.org) | | DOS Chain Mainnet | mainnet | 30149 | DOS\_V2\_MAINNET | 7979 | [https://main.doschain.com](https://main.doschain.com) | | EDU Chain Mainnet | mainnet | 30328 | EDU\_V2\_MAINNET | 41923 | [https://rpc.edu-chain.raas.gelato.cloud](https://rpc.edu-chain.raas.gelato.cloud) | | Ethereal Mainnet | mainnet | 30391 | ETHEREAL\_V2\_MAINNET | 1380270412 | (add your RPC) | | Ethereum Mainnet | mainnet | 30101 | ETHEREUM\_V2\_MAINNET | 1 | (add your RPC) | | Etherlink Mainnet | mainnet | 30292 | ETHERLINK\_V2\_MAINNET | 42793 | [https://node.mainnet.etherlink.com](https://node.mainnet.etherlink.com) | | EVM on Flow Mainnet | mainnet | 30336 | FLOW\_V2\_MAINNET | 747 | [https://mainnet.evm.nodes.onflow.org](https://mainnet.evm.nodes.onflow.org) | | Flare Mainnet | mainnet | 30295 | FLARE\_V2\_MAINNET | 14 | [https://flare-api.flare.network/ext/C/rpc](https://flare-api.flare.network/ext/C/rpc) | | Fraxtal Mainnet | mainnet | 30255 | FRAXTAL\_V2\_MAINNET | 252 | [https://rpc.frax.com](https://rpc.frax.com) | | Fuse Mainnet | mainnet | 30138 | FUSE\_V2\_MAINNET | 122 | [https://fuse-pokt.nodies.app](https://fuse-pokt.nodies.app) | | Gate Layer Mainnet | mainnet | 30389 | GATELAYER\_V2\_MAINNET | 10088 | [https://gatelayer-mainnet.gatenode.cc](https://gatelayer-mainnet.gatenode.cc) | | Gensyn Mainnet | mainnet | 30412 | GENSYN\_V2\_MAINNET | 685689 | (add your RPC) | | Gnosis Mainnet | mainnet | 30145 | GNOSIS\_V2\_MAINNET | 100 | [https://rpc.gnosischain.com](https://rpc.gnosischain.com) | | Goat Mainnet | mainnet | 30361 | GOAT\_V2\_MAINNET | 2345 | [https://rpc.goat.network](https://rpc.goat.network) | | Gravity Mainnet | mainnet | 30294 | GRAVITY\_V2\_MAINNET | 1625 | [https://rpc.gravity.xyz](https://rpc.gravity.xyz) | | Gunz Mainnet | mainnet | 30371 | GUNZ\_V2\_MAINNET | 43419 | [https://rpc.gunzchain.io/ext/bc/2M47TxWHGnhNtq6pM5zPXdATBtuqubxn5EPFgFmEawCQr9WFML/rpc](https://rpc.gunzchain.io/ext/bc/2M47TxWHGnhNtq6pM5zPXdATBtuqubxn5EPFgFmEawCQr9WFML/rpc) | | Harmony Mainnet | mainnet | 30116 | HARMONY\_V2\_MAINNET | 1666600000 | [https://api.s0.t.hmny.io](https://api.s0.t.hmny.io) | | Hedera Mainnet | mainnet | 30316 | HEDERA\_V2\_MAINNET | 295 | [https://mainnet.hashio.io/api](https://mainnet.hashio.io/api) | | Hemi Mainnet | mainnet | 30329 | HEMI\_V2\_MAINNET | 43111 | [https://rpc.hemi.network/rpc](https://rpc.hemi.network/rpc) | | Horizen Mainnet | mainnet | 30399 | HORIZEN\_V2\_MAINNET | 26514 | [https://horizen.calderachain.xyz/http](https://horizen.calderachain.xyz/http) | | Hubble Mainnet | mainnet | 30182 | HUBBLE\_V2\_MAINNET | 1992 | [https://sanko-arb-sepolia.rpc.caldera.xyz/http](https://sanko-arb-sepolia.rpc.caldera.xyz/http) | | Humanity Mainnet | mainnet | 30382 | HUMANITY\_V2\_MAINNET | 6985385 | [https://humanity-mainnet.g.alchemy.com/public](https://humanity-mainnet.g.alchemy.com/public) | | HyperEVM Mainnet | mainnet | 30367 | HYPERLIQUID\_V2\_MAINNET | 999 | [https://hyperliquid.rpc.blxrbdn.com](https://hyperliquid.rpc.blxrbdn.com) | | Injective EVM Mainnet | mainnet | 30394 | INJECTIVEEVM\_V2\_MAINNET | 1776 | [https://injectiveevm-rpc.polkachu.com](https://injectiveevm-rpc.polkachu.com) | | Ink Mainnet | mainnet | 30339 | INK\_V2\_MAINNET | 57073 | [https://rpc-gel.inkonchain.com](https://rpc-gel.inkonchain.com) | | IOTA EVM Mainnet | mainnet | 30284 | IOTA\_V2\_MAINNET | 8822 | [https://rpc.ankr.com/iota\_evm](https://rpc.ankr.com/iota_evm) | | Irys Mainnet | mainnet | 30408 | IRYS\_V2\_MAINNET | 3282 | [https://mainnet-beta-rpc-2.irys.xyz/v1/execution-rpc](https://mainnet-beta-rpc-2.irys.xyz/v1/execution-rpc) | | Japan Open Chain Mainnet | mainnet | 30285 | JOC\_V2\_MAINNET | 81 | [https://rpc-1.japanopenchain.org:8545](https://rpc-1.japanopenchain.org:8545) | | Kaia Mainnet (formerly Klaytn) | mainnet | 30150 | KLAYTN\_V2\_MAINNET | 8217 | [https://public-en.node.kaia.io](https://public-en.node.kaia.io) | | Katana | mainnet | 30375 | KATANA\_V2\_MAINNET | 747474 | [https://rpc.katana.network](https://rpc.katana.network) | | Kava Mainnet | mainnet | 30177 | KAVA\_V2\_MAINNET | 2222 | [https://evm.kava.io](https://evm.kava.io) | | Kite Mainnet | mainnet | 30406 | KITE\_V2\_MAINNET | 2366 | [https://rpc.gokite.ai](https://rpc.gokite.ai) | | Lens Mainnet | mainnet | 30373 | LENS\_V2\_MAINNET | 232 | [https://rpc.lens.xyz](https://rpc.lens.xyz) | | Lightlink Mainnet | mainnet | 30309 | LIGHTLINK\_V2\_MAINNET | 1890 | [https://replicator.phoenix.lightlink.io/rpc/v1](https://replicator.phoenix.lightlink.io/rpc/v1) | | Linea Mainnet | mainnet | 30183 | ZKCONSENSYS\_V2\_MAINNET | 59144 | [https://rpc.linea.build](https://rpc.linea.build) | | Lisk Mainnet | mainnet | 30321 | LISK\_V2\_MAINNET | 1135 | [https://rpc.api.lisk.com](https://rpc.api.lisk.com) | | Manta Pacific Mainnet | mainnet | 30217 | MANTA\_V2\_MAINNET | 169 | [https://pacific-rpc.manta.network/http](https://pacific-rpc.manta.network/http) | | Mantle Mainnet | mainnet | 30181 | MANTLE\_V2\_MAINNET | 5000 | [https://rpc.mantle.xyz](https://rpc.mantle.xyz) | | MegaETH Mainnet | mainnet | 30398 | MEGAETH\_V2\_MAINNET | 4326 | (add your RPC) | | Merlin Mainnet | mainnet | 30266 | MERLIN\_V2\_MAINNET | 4200 | [https://merlin.drpc.org](https://merlin.drpc.org) | | Meter Mainnet | mainnet | 30176 | METER\_V2\_MAINNET | 82 | [https://rpc.meter.io](https://rpc.meter.io) | | Metis Mainnet | mainnet | 30151 | METIS\_V2\_MAINNET | 1088 | [https://andromeda.metis.io/?owner=1088](https://andromeda.metis.io/?owner=1088) | | Mode Mainnet | mainnet | 30260 | MODE\_V2\_MAINNET | 34443 | [https://mainnet.mode.network](https://mainnet.mode.network) | | Monad Mainnet | mainnet | 30390 | MONAD\_V2\_MAINNET | 143 | [https://rpc1.monad.xyz](https://rpc1.monad.xyz) | | Moonbeam Mainnet | mainnet | 30126 | MOONBEAM\_V2\_MAINNET | 1284 | [https://rpc.api.moonbeam.network](https://rpc.api.moonbeam.network) | | Moonriver Mainnet | mainnet | 30167 | MOONRIVER\_V2\_MAINNET | 1285 | [https://rpc.api.moonriver.moonbeam.network](https://rpc.api.moonriver.moonbeam.network) | | Morph Mainnet | mainnet | 30322 | MORPH\_V2\_MAINNET | 2818 | [https://rpc.morphl2.io](https://rpc.morphl2.io) | | Near Aurora Mainnet | mainnet | 30211 | AURORA\_V2\_MAINNET | 1313161554 | [https://mainnet.aurora.dev](https://mainnet.aurora.dev) | | Neo X Mainnet | mainnet | 30414 | NEOX\_V2\_MAINNET | 47763 | (add your RPC) | | Nexera Mainnet | mainnet | 30395 | NEXERA\_V2\_MAINNET | 7208 | [https://rpc.nexera.network](https://rpc.nexera.network) | | Nibiru Mainnet | mainnet | 30369 | NIBIRU\_V2\_MAINNET | 6900 | [https://evm-rpc.nibiru.fi](https://evm-rpc.nibiru.fi) | | opBNB Mainnet | mainnet | 30202 | OPBNB\_V2\_MAINNET | 204 | [https://opbnb-mainnet-rpc.bnbchain.org](https://opbnb-mainnet-rpc.bnbchain.org) | | OpenLedger Mainnet | mainnet | 30392 | OPENLEDGER\_V2\_MAINNET | 1612 | [https://rpc.openledger.xyz](https://rpc.openledger.xyz) | | Optimism Mainnet | mainnet | 30111 | OPTIMISM\_V2\_MAINNET | 10 | [https://mainnet.optimism.io](https://mainnet.optimism.io) | | Orderly Mainnet | mainnet | 30213 | ORDERLY\_V2\_MAINNET | 291 | [https://rpc.orderly.network](https://rpc.orderly.network) | | Otherworld Space Mainnet | mainnet | 30341 | SPACE\_V2\_MAINNET | 8227 | [https://subnets.avax.network/space/mainnet/rpc](https://subnets.avax.network/space/mainnet/rpc) | | Peaq Mainnet | mainnet | 30302 | PEAQ\_V2\_MAINNET | 3338 | [https://quicknode3.peaq.xyz](https://quicknode3.peaq.xyz) | | Pharos Mainnet | mainnet | 30407 | PHAROS\_V2\_MAINNET | 1672 | (add your RPC) | | Plasma Mainnet | mainnet | 30383 | PLASMA\_V2\_MAINNET | 9745 | [https://rpc.plasma.to](https://rpc.plasma.to) | | Plume Mainnet | mainnet | 30370 | PLUMEPHOENIX\_V2\_MAINNET | 98866 | [https://rpc.plume.org](https://rpc.plume.org) | | Polygon Mainnet | mainnet | 30109 | POLYGON\_V2\_MAINNET | 137 | [https://polygon.drpc.org](https://polygon.drpc.org) | | Rari Chain Mainnet | mainnet | 30235 | RARIBLE\_V2\_MAINNET | 1380012617 | [https://mainnet.rpc.rarichain.org/http](https://mainnet.rpc.rarichain.org/http) | | Rayls Mainnet | mainnet | 30415 | RAYLS\_V2\_MAINNET | 72957 | (add your RPC) | | re.al Mainnet | mainnet | 30237 | REAL\_V2\_MAINNET | 111188 | [https://rpc.realforreal.gelato.digital](https://rpc.realforreal.gelato.digital) | | Redbelly Mainnet | mainnet | 30402 | REDBELLY\_V2\_MAINNET | 151 | [https://governors.mainnet.redbelly.network](https://governors.mainnet.redbelly.network) | | Reya Mainnet | mainnet | 30313 | REYA\_V2\_MAINNET | 1729 | [https://rpc.reya.network](https://rpc.reya.network) | | Rise Mainnet | mainnet | 30401 | RISE\_V2\_MAINNET | 4153 | (add your RPC) | | Robinhood Chain Mainnet | mainnet | 30416 | ROBINHOOD\_V2\_MAINNET | 4663 | (add your RPC) | | Rootstock Mainnet | mainnet | 30333 | ROOTSTOCK\_V2\_MAINNET | 30 | [https://mycrypto.rsk.co](https://mycrypto.rsk.co) | | Scroll Mainnet | mainnet | 30214 | SCROLL\_V2\_MAINNET | 534352 | [https://rpc.scroll.io](https://rpc.scroll.io) | | Sei Mainnet | mainnet | 30280 | SEI\_V2\_MAINNET | 1329 | [https://evm-rpc.sei-apis.com](https://evm-rpc.sei-apis.com) | | Shimmer Mainnet | mainnet | 30230 | SHIMMER\_V2\_MAINNET | 148 | [https://json-rpc.evm.shimmer.network](https://json-rpc.evm.shimmer.network) | | Silicon Mainnet | mainnet | 30379 | SILICON\_V2\_MAINNET | 2355 | [https://silicon-mainnet.nodeinfra.com](https://silicon-mainnet.nodeinfra.com) | | Skale Mainnet | mainnet | 30273 | SKALE\_V2\_MAINNET | 2046399126 | [https://mainnet.skalenodes.com/v1/elated-tan-skat](https://mainnet.skalenodes.com/v1/elated-tan-skat) | | Somnia Mainnet | mainnet | 30380 | SOMNIA\_V2\_MAINNET | 5031 | (add your RPC) | | Soneium Mainnet | mainnet | 30340 | SONEIUM\_V2\_MAINNET | 1868 | [https://rpc.soneium.org](https://rpc.soneium.org) | | Sonic Mainnet | mainnet | 30332 | SONIC\_V2\_MAINNET | 146 | [https://rpc.soniclabs.com](https://rpc.soniclabs.com) | | Sophon Mainnet | mainnet | 30334 | SOPHON\_V2\_MAINNET | 50104 | [https://rpc.sophon.xyz](https://rpc.sophon.xyz) | | Stable Mainnet | mainnet | 30396 | STABLE\_V2\_MAINNET | 988 | (add your RPC) | | Story Mainnet | mainnet | 30364 | STORY\_V2\_MAINNET | 1514 | [https://rpc.ankr.com/story\_mainnet](https://rpc.ankr.com/story_mainnet) | | Subtensor EVM Mainnet | mainnet | 30374 | SUBTENSOREVM\_V2\_MAINNET | 964 | [https://bittensor-lite-public.nodies.app](https://bittensor-lite-public.nodies.app) | | Superposition Mainnet | mainnet | 30327 | SUPERPOSITION\_V2\_MAINNET | 55244 | [https://rpc.superposition.so](https://rpc.superposition.so) | | Tac | mainnet | 30377 | TAC\_V2\_MAINNET | 239 | [https://rpc.ankr.com/tac](https://rpc.ankr.com/tac) | | Taiko Mainnet | mainnet | 30290 | TAIKO\_V2\_MAINNET | 167000 | [https://rpc.taiko.xyz](https://rpc.taiko.xyz) | | TelosEVM Mainnet | mainnet | 30199 | TELOS\_V2\_MAINNET | 40 | [https://rpc.telos.net](https://rpc.telos.net) | | Tempo Mainnet | mainnet | 30410 | TEMPO\_V2\_MAINNET | 4217 | (add your RPC) | | Tenet Mainnet | mainnet | 30173 | TENET\_V2\_MAINNET | 1559 | [https://rpc.ankr.com/tenet\_evm](https://rpc.ankr.com/tenet_evm) | | Tiltyard Mainnet | mainnet | 30238 | TILTYARD\_V2\_MAINNET | 710420 | [https://subnets.avax.network/tiltyard/mainnet/rpc](https://subnets.avax.network/tiltyard/mainnet/rpc) | | Unichain Mainnet | mainnet | 30320 | UNICHAIN\_V2\_MAINNET | 130 | [https://unichain.api.onfinality.io/public](https://unichain.api.onfinality.io/public) | | Vana Mainnet | mainnet | 30330 | ISLANDER\_V2\_MAINNET | 1480 | [https://rpc.vana.org](https://rpc.vana.org) | | Viction Mainnet | mainnet | 30196 | TOMO\_V2\_MAINNET | 88 | [https://viction.blockpi.network/v1/rpc/public](https://viction.blockpi.network/v1/rpc/public) | | Worldchain Mainnet | mainnet | 30319 | WORLDCHAIN\_V2\_MAINNET | 480 | [https://worldchain.drpc.org](https://worldchain.drpc.org) | | X Layer Mainnet | mainnet | 30274 | XLAYER\_V2\_MAINNET | 196 | [https://xlayerrpc.okx.com](https://xlayerrpc.okx.com) | | Xai Mainnet | mainnet | 30236 | XAI\_V2\_MAINNET | 660279 | [https://xai-chain.net/rpc](https://xai-chain.net/rpc) | | XDC Mainnet | mainnet | 30365 | XDC\_V2\_MAINNET | 50 | [https://rpc.ankr.com/xdc](https://rpc.ankr.com/xdc) | | XPLA Mainnet | mainnet | 30216 | XPLA\_V2\_MAINNET | 37 | [https://dimension-evm-rpc.xpla.dev](https://dimension-evm-rpc.xpla.dev) | | Zama Mainnet | mainnet | 30397 | ZAMA\_V2\_MAINNET | 261131 | (add your RPC) | | Zircuit Mainnet | mainnet | 30303 | ZIRCUIT\_V2\_MAINNET | 48900 | [https://mainnet.zircuit.com](https://mainnet.zircuit.com) | | zkSync Era Mainnet | mainnet | 30165 | ZKSYNC\_V2\_MAINNET | 324 | [https://mainnet.era.zksync.io](https://mainnet.era.zksync.io) | | zkVerify Mainnet | mainnet | 30386 | ZKVERIFY\_V2\_MAINNET | 1408 | [https://vflow-rpc.zkverify.io](https://vflow-rpc.zkverify.io) | | Zora Mainnet | mainnet | 30195 | ZORA\_V2\_MAINNET | 7777777 | [https://rpc.zora.energy](https://rpc.zora.energy) | | Arbitrum Sepolia Testnet | testnet | 40231 | ARBSEP\_V2\_TESTNET | 421614 | [https://sepolia-rollup.arbitrum.io/rpc](https://sepolia-rollup.arbitrum.io/rpc) | | Avalanche Fuji Testnet | testnet | 40106 | AVALANCHE\_V2\_TESTNET | 43113 | [https://api.avax-test.network/ext/bc/C/rpc](https://api.avax-test.network/ext/bc/C/rpc) | | Base Sepolia Testnet | testnet | 40245 | BASESEP\_V2\_TESTNET | 84532 | [https://base-sepolia-rpc.publicnode.com](https://base-sepolia-rpc.publicnode.com) | | Berachain Bepolia Testnet | testnet | 40371 | BEPOLIA\_V2\_TESTNET | 80069 | [https://bepolia.rpc.berachain.com](https://bepolia.rpc.berachain.com) | | BNB Smart Chain (BSC) Testnet | testnet | 40102 | BSC\_V2\_TESTNET | 97 | [https://data-seed-prebsc-2-s1.bnbchain.org:8545](https://data-seed-prebsc-2-s1.bnbchain.org:8545) | | Ethereum Holesky Testnet | testnet | 40217 | HOLESKY\_V2\_TESTNET | 17000 | [https://holesky.drpc.org](https://holesky.drpc.org) | | Ethereum Sepolia Testnet | testnet | 40161 | SEPOLIA\_V2\_TESTNET | 11155111 | [https://ethereum-sepolia-rpc.publicnode.com](https://ethereum-sepolia-rpc.publicnode.com) | | EVM on Flow Testnet | testnet | 40351 | FLOW\_V2\_TESTNET | 545 | [https://testnet.evm.nodes.onflow.org](https://testnet.evm.nodes.onflow.org) | | Flare Testnet | testnet | 40294 | FLARE\_V2\_TESTNET | 114 | [https://coston2.enosys.global/ext/C/rpc](https://coston2.enosys.global/ext/C/rpc) | | Hedera Testnet | testnet | 40285 | HEDERA\_V2\_TESTNET | 296 | [https://296.rpc.thirdweb.com](https://296.rpc.thirdweb.com) | | Hoodi Testnet | testnet | 40449 | HOODI\_V2\_TESTNET | 560048 | [https://0xrpc.io/hoodi](https://0xrpc.io/hoodi) | | HyperEVM Testnet | testnet | 40362 | HYPERLIQUID\_V2\_TESTNET | 998 | [https://rpc.hyperliquid-testnet.xyz/evm](https://rpc.hyperliquid-testnet.xyz/evm) | | Optimism Sepolia Testnet | testnet | 40232 | OPTSEP\_V2\_TESTNET | 11155420 | [https://optimism-sepolia-public.nodies.app](https://optimism-sepolia-public.nodies.app) | | Polygon Amoy Testnet | testnet | 40267 | AMOY\_V2\_TESTNET | 80002 | [https://rpc-amoy.polygon.technology](https://rpc-amoy.polygon.technology) | | Tempo Testnet | testnet | 40439 | TEMPODEV1\_V2\_TESTNET | 42429 | [https://tempo-testnet.drpc.org](https://tempo-testnet.drpc.org) | | Unichain Testnet | testnet | 40333 | UNICHAIN\_V2\_TESTNET | 1301 | [https://sepolia.unichain.org](https://sepolia.unichain.org) | | zkSync Sepolia Testnet | testnet | 40305 | ZKSYNCSEP\_V2\_TESTNET | 300 | [https://sepolia.era.zksync.dev](https://sepolia.era.zksync.dev) | ## Installation Below, you can find instructions for installing the OFT contract: ### OFT in a new project To start using LayerZero OFT contracts in a new project, use the LayerZero CLI tool, [**create-lz-oapp**](../../../get-started/create-lz-oapp/start). The CLI tool allows developers to create any omnichain application in \<4 minutes! Get started by running the following from your command line: ```bash wrap theme={null} npx create-lz-oapp@latest --example oft ``` This will create an example repository containing both the Hardhat and Foundry frameworks, LayerZero development utilities, as well as the **OFT contract package** pre-installed. ### OFT in an existing project To use LayerZero contracts in an existing project, you can install the **OFT package** directly: ```bash wrap theme={null} npm install @layerzerolabs/oft-evm ``` ```bash wrap theme={null} yarn add @layerzerolabs/oft-evm ``` ```bash wrap theme={null} pnpm add @layerzerolabs/oft-evm ``` ```bash wrap theme={null} forge init ``` ```bash wrap theme={null} forge install layerzero-labs/devtools forge install layerzero-labs/LayerZero-v2 forge install OpenZeppelin/openzeppelin-contracts git submodule add https://github.com/GNSPS/solidity-bytes-utils.git lib/solidity-bytes-utils ``` Then add to your `foundry.toml` under `[profile.default]`: ```toml wrap theme={null} [profile.default] src = "src" out = "out" libs = ["lib"] remappings = [ '@layerzerolabs/oft-evm/=lib/devtools/packages/oft-evm/', '@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/', '@layerzerolabs/lz-evm-protocol-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/protocol', '@layerzerolabs/lz-evm-messagelib-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/messagelib', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', 'solidity-bytes-utils/=lib/solidity-bytes-utils/', ] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options ``` LayerZero contracts work with both [**OpenZeppelin V5**](https://docs.openzeppelin.com/contracts/5.x/access-control#ownership-and-ownable) and V4 contracts. Specify your desired version in your project's `package.json`: ```typescript wrap theme={null} "resolutions": { "@openzeppelin/contracts": "^5.0.1", } ``` ## Custom OFT Contract To build your own omnichain token contract, inherit from `OFT.sol` or `OFTAdapter.sol` depending on whether you're creating a new token or bridging an existing one. Below is a complete example showing the key pieces you need to implement: ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol"; /// @notice OFT is an ERC-20 token that extends the OFTCore contract. contract MyOFT is OFT { constructor( string memory _name, string memory _symbol, address _lzEndpoint, address _owner ) OFT(_name, _symbol, _lzEndpoint, _owner) Ownable(_owner) {} } ``` Remember to add the ERC20 `_mint` method either in the constructor or as a protected `mint` function before deploying. This contract provides a complete omnichain ERC20 implementation. The OFT automatically handles: * **Burning tokens** on the source chain when sending * **Minting tokens** on the destination chain when receiving * **Decimal precision** conversion between different chains * **Unified supply** management across all networks ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { OFTAdapter } from "@layerzerolabs/oft-evm/contracts/OFTAdapter.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; /// @notice OFTAdapter uses a deployed ERC-20 token and SafeERC20 to interact with the OFTCore contract. contract MyOFTAdapter is OFTAdapter { constructor( address _token, address _lzEndpoint, address _owner ) OFTAdapter(_token, _lzEndpoint, _owner) Ownable(_owner) {} } ``` **There can only be one OFT Adapter lockbox in your omnichain deployment.** Multiple adapters break unified liquidity and can cause permanent token loss due to insufficient destination supply. The OFT Adapter enables existing ERC20 tokens to become omnichain without code changes. The adapter: * **Locks tokens** in the adapter contract when sending * **Unlocks tokens** from the adapter when receiving * **Requires approval** of the underlying token for transfers * **Maintains the original token** contract unchanged **Fee-on-transfer and rebasing tokens are not supported.** The OFT `_debit` / `_credit` accounting assumes lossless ERC20 transfers (debit amount equals credit amount). To support such tokens, override `_debit` and `_credit` to reconcile against actual balance changes. ### Constructor * Pass the Endpoint V2 address and owner address into the base contracts. * `OFT(_name, _symbol, _lzEndpoint, _owner)` binds your contract to the local LayerZero Endpoint V2 and registers the delegate * `Ownable(_owner)` makes `_owner` the only address that can change configurations (such as peers, enforced options, and delegate) * After deployment, the owner can call: * `setConfig(...)` to adjust library or DVN parameters * `setSendLibrary(...)` and `setReceiveLibrary(...)` to override default libraries * `setPeer(...)` to whitelist remote OFT addresses * `setDelegate(...)` to assign a different delegate address * `setEnforcedOptions(...)` to set mandatory execution options ## Deployment and Wiring After you finish writing and testing your `MyOFT` contract, follow these steps to deploy it on each network and wire up the messaging stack. We **strongly recommend** using the LayerZero CLI tool to manage your configurations. Our config generator simplifies access to all available deployments across networks and is the preferred method for crosschain messaging. See the [**CLI Guide**](../../../get-started/create-lz-oapp/start) for examples and how to use it in your project. ### 1. Deploy Your OFT Contract Deploy `MyOFT` on each chain using either the LayerZero CLI (recommended) or manual deployment scripts. After running `pnpm compile` at the root level of your example repo, you can deploy your contracts. #### Network Configuration Before using the CLI, you'll need to configure your networks in `hardhat.config.ts` with LayerZero [Endpoint IDs (EIDs)](/v2/concepts/glossary#endpoint-id) and declare an RPC URL in your `.env` or directly in the config file: ```typescript wrap theme={null} // hardhat.config.ts import { EndpointId } from '@layerzerolabs/lz-definitions' // ... rest of hardhat config omitted for brevity networks: { 'optimism-sepolia-testnet': { // highlight-next-line eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, 'arbitrum-sepolia-testnet': { // highlight-next-line eid: EndpointId.ARBSEP_V2_TESTNET, url: process.env.RPC_URL_ARB_SEPOLIA || 'https://arbitrum-sepolia.gateway.tenderly.co', accounts, }, } ``` The key addition to a standard `hardhat.config.ts` is the inclusion of LayerZero Endpoint IDs (`eid`) for each network. Check the [Deployments](../../../deployments/deployed-contracts) section for all available endpoint IDs. The LayerZero CLI provides automated deployment with built-in endpoint detection based on your `hardhat.config.ts` networks object: ```bash wrap theme={null} # Deploy using interactive prompts npx hardhat lz:deploy ``` The CLI will prompt you to: 1. **Select chains to deploy to:** ```bash wrap theme={null} ? Which networks would you like to deploy? › ◉ fuji ◉ amoy ◉ sepolia ``` 2. **Choose deploy script tags:** ```bash wrap theme={null} ? Which deploy script tags would you like to use? › MyOFT ``` 3. **Confirm deployment:** ```bash wrap theme={null} ✔ Do you want to continue? … yes Network: amoy Deployer: 0x0000000000000000000000000000000000000000 Network: sepolia Deployer: 0x0000000000000000000000000000000000000000 Deployed contract: MyOApp, network: amoy, address: 0x0000000000000000000000000000000000000000 Deployed contract: MyOApp, network: sepolia, address: 0x0000000000000000000000000000000000000000 ``` The CLI automatically: * Detects the correct LayerZero Endpoint V2 address for each chain * Deploys your OApp contract with proper constructor arguments * Generates deployment artifacts in `./deployments/` folder * Creates network-specific deployment files (e.g., `deployments/sepolia/MyOApp.json`) For manual deployment using Foundry, create a deployment script that handles endpoint addresses: ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { MyOApp } from "../contracts/MyOApp.sol"; contract DeployOApp is Script { function run() external { // Replace these env vars with your own values address endpoint = vm.envAddress("ENDPOINT_ADDRESS"); address owner = vm.envAddress("OWNER_ADDRESS"); vm.startBroadcast(vm.envUint("PRIVATE_KEY")); MyOApp oapp = new MyOApp(endpoint, owner); vm.stopBroadcast(); console.log("MyOApp deployed to:", address(oapp)); } } ``` Run the deployment script: ```bash wrap theme={null} # Deploy to testnet forge script script/DeployOApp.s.sol --rpc-url $RPC_URL --broadcast --verify # Deploy to multiple chains forge script script/DeployOApp.s.sol --rpc-url $ETHEREUM_RPC --broadcast --verify forge script script/DeployOApp.s.sol --rpc-url $POLYGON_RPC --broadcast --verify ``` You'll need to set the correct LayerZero Endpoint V2 addresses for each chain in your environment variables. Check the [Deployments](../../../deployments/deployed-contracts) section for endpoint addresses. ### 2. Wire Messaging Libraries and Configurations Once your contracts are onchain, you must set up send/receive libraries and DVN/Executor settings so crosschain messages flow correctly. **Production deployments should use multiple required DVNs from independent operators.** A single-DVN configuration means a compromise of that one verifier results in unrestricted forged messages on the pathway. See the [Integration Checklist](/v2/tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway) for production DVN guidance. The LayerZero CLI automatically handles all wiring via a single configuration file and command: #### Configuration File In your project root, you can find a `layerzero.config.ts` file: ```typescript wrap theme={null} import {EndpointId} from '@layerzerolabs/lz-definitions'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; import {OAppEnforcedOption, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; // This contract object defines the OApp deployment on Optimism Sepolia testnet // The config references the contract deployment from your ./deployments folder const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', }; const arbitrumContract: OmniPointHardhat = { eid: EndpointId.ARBSEP_V2_TESTNET, contractName: 'MyOFT', }; // For this example's simplicity, we will use the same enforced options values for sending to all chains // For production, you should ensure `gas` is set to the correct value through profiling the gas usage of calling OApp._lzReceive(...) on the destination chain // To learn more, read https://docs.layerzero.network/v2/concepts/applications/oapp-standard#execution-options-and-enforced-settings const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 80000, value: 0, }, ]; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> Arbitrum // With the config generator, pathways declared are automatically bidirectional // i.e. if you declare A,B there's no need to declare B,A const pathways: TwoWayConfig[] = [ [ optimismContract, // Chain A contract arbitrumContract, // Chain B contract // Replace with a non-LayerZero-Labs DVN provider for this pathway. // See /v2/deployments/dvn-addresses for the providers available on each chain. [['LayerZero Labs', ''], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [1, 1], // [A to B confirmations, B to A confirmations] — adjust per pathway; production deployments typically use larger values [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: arbitrumContract}], connections, }; } ``` Make sure your contract object's `contractName` matches the named deployment file for the network under `./deployments/`. #### Wire Everything Run a single command to configure all pathways: ```bash wrap theme={null} npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` This automatically handles: * Fetching the necessary contract addresses for each network from metadata * Setting send and receive libraries * Configuring DVNs and Executors * Setting up peers between contracts * Applying enforced options * All bidirectional pathways in your config For manual configuration using Foundry scripts, follow these steps: #### Environment Setup Here's a comprehensive `.env.example` file showing all the environment variables needed for the different configuration scripts: ```bash wrap theme={null} # Common variables used across scripts ENDPOINT_ADDRESS=0x... # LayerZero Endpoint V2 address OAPP_ADDRESS=0x... # Your OApp contract address SIGNER=0x... # Address with permissions to configure/send # Library Configuration (SetLibraries.s.sol) SEND_LIB_ADDRESS=0x... # SendUln302 address RECEIVE_LIB_ADDRESS=0x... # ReceiveUln302 address DST_EID=30101 # Destination chain EID SRC_EID=30110 # Source chain EID GRACE_PERIOD=0 # Grace period for library switch (0 for immediate) # Send Config (SetSendConfig.s.sol) SOURCE_ENDPOINT_ADDRESS=0x... # Chain A Endpoint address SENDER_OAPP_ADDRESS=0x... # OApp on Chain A REMOTE_EID=30101 # Endpoint ID for Chain B # Peer Configuration (SetPeers.s.sol) CHAIN1_EID=30101 # First chain EID CHAIN1_PEER=0x... # OApp address on first chain CHAIN2_EID=30110 # Second chain EID CHAIN2_PEER=0x... # OApp address on second chain CHAIN3_EID=30111 # Third chain EID CHAIN3_PEER=0x... # OApp address on third chain # Message Sending (SendMessage.s.sol) MESSAGE="Hello World" # Message to send crosschain ``` #### 2.1 Set Send and Receive Libraries 1. **Choose your libraries** (addresses of deployed MessageLib contracts). For standard crosschain messaging, you should use `SendUln302.sol` for `setSendLibrary(...)` and `ReceiveUln302.sol` for `setReceiveLibrary(...)`. You can find the deployments for these contracts under the [Deployments](../../../deployments/deployed-contracts) section. 2. Call `setSendLibrary(oappAddress, dstEid, sendLibAddress)` on the Endpoint. 3. Call `setReceiveLibrary(oappAddress, srcEid, receiveLibAddress, gracePeriod)` on the Endpoint. ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; /// @title LayerZero Library Configuration Script /// @notice Sets up send and receive libraries for OApp messaging contract SetLibraries is Script { function run() external { // Load environment variables address endpoint = vm.envAddress("ENDPOINT_ADDRESS"); // LayerZero Endpoint address address oapp = vm.envAddress("OAPP_ADDRESS"); // Your OApp contract address address signer = vm.envAddress("SIGNER"); // Address with permissions to configure // Library addresses address sendLib = vm.envAddress("SEND_LIB_ADDRESS"); // SendUln302 address address receiveLib = vm.envAddress("RECEIVE_LIB_ADDRESS"); // ReceiveUln302 address // Chain configurations uint32 dstEid = uint32(vm.envUint("DST_EID")); // Destination chain EID uint32 srcEid = uint32(vm.envUint("SRC_EID")); // Source chain EID uint32 gracePeriod = uint32(vm.envUint("GRACE_PERIOD")); // Grace period for library switch vm.startBroadcast(signer); // Set send library for outbound messages ILayerZeroEndpointV2(endpoint).setSendLibrary( oapp, // OApp address dstEid, // Destination chain EID sendLib // SendUln302 address ); // Set receive library for inbound messages ILayerZeroEndpointV2(endpoint).setReceiveLibrary( oapp, // OApp address srcEid, // Source chain EID receiveLib, // ReceiveUln302 address gracePeriod // Grace period for library switch ); vm.stopBroadcast(); } } ``` You would need to set up your `.env` file with the appropriate values: ```env wrap theme={null} ENDPOINT_ADDRESS=0x... OAPP_ADDRESS=0x... SIGNER=0x... SEND_LIB_ADDRESS=0x... # SendUln302 address RECEIVE_LIB_ADDRESS=0x... # ReceiveUln302 address DST_EID=30101 SRC_EID=30110 GRACE_PERIOD=0 # Set to 0 for immediate switch, or block number for gradual migration ``` #### 2.2 Set Send Config and Receive Config If you need non-default DVN or Executor settings (block confirmations, required DVNs, max message size, etc.), call `setConfig(...)` next. To see defaults, use `getConfig(...)`. **Send Config (A → B):** The send config is set on the source chain (Chain A) and applies to messages being sent from Chain A to Chain B. This config determines the DVN and Executor settings for outbound messages leaving Chain A and destined for Chain B. You must call `setConfig` on the Endpoint contract on Chain A, specifying the remote Endpoint ID for Chain B and the appropriate SendLib address for the A → B pathway. ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { ILayerZeroEndpointV2, SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; import { ExecutorConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBase.sol"; /// @title LayerZero Send Configuration Script (A → B) /// @notice Defines and applies ULN (DVN) + Executor configs for cross‑chain messages sent from Chain A to Chain B via LayerZero Endpoint V2. contract SetSendConfig is Script { uint32 constant EXECUTOR_CONFIG_TYPE = 1; uint32 constant ULN_CONFIG_TYPE = 2; /// @notice Broadcasts transactions to set both Send ULN and Executor configurations for messages sent from Chain A to Chain B function run() external { address endpoint = vm.envAddress("SOURCE_ENDPOINT_ADDRESS"); // Chain A Endpoint address oapp = vm.envAddress("SENDER_OAPP_ADDRESS"); // OApp on Chain A uint32 eid = uint32(vm.envUint("REMOTE_EID")); // Endpoint ID for Chain B address sendLib = vm.envAddress("SEND_LIB_ADDRESS"); // SendLib for A → B address signer = vm.envAddress("SIGNER"); /// @notice ULNConfig defines security parameters (DVNs + confirmation threshold) for A → B /// @notice Send config requests these settings to be applied to the DVNs and Executor for messages sent from A to B /// @dev 0 values will be interpretted as defaults, so to apply NIL settings, use: /// @dev uint8 internal constant NIL_DVN_COUNT = type(uint8).max; /// @dev uint64 internal constant NIL_CONFIRMATIONS = type(uint64).max; UlnConfig memory uln = UlnConfig({ confirmations: 15, // minimum block confirmations required on A before sending to B requiredDVNCount: 2, // number of DVNs required optionalDVNCount: type(uint8).max, // optional DVNs count, uint8 optionalDVNThreshold: 0, // optional DVN threshold requiredDVNs: [address(0x1111...), address(0x2222...)], // sorted list of required DVN addresses optionalDVNs: [] // sorted list of optional DVNs }); /// @notice ExecutorConfig sets message size limit + fee‑paying executor for A → B ExecutorConfig memory exec = ExecutorConfig({ maxMessageSize: 10000, // max bytes per crosschain message executor: address(0x3333...) // address that pays destination execution fees on B }); bytes memory encodedUln = abi.encode(uln); bytes memory encodedExec = abi.encode(exec); SetConfigParam[] memory params = new SetConfigParam[](2); params[0] = SetConfigParam(eid, EXECUTOR_CONFIG_TYPE, encodedExec); params[1] = SetConfigParam(eid, ULN_CONFIG_TYPE, encodedUln); vm.startBroadcast(signer); ILayerZeroEndpointV2(endpoint).setConfig(oapp, sendLib, params); // Set config for messages sent from A to B vm.stopBroadcast(); } } ``` **Receive Config (B ← A):** The receive config is set on the destination chain (Chain B) and applies to messages being received on Chain B from Chain A. This config determines the DVN settings for inbound messages arriving from Chain A. You must call `setConfig` on the Endpoint contract on Chain B, specifying the remote Endpoint ID for Chain A and the appropriate ReceiveLib address for the B ← A pathway. ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { ILayerZeroEndpointV2, SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; /// @title LayerZero Receive Configuration Script (B ← A) /// @notice Defines and applies ULN (DVN) config for inbound message verification on Chain B for messages received from Chain A via LayerZero Endpoint V2. contract SetReceiveConfig is Script { uint32 constant RECEIVE_CONFIG_TYPE = 2; function run() external { address endpoint = vm.envAddress("ENDPOINT_ADDRESS"); // Chain B Endpoint address oapp = vm.envAddress("OAPP_ADDRESS"); // OApp on Chain B uint32 eid = uint32(vm.envUint("REMOTE_EID")); // Endpoint ID for Chain A address receiveLib= vm.envAddress("RECEIVE_LIB_ADDRESS"); // ReceiveLib for B ← A address signer = vm.envAddress("SIGNER"); /// @notice UlnConfig controls verification threshold for incoming messages from A to B /// @notice Receive config enforces these settings have been applied to the DVNs for messages received from A /// @dev 0 values will be interpretted as defaults, so to apply NIL settings, use: /// @dev uint8 internal constant NIL_DVN_COUNT = type(uint8).max; /// @dev uint64 internal constant NIL_CONFIRMATIONS = type(uint64).max; UlnConfig memory uln = UlnConfig({ confirmations: 15, // min block confirmations from source (A) requiredDVNCount: 2, // required DVNs for message acceptance optionalDVNCount: type(uint8).max, // optional DVNs count optionalDVNThreshold: 0, // optional DVN threshold requiredDVNs: [address(0x1111...), address(0x2222...)], // sorted required DVNs optionalDVNs: [] // no optional DVNs }); bytes memory encodedUln = abi.encode(uln); SetConfigParam[] memory params = new SetConfigParam[](1); params[0] = SetConfigParam(eid, RECEIVE_CONFIG_TYPE, encodedUln); vm.startBroadcast(signer); ILayerZeroEndpointV2(endpoint).setConfig(oapp, receiveLib, params); // Set config for messages received on B from A vm.stopBroadcast(); } } ``` #### 2.3 Set Peers Once you've finished your **OApp Configuration** you can open the messaging channel and connect your OApp deployments by calling `setPeer`. A peer is required to be set for each EID (or network). Ideally an OApp (or OFT) will have multiple peers set where one and only one peer exists for one EID. The function takes 2 arguments: `_eid`, the destination endpoint ID for the chain our other OApp contract lives on, and `_peer`, the destination OApp contract address in `bytes32` format. ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { MyOApp } from "../contracts/MyOApp.sol"; /// @title LayerZero OApp Peer Configuration Script /// @notice Sets up peer connections between OApp deployments on different chains contract SetPeers is Script { function run() external { // Load environment variables address oapp = vm.envAddress("OAPP_ADDRESS"); // Your OApp contract address address signer = vm.envAddress("SIGNER"); // Address with owner permissions // Example: Set peers for different chains // Format: (chain EID, peer address in bytes32) (uint32 eid1, bytes32 peer1) = (uint32(vm.envUint("CHAIN1_EID")), bytes32(uint256(uint160(vm.envAddress("CHAIN1_PEER"))))); (uint32 eid2, bytes32 peer2) = (uint32(vm.envUint("CHAIN2_EID")), bytes32(uint256(uint160(vm.envAddress("CHAIN2_PEER"))))); (uint32 eid3, bytes32 peer3) = (uint32(vm.envUint("CHAIN3_EID")), bytes32(uint256(uint160(vm.envAddress("CHAIN3_PEER"))))); vm.startBroadcast(signer); // Set peers for each chain MyOApp(oapp).setPeer(eid1, peer1); MyOApp(oapp).setPeer(eid2, peer2); MyOApp(oapp).setPeer(eid3, peer3); vm.stopBroadcast(); } } ``` This function opens your OApp to start receiving messages from the messaging channel, meaning you should configure any application settings you intend on changing prior to calling `setPeer`. OApps need `setPeer` to be called correctly on both contracts to send messages. The peer address uses `bytes32` for handling non-EVM destination chains. If the peer has been set to an incorrect destination address, your messages will not be delivered and handled properly. If not resolved, users can potentially pay gas on source without any corresponding action on destination. You can confirm the peer address is the expected destination OApp address by viewing the `peers` mapping directly. #### 2.4 Set Enforced Options Enforced options allow the OApp owner to set mandatory execution parameters that will be applied to all messages of a specific type sent to a destination chain. These options are automatically combined with any caller-provided options when using `OAppOptionsType3`. **Why use enforced options?** * Ensure sufficient gas is always allocated for message execution on the destination * Enforce payment for additional services like PreCrime verification * Set consistent execution parameters across all users of your OApp * Prevent failed deliveries due to insufficient gas ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { MyOApp } from "../contracts/MyOApp.sol"; import { EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; /// @title LayerZero OApp Enforced Options Configuration Script /// @notice Sets enforced execution options for specific message types and destinations contract SetEnforcedOptions is Script { using OptionsBuilder for bytes; function run() external { // Load environment variables address oapp = vm.envAddress("OAPP_ADDRESS"); // Your OApp contract address address signer = vm.envAddress("SIGNER"); // Address with owner permissions // Destination chain configurations uint32 dstEid1 = uint32(vm.envUint("DST_EID_1")); // First destination EID uint32 dstEid2 = uint32(vm.envUint("DST_EID_2")); // Second destination EID // Message type (should match your contract's constant) uint16 SEND = 1; // Message type for sendString function // Build options using OptionsBuilder bytes memory options1 = OptionsBuilder.newOptions().addExecutorLzReceiveOption(80000, 0); bytes memory options2 = OptionsBuilder.newOptions().addExecutorLzReceiveOption(100000, 0); // Create enforced options array EnforcedOptionParam[] memory enforcedOptions = new EnforcedOptionParam[](2); // Set enforced options for first destination enforcedOptions[0] = EnforcedOptionParam({ eid: dstEid1, msgType: SEND, options: options1 }); // Set enforced options for second destination enforcedOptions[1] = EnforcedOptionParam({ eid: dstEid2, msgType: SEND, options: options2 }); vm.startBroadcast(signer); // Set enforced options on the OApp MyOApp(oapp).setEnforcedOptions(enforcedOptions); vm.stopBroadcast(); console.log("Enforced options set successfully!"); console.log("Destination 1 EID:", dstEid1, "Gas:", 80000); console.log("Destination 2 EID:", dstEid2, "Gas:", 100000); } } ``` **Environment variables needed:** ```env wrap theme={null} OAPP_ADDRESS=0x... # Your deployed MyOApp address SIGNER=0x... # Address with owner permissions DST_EID_1=30101 # First destination endpoint ID DST_EID_2=30110 # Second destination endpoint ID ``` **Run the script:** ```bash wrap theme={null} forge script script/SetEnforcedOptions.s.sol --rpc-url $RPC_URL --broadcast ``` Once set, these enforced options will be automatically applied when using `combineOptions()` in your send functions, ensuring consistent execution parameters across all messages.
## Usage Once deployed and wired, you can begin sending tokens across chains. ### Send tokens The OFT standard provides methods for quoting and sending tokens crosschain via the [IOFT interface](https://github.com/LayerZero-Labs/devtools/blob/main/packages/oft-evm/contracts/interfaces/IOFT.sol). #### quoteSend() - Get Transfer Fees #### quoteOFT() - Get Detailed Transfer Quote #### send() - Transfer Tokens #### How send() Works Under the Hood When you call `send()`, it triggers a chain of calls through the LayerZero protocol: 1. **OFT Contract** → Debits tokens 2. **LayerZero Endpoint** → Routes the message to your configured MessageLib 3. **Message Library (SendUln302)** → Requests verification/execution from configured DVNs/Executor 4. **Workers (DVNs + Executor)** → Quote their fees for verification and execution services 5. **Fee Aggregation** → Returns total `nativeFee` needed for the transfer When deploying an OFT, you choose your own trust assumptions for verification and execution: * **Send/Receive libraries** - Set the MessageLibs for your contract * **DVNs** - Select which DVNs verify your crosschain messages (at least one required) * **Enforced options OR caller options** - Provide gas settings (globally or per call), or transactions fail with `LZ_ULN_InvalidWorkerOptions` * **Peers** - Register destination OFT addresses for crosschain transfers **These requirements must be satisfied** before `send()` will work. They are configured during the "Deployment and Wiring" step above. ### Trust Decisions **Using Managed Applications** (e.g., Stargate): You trust the application team's selected DVNs and Executor configurations. **Deploying Your Own OFT**: You select your trusted DVNs and Executors, giving you full control over your security assumptions. ### Quote Freshness Call `quoteSend()` as close as possible to `send()` execution to avoid stale fee quotes. Fees can change due to: * Gas price fluctuations on source/destination chains * Price feed updates for crosschain gas estimation * DVN fee adjustments In production applications, quote and send in the same transaction or block when possible. The LayerZero CLI provides a convenient task for sending OFT tokens that automatically handles fee estimation and transaction execution. #### Using the Send Task The CLI includes a built-in `lz:oft:send` task that: 1. Finds your deployed OFT contract automatically 2. Quotes the gas cost using your OFT's `quoteSend()` function 3. Sends the tokens with the correct fee 4. Provides tracking links for the transaction **Basic usage:** ```bash wrap theme={null} npx hardhat lz:oft:send --src-eid 40232 --dst-eid 40231 --amount 1.5 --to 0x1234567890123456789012345678901234567890 ``` **Required Parameters:** * `--src-eid`: Source endpoint ID (e.g., 40232 for Optimism Sepolia) * `--dst-eid`: Destination endpoint ID (e.g., 40231 for Arbitrum Sepolia) * `--amount`: Amount to send in human readable units (e.g., "1.5") * `--to`: Recipient address (20-byte hex for EVM) **Optional Parameters:** * `--min-amount`: Minimum amount to receive for slippage protection (e.g., "1.4") * `--extra-options`: Additional gas units for lzReceive, lzCompose, or receiver address * `--compose-msg`: Arbitrary bytes message to deliver alongside the OFT * `--oft-address`: Override the source OFT address (if not using deployment artifacts) **Example with optional parameters:** ```bash wrap theme={null} npx hardhat lz:oft:send \ --src-eid 40232 \ --dst-eid 40231 \ --amount 10.0 \ --to 0x1234567890123456789012345678901234567890 \ --min-amount 9.5 \ --extra-options 0x00030100110100000000000000000000000000030d40 ``` The task automatically: * Finds your deployed OFT contract from deployment artifacts * Handles token approvals (for OFTAdapter) * Quotes the exact gas fee needed * Provides block explorer and LayerZero Scan links for tracking Remember to generate a fee estimate using `quoteSend` first, then pass the returned native gas amount as your `msg.value` If using the base `OFTAdapter.sol`, you will want to approve the adapter contract to spend your ERC20 tokens: ```solidity wrap theme={null} ERC20(tokenAddress).approve(adapterAddress, amount); ``` For manual token sending using Foundry, create a script that handles fee estimation and token transfer: ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { MyOFT } from "../contracts/MyOFT.sol"; import { SendParam } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; import { MessagingFee } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() external { // Load environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 tokensToSend = vm.envUint("TOKENS_TO_SEND"); uint32 dstEid = uint32(vm.envUint("DST_EID")); uint256 privateKey = vm.envUint("PRIVATE_KEY"); vm.startBroadcast(privateKey); MyOFT oft = MyOFT(oftAddress); // Build send parameters bytes memory extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam({ dstEid: dstEid, to: addressToBytes32(toAddress), amountLD: tokensToSend, minAmountLD: tokensToSend * 95 / 100, // 5% slippage tolerance extraOptions: extraOptions, composeMsg: "", oftCmd: "" }); // Get fee quote MessagingFee memory fee = oft.quoteSend(sendParam, false); console.log("Sending tokens..."); console.log("Fee amount:", fee.nativeFee); // Send tokens oft.send{value: fee.nativeFee}(sendParam, fee, msg.sender); vm.stopBroadcast(); } } ``` **Environment variables needed:** ```env wrap theme={null} OFT_ADDRESS=0x... # Your deployed OFT address TO_ADDRESS=0x... # Recipient address TOKENS_TO_SEND=1000000000000000000 # Amount in wei (18 decimals) DST_EID=30101 # Destination endpoint ID PRIVATE_KEY=0x... # Private key for sending ``` **Run the script:** ```bash wrap theme={null} forge script script/SendOFT.s.sol --rpc-url $RPC_URL --broadcast ``` ### Send tokens + call composer **Horizontal composability** allows your OFT to trigger additional actions on the destination chain through separate, containerized message packets. Unlike vertical composability (multiple calls in a single transaction), horizontal composability processes operations independently, providing better fault isolation and gas efficiency. Diagram showing horizontal composability flow: OFT processes token transfer in lzReceive, then calls endpoint.sendCompose to queue a separate composed message that the Composer contract receives via lzCompose for custom logic execution Diagram showing horizontal composability flow: OFT processes token transfer in lzReceive, then calls endpoint.sendCompose to queue a separate composed message that the Composer contract receives via lzCompose for custom logic execution #### Benefits of Horizontal Composability * **Fault Isolation**: If a composed call fails, it doesn't revert the main token transfer * **Gas Efficiency**: Each step can have independent gas limits and execution options * **Flexible Workflows**: Complex multi-step operations can be broken into manageable pieces * **Non-Critical Operations**: Secondary actions (like swaps or staking) can fail without affecting token delivery #### Workflow Overview 1. **Token Transfer**: OFT processes the token transfer in `_lzReceive()` and credits tokens to the recipient 2. **Compose Message**: OFT calls `endpoint.sendCompose()` to queue a separate composed message 3. **Composer Execution**: The composer contract receives the message via `lzCompose()` and executes custom logic #### Sending with ComposeMsg When sending tokens with composed actions, set the `to` address to your composer contract and include your custom `composeMsg`: ```solidity wrap theme={null} SendParam memory sendParam = SendParam({ dstEid: dstEid, to: addressToBytes32(composerAddress), // Composer contract address, NOT end recipient amountLD: tokensToSend, minAmountLD: tokensToSend * 95 / 100, // highlight-start extraOptions: extraOptions, composeMsg: abi.encode(finalRecipient, swapParams), // Data for composer logic // highlight-end oftCmd: "" }); ``` #### Understanding the Message Encoding When using composed messages, the OFT encodes your `composeMsg` along with token transfer data. After processing the transfer, the destination OFT re-encodes this data and delivers it to your composer contract via `endpoint.sendCompose()`. For the complete message structures and codec functions, see the [Message Encoding Reference](../composer/overview#message-encoding-reference) documentation. #### Execution Options for Composed Messages Composed messages require gas for **two separate executions**: 1. **Token Transfer (`lzReceive`)**: Credits tokens and queues the composed message 2. **Composer Call (`lzCompose`)**: Executes your custom logic in the composer contract ```solidity wrap theme={null} bytes memory options = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(65000, 0) // Token transfer + compose queuing .addExecutorLzComposeOption(0, 50000, 0); // Composer contract execution ``` **Two-Phase Gas Requirements**: * **`lzReceiveOption`**: Gas for token crediting + `endpoint.sendCompose()` call (varies with `composeMsg` size) * **`lzComposeOption`**: Gas for your composer contract's business logic (depends on complexity) Always test your composed implementation to determine adequate gas limits for both phases. If either phase runs out of gas, you'll need to manually retry the failed execution. #### Using the CLI with Composed Messages The `lz:oft:send` task supports composed messages via the `--compose-msg` and `--extra-options` parameters: ```bash wrap theme={null} npx hardhat lz:oft:send \ --src-eid 40232 \ --dst-eid 40231 \ --amount 5 \ --to 0x1234567890123456789012345678901234567890 \ --compose-msg 0x000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd \ --extra-options 0x00030100110100000000000000000000000000fdfe00030200010000000000000000000000000000c350 ``` **Encoding Compose Messages**: The `--compose-msg` parameter expects hex-encoded bytes. You can encode data using: * **Online tools**: Use ethers.js playground or similar tools to encode your data * **Cast command**: `cast abi-encode "function_signature" param1 param2` * **Hardhat console**: `ethers.utils.defaultAbiCoder.encode(['address'], ['0x...'])` **Extra Options**: The `--extra-options` above includes both `lzReceiveOption` (gas: 65534) and `lzComposeOption` (index: 0, gas: 50000) for composed messages. #### Implementing a Composer Contract The composer contract must implement `IOAppComposer` to handle composed messages. Here's a comprehensive example: ```solidity wrap theme={null} // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import { IOAppComposer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol"; import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; /** * @title TokenSwapper * @notice Receives OFT tokens and automatically swaps them for another token */ contract TokenSwapper is IOAppComposer { using SafeERC20 for IERC20; /// @notice LayerZero endpoint address address public immutable endpoint; /// @notice Trusted OFT that can send composed messages address public immutable trustedOFT; /// @notice Token to swap to IERC20 public immutable targetToken; event TokenSwapped( address indexed originalSender, address indexed recipient, uint256 amountIn, uint256 amountOut ); constructor(address _endpoint, address _trustedOFT, address _targetToken) { endpoint = _endpoint; trustedOFT = _trustedOFT; targetToken = IERC20(_targetToken); } /** * @notice Handles composed messages from the OFT * @param _oApp Address of the originating OApp (must be trusted OFT) * @param _guid Unique identifier for this message * @param _message Encoded message containing compose data */ function lzCompose( address _oApp, bytes32 _guid, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) external payable override { // Security: Verify the message source require(msg.sender == endpoint, "TokenSwapper: unauthorized sender"); require(_oApp == trustedOFT, "TokenSwapper: untrusted OApp"); // Decode the full composed message context uint64 nonce = OFTComposeMsgCodec.nonce(_message); uint32 srcEid = OFTComposeMsgCodec.srcEid(_message); uint256 amountLD = OFTComposeMsgCodec.amountLD(_message); // Get original sender (who initiated the OFT transfer) bytes32 composeFromBytes = OFTComposeMsgCodec.composeFrom(_message); address originalSender = OFTComposeMsgCodec.bytes32ToAddress(composeFromBytes); // Decode your custom compose message bytes memory composeMsg = OFTComposeMsgCodec.composeMsg(_message); (address recipient, uint256 minAmountOut) = abi.decode(composeMsg, (address, uint256)); // Execute the swap logic uint256 amountOut = _performSwap(amountLD, minAmountOut); // Transfer swapped tokens to recipient targetToken.safeTransfer(recipient, amountOut); emit TokenSwapped(originalSender, recipient, amountLD, amountOut); } function _performSwap(uint256 amountIn, uint256 minAmountOut) internal returns (uint256 amountOut) { // Your swap logic here (DEX integration, etc.) // This is a simplified example amountOut = amountIn * 95 / 100; // Simulate 5% slippage require(amountOut >= minAmountOut, "TokenSwapper: insufficient output"); } } ``` #### Key Security Considerations * **Endpoint Verification**: Always verify `msg.sender == endpoint` * **OApp Authentication**: Only accept messages from trusted OApps * **Message Validation**: Validate all decoded parameters before execution * **Reentrancy Protection**: Consider using `ReentrancyGuard` for complex operations **Token Availability**: The OFT automatically credits tokens to the composer address before calling `lzCompose`, so your composer can immediately use the received tokens. The tokens are already available in the composer's balance when `lzCompose` executes. ## Extensions The OFT Standard can be extended to support several different use cases, similar to the ERC20 token standard. Since OFT inherits from the base OApp contract, all OApp extensions and patterns are also available to OFT implementations, providing maximum flexibility for crosschain token applications. Below you can find relevant patterns and extensions: ### Rate Limiting The `RateLimiter` pattern controls the number of tokens that can be transferred crosschain within a specific time window. This is particularly valuable for OFTs to prevent abuse and ensure controlled token flow across chains. #### Why Use Rate Limiting for OFTs? * **Prevent Token Drain Attacks**: Protects against malicious actors attempting to rapidly drain tokens from a chain * **Regulatory Compliance**: Helps meet compliance requirements for controlled cross-blockchain token transfers * **Supply Management**: Maintains balanced token distribution across chains by limiting transfer velocity * **Risk Management**: Reduces exposure to smart contract vulnerabilities or bridge exploits #### Implementation Inherit from both `OFT` and `RateLimiter` in your contract: ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol"; import { RateLimiter } from "@layerzerolabs/oapp-evm/contracts/oapp/utils/RateLimiter.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; contract MyRateLimitedOFT is OFT, RateLimiter { constructor( string memory _name, string memory _symbol, address _lzEndpoint, address _owner, RateLimitConfig[] memory _rateLimitConfigs ) OFT(_name, _symbol, _lzEndpoint, _owner) Ownable(_owner) { _setRateLimits(_rateLimitConfigs); } // Override _debit to enforce rate limits on token transfers function _debit( address _from, uint256 _amountLD, uint256 _minAmountLD, uint32 _dstEid ) internal override returns (uint256 amountSentLD, uint256 amountReceivedLD) { // Check rate limit before allowing the transfer _outflow(_dstEid, _amountLD); // Proceed with normal OFT debit logic return super._debit(_amountLD, _minAmountLD, _dstEid); } } ``` #### Configuration Set up rate limits per destination chain during deployment: ```solidity wrap theme={null} // Example: Allow max 1000 tokens per hour to Ethereum, 500 per hour to Polygon RateLimitConfig[] memory configs = new RateLimitConfig[](2); configs[0] = RateLimitConfig({ dstEid: 30101, // Ethereum endpoint ID limit: 1000 ether, // 1000 tokens (18 decimals) window: 3600 // 1 hour window }); configs[1] = RateLimitConfig({ dstEid: 30109, // Polygon endpoint ID limit: 500 ether, // 500 tokens (18 decimals) window: 3600 // 1 hour window }); ``` #### Dynamic Rate Limit Management Add functions to update rate limits post-deployment: ```solidity wrap theme={null} function setRateLimits( RateLimitConfig[] calldata _rateLimitConfigs ) external onlyOwner { _setRateLimits(_rateLimitConfigs); } function getRateLimit(uint32 _dstEid) external view returns (RateLimit memory) { return rateLimits[_dstEid]; } ``` #### Rate Limit Behavior When a transfer exceeds the rate limit: * The transaction reverts with a rate limit error * Users must wait for the time window to reset * The limit resets based on a sliding window mechanism Consider implementing different rate limits for different user tiers (e.g., higher limits for verified institutions) by overriding the rate limit check logic. Rate limiting may not be suitable for all OFT applications. High-frequency trading or time-sensitive applications might be negatively impacted by rate limits. ### Mint & Burn OFT Adapter The `MintBurnOFTAdapter` is a specialized adapter for existing ERC20 tokens that have exposed mint and burn functions. Unlike the standard `OFTAdapter` which locks/unlocks tokens, this adapter burns tokens on the source chain and mints them on the destination chain. #### Key Differences from Standard OFTAdapter | Feature | Standard OFTAdapter | MintBurnOFTAdapter | | ------------------------ | ----------------------------------- | ------------------------------ | | **Token Supply** | Locks/unlocks existing tokens | Burns/mints tokens dynamically | | **Multiple Deployments** | Only one adapter per token globally | Multiple adapters can exist | | **Approval Required** | Yes, users must approve adapter | No, uses mint/burn privileges | | **Token Mechanism** | Escrow (locks tokens) | Non-escrow (burns/mints) | #### When to Use MintBurnOFTAdapter * **Tokens with mint/burn capabilities**: Your ERC20 already has `mint()` and `burn()` functions * **Dynamic supply management**: You prefer burning/minting over locking mechanisms * **Reduced custody risk**: Eliminate the risk of locked token supply running dry when using multiple adapters #### Installation To get started with a MintBurnOFTAdapter example, use the LayerZero CLI tool to create a new project: ```bash wrap theme={null} LZ_ENABLE_MINTBURN_EXAMPLE=1 npx create-lz-oapp@latest --example mint-burn-oft-adapter ``` This creates a complete project with: * Example `MintBurnOFTAdapter` contracts * Sample `ElevatedMinterBurner` implementation * Deployment and configuration scripts * Crosschain unit tests The example includes both the adapter contract and the underlying token with mint/burn capabilities, showing the complete integration pattern. #### Implementation Create your mint/burn adapter by inheriting from `MintBurnOFTAdapter`: ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { MintBurnOFTAdapter } from "@layerzerolabs/oft-evm/contracts/MintBurnOFTAdapter.sol"; import { IMintableBurnable } from "@layerzerolabs/oft-evm/contracts/interfaces/IMintableBurnable.sol"; contract MyMintBurnOFTAdapter is MintBurnOFTAdapter { constructor( address _token, // Your existing ERC20 token with mint/burn exposed IMintableBurnable _minterBurner, // Contract with mint/burn privileges address _lzEndpoint, // Local LayerZero endpoint address _owner // Contract owner ) MintBurnOFTAdapter(_token, _minterBurner, _lzEndpoint, _owner) Ownable(_owner) {} } ``` #### Token Requirements You need a contract that implements the `IMintableBurnable` interface. This can be either: **Option 1: Token directly implements the interface** ```solidity wrap theme={null} interface IMintableBurnable { function burn(address _from, uint256 _amount) external returns (bool success); function mint(address _to, uint256 _amount) external returns (bool success); } ``` **Option 2: Elevated minter/burner contract (Recommended)** For existing tokens that already have mint/burn capabilities but don't implement `IMintableBurnable`, use an intermediary contract: ```solidity wrap theme={null} contract ElevatedMinterBurner is IMintableBurnable, Ownable { IMintableBurnable public immutable token; mapping(address => bool) public operators; modifier onlyOperators() { require(operators[msg.sender] || msg.sender == owner(), "Not authorized"); _; } constructor(IMintableBurnable _token, address _owner) Ownable(_owner) { token = _token; } function setOperator(address _operator, bool _status) external onlyOwner { operators[_operator] = _status; } function burn(address _from, uint256 _amount) external override onlyOperators returns (bool) { return token.burn(_from, _amount); } function mint(address _to, uint256 _amount) external override onlyOperators returns (bool) { return token.mint(_to, _amount); } } ``` The elevated contract approach allows you to: * Use existing tokens without modification * Control which contracts can mint/burn through operator management * Maintain existing token governance while adding bridge functionality #### Usage Flow 1. **Sending tokens**: * User calls `send()` on the MintBurnOFTAdapter * Adapter burns tokens from user's balance * LayerZero message sent to destination 2. **Receiving tokens**: * Destination adapter receives LayerZero message * Adapter mints new tokens to recipient's address #### Security Considerations The `MintBurnOFTAdapter` requires careful access control since it can mint tokens: ```solidity wrap theme={null} // Example: Ensure only the adapter can mint/burn contract SecureMintBurner is IMintableBurnable, Ownable { IERC20Mintable public token; address public adapter; modifier onlyAdapter() { require(msg.sender == adapter, "Only adapter can mint/burn"); _; } function mint(address _to, uint256 _amount) external onlyAdapter returns (bool) { token.mint(_to, _amount); return true; } function burn(address _from, uint256 _amount) external onlyAdapter returns (bool) { token.burnFrom(_from, _amount); return true; } } ``` Unlike standard OFTAdapter, you can deploy multiple MintBurnOFTAdapters for the same omnichain mesh. ### OFT Alt When the native gas token cannot be used to pay LayerZero fees, you can use `OFTAlt` which supports payment in an alternative ERC20 token. #### Installation To get started with an OFTAlt example, use the LayerZero CLI tool to create a new project: ```bash wrap theme={null} LZ_ENABLE_ALT_EXAMPLE=1 npx create-lz-oapp@latest --example oft-alt ``` This creates a complete project with: * Example `OFTAlt` contracts with alternative fee payment * [`EndpointV2Alt`](/v2/concepts/protocol/layerzero-endpoint-alt) integration setup * Alternative fee token configuration * Deployment and configuration scripts * Crosschain unit tests with ERC20 fee payments The example includes both the OFT Alt contract and the necessary setup for using alternative fee tokens, showing the complete integration pattern. #### Implementation ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { OFTAlt } from "@layerzerolabs/oft-evm/contracts/OFTAlt.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; contract MyOFTAlt is OFTAlt { constructor( string memory _name, string memory _symbol, address _lzEndpointAlt, address _owner ) OFTAlt(_name, _symbol, _lzEndpointAlt, _owner) Ownable(_owner) {} } ``` #### Key Differences 1. **Fee Payment**: Uses ERC20 tokens instead of native gas 2. **Approval Required**: You must approve the OFT contract to spend your fee tokens 3. **Endpoint**: Must use `EndpointV2Alt` instead of standard `EndpointV2` #### Using OFT Alt Before sending messages, approve the fee token: ```solidity wrap theme={null} // Approve the OFT to spend fee tokens IERC20(feeToken).approve(oftAltAddress, feeAmount); // Then send normally oft.send{value: 0}(sendParam, fee, refundAddress); // No native value needed ``` OFT Alt is designed for chains where native gas tokens are not suitable for LayerZero fees, such as certain L2s or sidechains with alternative fee mechanisms. ### Further Reading For more advanced patterns and detailed implementations, see: * [OApp Design Patterns](../oapp/message-design-patterns) - Additional messaging patterns * [Message Execution Options](../configuration/options) - Detailed options configuration * [OFT Technical Reference](../../../concepts/technical-reference/oft-reference) - Deep dive into OFT mechanics ### Tracing and Troubleshooting You can follow your testnet and mainnet transaction statuses using [LayerZero Scan](https://layerzeroscan.com/). Refer to [Debugging Messages](../troubleshooting/debugging-messages) for any unexpected complications when sending a message. You can also ask for help or follow development in the [Discord](https://discord.com/invite/ktbvm8Nkcr). # LayerZero V2 ONFT Quickstart Source: https://docs.layerzero.network/v2/developers/evm/onft/quickstart Get started with ONFT Quickstart. Step-by-step tutorial for building omnichain applications on LayerZero V2. LayerZero enables secure crosschain messaging. The **Omnichain Non-Fungible Token (ONFT) Standard** allows **non-fungible tokens (NFTs)** to be transferred across multiple blockchains without asset wrapping or middlechains. * **ONFT Contract**: Uses a burn-and-mint mechanism. For a fluid NFT that can move directly between chains (e.g. Chain A and Chain B), you must deploy an ONFT contract on every chain. This creates a "mesh" of interconnected contracts. * **ONFT Adapter**: Uses a lock-and-mint mechanism. If you already have an NFT collection on one chain and want to extend it omnichain, you deploy **a single ONFT Adapter on the source chain**. Then, you deploy ONFT contracts on any new chains where the collection will be transferred. Note that only one ONFT Adapter is allowed in the entire mesh. This mesh concept is central to all LayerZero implementations: it represents the network of contracts that work together to enable omnichain NFT functionality. ### ONFT (Burn & Mint) Diagram showing ONFT burn-and-mint mechanism: NFTs are burned on Network A and minted on Network B, connected by an arrow representing the crosschain transfer Diagram showing ONFT burn-and-mint mechanism: NFTs are burned on Network A and minted on Network B, connected by an arrow representing the crosschain transfer When using **ONFT**, tokens are **burned** on the source chain whenever an omnichain transfer is initiated. LayerZero sends a message to the destination contract instructing it to **mint** the same number of tokens that were burned, ensuring the overall token supply remains consistent. ```solidity wrap theme={null} function _debit(address _from, uint256 _tokenId, uint32 /*_dstEid*/) internal virtual override { if (_from != ERC721.ownerOf(_tokenId)) revert OnlyNFTOwner(_from, ERC721.ownerOf(_tokenId)); _burn(_tokenId); } function _credit(address _to, uint256 _tokenId, uint32 /*_srcEid*/) internal virtual override { _mint(_to, _tokenId); } ``` **Key Points** * Default pattern for **new NFT collections**. * `ONFT721` extends [`ERC721`](https://docs.openzeppelin.com/contracts/5.x/api/token/erc721#ERC721) (OpenZeppelin) and adds crosschain logic. * Unified supply across chains is maintained by burning on source, minting on destination. ### ONFT Adapter (Lock & Mint) Diagram showing ONFT Adapter lock-and-mint mechanism: existing NFTs are locked in an adapter contract on the source chain, and equivalent NFTs are minted on the destination chain Diagram showing ONFT Adapter lock-and-mint mechanism: existing NFTs are locked in an adapter contract on the source chain, and equivalent NFTs are minted on the destination chain When using **ONFT Adapter**, tokens are **locked** in a contract on the source chain, while the destination contract **mints** or **unlocks** the token after receiving a message from LayerZero. When bridging back, the minted token is **burned** on the remote side, and the original is **unlocked** on the source side. ```solidity wrap theme={null} function _debit(address _from, uint256 _tokenId, uint32 /*_dstEid*/) internal virtual override { // Lock the token by transferring it to this adapter contract innerToken.transferFrom(_from, address(this), _tokenId); } function _credit(address _toAddress, uint256 _tokenId, uint32 /*_srcEid*/) internal virtual override { // Unlock the token by transferring it back to the user innerToken.transferFrom(address(this), _toAddress, _tokenId); } ``` **Key Points** * Suitable for **existing NFT collections**. * The adapter contract is effectively a “lockbox” for your existing ERC721 tokens. * No changes to your original NFT contract are required. Instead, the adapter implements the crosschain logic. ```solidity wrap ONFT theme={null} // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import { ONFT721Core } from "./ONFT721Core.sol"; /** * @title ONFT721 Contract * @dev ONFT721 is an ERC-721 token that extends the functionality of the ONFT721Core contract. */ abstract contract ONFT721 is ONFT721Core, ERC721 { string internal baseTokenURI; event BaseURISet(string baseURI); /** * @dev Constructor for the ONFT721 contract. * @param _name The name of the ONFT. * @param _symbol The symbol of the ONFT. * @param _lzEndpoint The LayerZero endpoint address. * @param _delegate The delegate capable of making OApp configurations inside of the endpoint. */ constructor( string memory _name, string memory _symbol, address _lzEndpoint, address _delegate ) ERC721(_name, _symbol) ONFT721Core(_lzEndpoint, _delegate) {} // @notice Retrieves the address of the underlying ERC721 implementation (ie. this contract). function token() external view returns (address) { return address(this); } function setBaseURI(string calldata _baseTokenURI) external onlyOwner { baseTokenURI = _baseTokenURI; emit BaseURISet(baseTokenURI); } function _baseURI() internal view override returns (string memory) { return baseTokenURI; } /** * @notice Indicates whether the ONFT721 contract requires approval of the 'token()' to send. * @dev In the case of ONFT where the contract IS the token, approval is NOT required. * @return requiresApproval Needs approval of the underlying token implementation. */ function approvalRequired() external pure virtual returns (bool) { return false; } // highlight-start // @dev Key crosschain overrides function _debit(address _from, uint256 _tokenId, uint32 /*_dstEid*/) internal virtual override { if (_from != ERC721.ownerOf(_tokenId)) revert OnlyNFTOwner(_from, ERC721.ownerOf(_tokenId)); _burn(_tokenId); } function _credit(address _to, uint256 _tokenId, uint32 /*_srcEid*/) internal virtual override { _mint(_to, _tokenId); } // highlight-end } ``` ```solidity wrap ONFT Adapter theme={null} // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import { ONFT721Core } from "./ONFT721Core.sol"; // @dev ONFT721Adapter is an adapter contract used to enable crosschain transferring of an existing ERC721 token. abstract contract ONFT721Adapter is ONFT721Core { IERC721 internal immutable innerToken; /** * @dev Constructor for the ONFT721 contract. * @param _token The underlying ERC721 token address this adapts * @param _lzEndpoint The LayerZero endpoint address. * @param _delegate The delegate capable of making OApp configurations inside of the endpoint. */ constructor(address _token, address _lzEndpoint, address _delegate) ONFT721Core(_lzEndpoint, _delegate) { innerToken = IERC721(_token); } // @notice Retrieves the address of the underlying ERC721 implementation (ie. external contract). function token() external view returns (address) { return address(innerToken); } /** * @notice Indicates whether the ONFT721 contract requires approval of the 'token()' to send. * @dev In the case of ONFT where the contract IS the token, approval is NOT required. * @return requiresApproval Needs approval of the underlying token implementation. */ function approvalRequired() external pure virtual returns (bool) { return true; } // highlight-start // @dev Key crosschain overrides function _debit(address _from, uint256 _tokenId, uint32 /*_dstEid*/) internal virtual override { // @dev Dont need to check onERC721Received() when moving into this contract, ie. no 'safeTransferFrom' required innerToken.transferFrom(_from, address(this), _tokenId); } function _credit(address _toAddress, uint256 _tokenId, uint32 /*_srcEid*/) internal virtual override { // @dev Do not need to check onERC721Received() when moving out of this contract, ie. no 'safeTransferFrom' // required // @dev The default implementation does not implement IERC721Receiver as 'safeTransferFrom' is not used. // @dev If IERC721Receiver is required, ensure proper re-entrancy protection is implemented. innerToken.transferFrom(address(this), _toAddress, _tokenId); } // highlight-end } ``` ## Installation To start using the `ONFT721` and `ONFT721Adapter` contracts, you can either create a new project via the LayerZero CLI or add the contract package to an existing project: ### New project If you're creating a new contract, LayerZero provides [`create-lz-oapp`](../../../get-started/create-lz-oapp/start), an npx package that allows developers to create any omnichain application in **less than 4 minutes**. Get started by running the following from your command line and choose `ONFT721` when asked about a starting point. It will create both `ONFT721` and `ONFT721Adapter` contracts for your project. ```bash wrap theme={null} npx create-lz-oapp@latest ``` ### Existing project To use ONFT in your existing project, install the [**@layerzerolabs/onft-evm**](https://www.npmjs.com/package/@layerzerolabs/onft-evm) package. This library provides both `ONFT721` (burn-and-mint) and `ONFT721Adapter` (lock-and-mint) variants. ```bash wrap theme={null} npm install @layerzerolabs/onft-evm ``` ```bash wrap theme={null} yarn add @layerzerolabs/onft-evm ``` ```bash wrap theme={null} pnpm add @layerzerolabs/onft-evm ``` ```bash wrap theme={null} forge init ``` ```bash wrap theme={null} forge install layerzero-labs/devtools forge install layerzero-labs/LayerZero-v2 forge install OpenZeppelin/openzeppelin-contracts git submodule add https://github.com/GNSPS/solidity-bytes-utils.git lib/solidity-bytes-utils ``` Then add to your `foundry.toml` under `[profile.default]`: ```toml wrap theme={null} [profile.default] src = "src" out = "out" libs = ["lib"] remappings = [ '@layerzerolabs/onft-evm/=lib/devtools/packages/onft-evm/', '@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/', '@layerzerolabs/lz-evm-protocol-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/protocol', '@layerzerolabs/lz-evm-messagelib-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/messagelib', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', 'solidity-bytes-utils/=lib/solidity-bytes-utils/', ] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options ``` LayerZero contracts work with both [**OpenZeppelin V5**](https://docs.openzeppelin.com/contracts/5.x/erc721) and V4 contracts. Specify your desired version in your project's package.json: ```json wrap theme={null} "resolutions": { "@openzeppelin/contracts": "^5.0.1", } ``` To create an ONFT, you should decide which implementation is appropriate for your use case: 1. Use `ONFT721` when you're creating a new NFT collection that will exist on multiple chains. 2. Use `ONFT721Adapter` when you need to make an existing NFT collection crosschain compatible. #### ONFT721 Implementation Deploy an **ONFT** that inherits from `ONFT721`, which combines `ERC721` with the crosschain functionality needed for omnichain transfers. The contract automatically handles token burning on the source chain and minting on the destination chain. You can pass in your chosen contract name, symbol, the LayerZero Endpoint address, and the contract's delegate (owner or governance address). This contract becomes the "canonical" NFT on every chain. ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { ONFT721 } from "@layerzerolabs/onft-evm/contracts/onft721/ONFT721.sol"; contract MyONFT721 is ONFT721 { constructor( string memory _name, string memory _symbol, address _lzEndpoint, address _delegate ) ONFT721(_name, _symbol, _lzEndpoint, _delegate) {} } ``` #### ONFT721Adapter Implementation Deploy an **ONFT Adapter** that references your existing NFT contract address. The `ONFT721Adapter` constructor takes an additional parameter `_token`, which is the address of the existing `ERC721` token that you want to make crosschain compatible. ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { ONFT721Adapter } from "@layerzerolabs/onft-evm/contracts/onft721/ONFT721Adapter.sol"; contract MyONFT721Adapter is ONFT721Adapter { constructor( address _token, address _lzEndpoint, address _delegate ) ONFT721Adapter(_token, _lzEndpoint, _delegate) {} } ``` ### Warning There can only be one ONFT Adapter used for a specific `ERC721` token, and it should be deployed on the chain where the original `ERC721` token is located. On all the other chains where you want to use the ONFT, you only need an `ONFT721` contract. ## Deployment Workflow The deployment process for ONFT contracts involves several steps, which we'll cover in detail: 1. **Deploy the ONFT** or ONFT Adapter contracts to all the chains you want to connect. 2. **Configure peer relationships** between contracts on different chains. 3. **Set security parameters** including Decentralized Validator Networks (DVNs). 4. **Configure message execution options**. ### 1. Deploy ONFT Contracts First, deploy your ONFT contracts to all the chains you want to connect: For new NFT collections: * Deploy `MyONFT721` on all chains. For existing NFT collections: * Deploy `MyONFT721Adapter` on the chain where the original NFT exists. * Deploy `MyONFT721` on all other chains you want to connect. ### 2. Configure Security Parameters **Production deployments should use multiple required DVNs from independent operators.** A single-DVN configuration means a compromise of that one verifier results in unrestricted forged messages on the pathway. See the [Integration Checklist](../../../tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway) for production DVN guidance. Set the DVN configuration, including block confirmations, security thresholds, executor settings, and messaging libraries: ```solidity wrap theme={null} EndpointV2.setSendLibrary(aONFT, bEid, newLib) EndpointV2.setReceiveLibrary(aONFT, bEid, newLib, gracePeriod) EndpointV2.setReceiveLibraryTimeout(aONFT, bEid, lib, gracePeriod) EndpointV2.setConfig(aONFT, sendLibrary, sendConfig) EndpointV2.setConfig(aONFT, receiveLibrary, receiveConfig) EndpointV2.setDelegate(delegate) ``` These configurations are stored in the `EndpointV2` contract and control how messages are verified and executed. If you don't set custom configurations, the system will use default configurations set by LayerZero Labs. **We strongly recommend reviewing these settings carefully and configuring your security stack according to your needs and preferences**. You can find example scripts to make these calls in [Security and Executor Configuration](../configuration/dvn-executor-config). ### 3. Configure Peer Relationships After deployment, you need to call `setPeer` on each contract to establish trust between ONFT contracts on different chains. Set peers by calling `setPeer(dstEid, addressToBytes32(remoteONFT))` on every chain. This whitelists each destination as the trusted contract to receive your message. ```solidity wrap theme={null} uint32 aEid = 1; // Example endpoint id for Chain A uint32 bEid = 2; // Example endpoint id for Chain B MyONFT721 aONFT; // Contract deployed on Chain A MyONFT721 bONFT; // Contract deployed on Chain B // Call on both sides for each pathway // On chain A aONFT.setPeer(bEid, addressToBytes32(address(bONFT))); // On chain B bONFT.setPeer(aEid, addressToBytes32(address(aONFT))); ``` The actual endpoint ids will vary per chain, see [Supported Chains](../../../deployments/deployed-contracts) for endpoint id reference. ### 4. Configure Message Execution Options *\[Optional but recommended]* ONFT inherits `OAppOptionsType3` from the `OApp` standard. This means you can define: 1. **enforcedOptions**: A contract-wide default that every `send` must abide by (e.g. minimum gas for `lzReceive`, or a maximum message size). 2. **extraOptions**: A call-specific set of execution settings or advanced features, such as adding a “composed” message on the remote side. ```solidity wrap theme={null} // Recommended gas setting for ONFT transfers EnforcedOptionParam[] memory aEnforcedOptions = new EnforcedOptionParam[](1); // Force 65k gas on the remote (chain B) when bridging from chain A aEnforcedOptions[0] = EnforcedOptionParam({ eid: bEid, // Remote chain id (chain B) msgType: SEND, options: OptionsBuilder.newOptions().addExecutorLzReceiveOption(100_000, 0) // Gas limit, msg.value }); aONFT.setEnforcedOptions(aEnforcedOptions); ``` This ensures every user who calls `myONFT.send(...)` must pay at least `100_000` gas on the remote chain for the bridging operation. This is useful for ensuring there's enough gas on the destination chain to execute the bridging operation and to receive the bridged tokens. `enforcedOptions` should only be set for `msgType: SEND`, to make sure there's enough gas on the destination chain to execute the bridging operation and to receive the bridged tokens. See [Message Execution Options](../configuration/options) for more details. ## Using ONFT Contracts #### Estimating Gas Fees Before calling `send`, you'll typically want to estimate the fee using `quoteSend`. Similar to OFT, you can call `quoteSend(...)` to get an estimate of how much `msg.value` you need to pass when bridging an NFT crosschain. This function takes in the same parameters as `send` but does not actually initiate the transfer. Instead, it queries the Endpoint for an estimated cost in `nativeFee`. Arguments of the estimate function: 1. `SendParam` *(struct)*: which parameters should be used for the `send` operation? ```solidity wrap theme={null} struct SendParam { uint32 dstEid; // Destination LayerZero EndpointV2 ID. bytes32 to; // Recipient address. uint256 tokenId; bytes extraOptions; // Additional options supplied by the caller to be used in the LayerZero message. bytes composeMsg; // The composed message for the send() operation. bytes onftCmd; // The ONFT command to be executed, unused in default ONFT implementations. } ``` 2. `payInLzToken` *(bool)*: which token (native or LZ token) will be used to pay for the transaction? `true` for LZ token and `false` for native token. This lets us construct the `quoteSend` function: ```solidity wrap theme={null} // @notice Provides a quote for the send() operation. // @param _sendParam The parameters for the send() operation. // @param _payInLzToken Flag indicating whether the caller is paying in the LZ token. // @return msgFee The calculated LayerZero messaging fee from the send() operation. function quoteSend( SendParam calldata _sendParam, bool _payInLzToken ) external view virtual returns (MessagingFee memory msgFee) { (bytes memory message, bytes memory options) = _buildMsgAndOptions(_sendParam); return _quote(_sendParam.dstEid, message, options, _payInLzToken); } ``` We now have everything we need to be able to send the NFT crosschain: * `SendParam` struct with all the parameters needed to send the NFT crosschain * `quoteSend` function to estimate the fee before sending the NFT crosschain * `refundAddress` parameter to specify the address to refund if the transaction fails on the source chain (default is the sender's address) Let's send some NFTs across the chains! #### Sending NFTs Across Chains To transfer an NFT to another chain, users call the `send` function with appropriate parameters: ```solidity wrap theme={null} function send( SendParam calldata _sendParam, // Parameters for the send() operation. MessagingFee calldata _fee, // The calculated LayerZero messaging fee from the send() operation. address _refundAddress // The address to refund if the transaction fails on the source chain. ) external payable virtual returns (MessagingReceipt memory msgReceipt) { _debit(msg.sender, _sendParam.tokenId, _sendParam.dstEid); // Debit the sender's balance. (bytes memory message, bytes memory options) = _buildMsgAndOptions(_sendParam); // @dev Sends the message to the LayerZero Endpoint, returning the MessagingReceipt. msgReceipt = _lzSend(_sendParam.dstEid, message, options, _fee, _refundAddress); emit ONFTSent(msgReceipt.guid, _sendParam.dstEid, msg.sender, _sendParam.tokenId); } ``` You can override the `_debit` function with any additional logic you want to execute before the message is sent via the protocol, for example, taking custom fees. #### Example Client Code Here's how the `send` function can be called, as a Hardhat task for an ONFT Adapter contract: ```js wrap theme={null} import {task} from 'hardhat/config'; import { Options, addressToBytes32 } from '@layerzerolabs/lz-v2-utilities' import {BigNumberish, BytesLike} from 'ethers'; interface SendParam { dstEid: BigNumberish // Destination LayerZero EndpointV2 ID. to: BytesLike // Recipient address. tokenId: BigNumberish // Token ID of the NFT to send. extraOptions: BytesLike // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike // The composed message for the send() operation. onftCmd: BytesLike // The ONFT command to be executed, unused in default ONFT implementations. } task('send-nft', 'Sends an NFT from chain A to chain B using MyONFTAdapter') .addParam('adapter', 'Address of MyONFTAdapter contract on source chain') .addParam('dstEndpointId', 'Destination chain endpoint ID') .addParam('recipient', 'Recipient on the destination chain') .addParam('tokenId', 'Token ID to send') .setAction(async (taskArgs, { ethers, deployments }) => { const { adapter, dstEndpointId, recipient, tokenId } = taskArgs const [signer] = await ethers.getSigners() const adapterDeployment = await deployments.get('MyONFT721Adapter') // Get adapter contract instance const adapterContract = new ethers.Contract(adapterDeployment.address, adapterDeployment.abi, signer) // Get the underlying ERC721 token address const tokenAddress = await adapterContract.token() const erc721Contract = await ethers.getContractAt('IERC721', tokenAddress) // Check and set approval for specific token ID const approved = await erc721Contract.getApproved(tokenId) if (approved.toLowerCase() !== adapterDeployment.address.toLowerCase()) { const approveTx = await erc721Contract.approve(adapterDeployment.address, tokenId) await approveTx.wait() // Grant approval for specific token ID } // Build the parameters const sendParam: SendParam = { dstEid: dstEndpointId, to: addressToBytes32(recipient), // convert to bytes32 tokenId: tokenId, extraOptions: '0x', // If you want to pass custom options composeMsg: '0x', // If you want additional logic on the remote chain onftCmd: '0x', } // Get quote for the transfer const quotedFee = await adapterContract.quoteSend(sendParam, false) // Send the NFT, using the returned quoted fee in msg.value const tx = await adapterContract.send( sendParam, quotedFee, signer.address, { value: quotedFee.nativeFee } ) const receipt = await tx.wait() console.log('🎉 NFT sent! Transaction hash:', receipt.transactionHash) }) ``` You can put this task in `sendNFT.ts` in the `tasks` directory and run the command below to send the NFT. This assumes that you have already deployed the adapter contract on Sepolia (testnet) and are sending the NFT to a recipient on Polygon Amoy (testnet). ```bash wrap theme={null} npx hardhat send-nft \ --adapter 0x05EBb5dBefE45451Da5aA367CA0c39E715E85c99 \ # ONFTAdapter address on Sepolia --dst-endpoint-id 40267 \ # Destination chain endpoint ID (Amoy) --recipient 0x777A711938F0E40d8dd8cB457aE0AB3596Bd476d \ # Recipient address on Amoy --token-id 7 \ # Token ID of the NFT you want to send --network sepolia-testnet # Network you're sending from ``` When you call `send`: * **ONFT** will `_burn` in the source chain contract, `_mint` in the destination chain contract. * **ONFT Adapter** will `transferFrom(...)` tokens into itself on the source chain (locking them), then `_mint` or `_unlock` on the destination. #### Receiving the NFT (`_lzReceive`) A successful `send` call will be delivered to the destination chain, invoking the `_lzReceive` method during execution on that chain: ```solidity wrap theme={null} function _lzReceive( Origin calldata _origin, bytes32 _guid, bytes calldata _message, address /*_executor*/, // @dev unused in the default implementation. bytes calldata /*_extraData*/ // @dev unused in the default implementation. ) internal virtual override { address toAddress = _message.sendTo().bytes32ToAddress(); uint256 tokenId = _message.tokenId(); // Mint / unlock the NFT to the recipient _credit(toAddress, tokenId, _origin.srcEid); // If there's a "composeMsg" for extra logic, handle it here... if (_message.isComposed()) { // ... } emit ONFTReceived(_guid, _origin.srcEid, toAddress, tokenId); } ``` You can see each step in [ONFT721Core.sol](https://github.com/LayerZero-Labs/devtools/blob/main/packages/onft-evm/contracts/onft721/ONFT721Core.sol). ## Advanced Features ### Composed Messages ONFT supports composed messages, allowing you to execute additional logic on the destination chain as part of the NFT transfer. When the `composeMsg` parameter is not empty, after the NFT is minted on the destination chain, the composed message will be executed in a separate transaction. For advanced use cases, you can leverage this feature to: * Trigger additional actions when an NFT arrives * Integrate with other protocols on the destination chain * Implement crosschain NFT marketplace functionality ### ONFT721Enumerable For collections that need enumeration capabilities, LayerZero provides an `ONFT721Enumerable` contract that extends `ONFT721` with the [ERC721Enumerable](https://docs.openzeppelin.com/contracts/5.x/api/token/erc721#ERC721Enumerable) functionality: ```solidity wrap theme={null} abstract contract ONFT721Enumerable is ONFT721Core, ERC721Enumerable { // Implementation details... } ``` This is useful for applications that need to enumerate or track all tokens within the collection. ## Example: Complete End-to-End Deployment Flow Here's a complete example showing how to deploy and configure an ONFT system with an existing NFT collection on Ethereum and bridging to Polygon: 1. **Create a new OApp with CLI** ```bash wrap theme={null} npx create-lz-oapp@latest ``` Choose `ONFT721` as the starting point. 2. **Configure OApp** * Modify `layerzero.config.ts` to configure the OApp and add all the chains you want your ONFT to be available on. * Add private key to `.env` file * Modify `hardhat.config.ts` to add the networks you want to deploy to 3. **Deploy Contracts**: Adapt the contracts to your needs and deploy them using Hardhat: ```bash wrap theme={null} npx hardhat lz:deploy ``` You'll be able to choose which chains you want to deploy to. 4. **Configure Peers**: Now that everything is deployed, it's time to wire all the contracts together. The fastest way is to use the CLI: ```bash wrap theme={null} npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` 5. **Verify Setup** Verify that everything was wired up correctly: ```bash wrap theme={null} npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` Verify configurations: ```bash wrap theme={null} npx hardhat lz:oapp:config:get:default # Outputs the default OApp config npx hardhat lz:oapp:config:get # Outputs Custom OApp Config, Default OApp Config, and Active OApp Config. Each config contains Send & Receive Libraries, Send Uln & Executor Configs, and Receive Executor Configs ``` In the output of the config command above: * **Custom OApp config**: what you customized in your OApp * **Default OApp config**: the defaults that are applied if you don't customize anything * **Active OApp config**: the config that is currently active (essentially, default + your applied customizations) And you are now ready to send the NFT across all your configured chains! 🎉 ## Security Considerations When deploying ONFT contracts, consider the following security aspects: 1. **Peer Configuration**: Only set trusted contract addresses as peers to prevent unauthorized minting. 2. **DVN Settings**: Use multiple required DVNs from independent operators in production. A single-DVN configuration means a compromise of that one verifier results in unrestricted forged messages on the pathway. See the [Integration Checklist](../../../tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway). 3. **Gas Limits**: Set appropriate gas limits in `enforceOptions` to prevent out-of-gas errors. 4. **Ownership Controls**: Implement proper access controls for administrative functions. 5. **Timeouts and Recovery**: Understand how message timeouts work and prepare recovery procedures. ## Next Steps The ONFT standard provides a powerful way to create truly crosschain NFT collections. By understanding the core concepts and following the deployment guidelines outlined in this document, you can build robust omnichain NFT applications that leverage LayerZero's secure messaging protocol. For more information, explore these related resources: * [OApp Contract Standard](../oapp/overview) * [Security and Executor Configuration](../configuration/dvn-executor-config) * [Message Execution Options](../configuration/options) * [LayerZero Endpoint Addresses](../../../deployments/deployed-contracts) **You’re ready to build omnichain NFTs!** # OVault EVM Implementation Source: https://docs.layerzero.network/v2/developers/evm/ovault/overview Step-by-step guide to ovault evm implementation using LayerZero V2. Build and deploy omnichain applications with crosschain messaging. Follow step-by-step d... Create an **omnichain ERC-4626 vault** that enable users to deposit assets or redeem shares on any blockchain network through a single transaction. Architecture diagram comparing traditional ERC-4626 vault with OVault, showing how OVault enables crosschain deposits and redemptions through OFT assets, OFTAdapter for shares, and VaultComposerSync for orchestration Architecture diagram comparing traditional ERC-4626 vault with OVault, showing how OVault enables crosschain deposits and redemptions through OFT assets, OFTAdapter for shares, and VaultComposerSync for orchestration ## Prerequisites Before implementing OVault, you should understand: 1. [OFT Standard](../oft/quickstart): How **Omnichain Fungible Tokens** work and what the typical deployment looks like 2. [Composer Pattern](../composer/overview): Understanding of `composeMsg` encoding and crosschain message workflows 3. [ERC-4626 Vaults](https://eips.ethereum.org/EIPS/eip-4626): How the tokenized vault standard interface works for `deposit`/`redeem` operations An **Omnichain Vault (OVault)** takes a new or existing ERC4626 vault, and connects the underlying asset or share to many blockchain networks using the **Omnichain Fungible Token (OFT) standard**. An OVault implementation requires **five** core contracts to be deployed: * an `OFT` asset * an `ERC4626` vault * an `OFTAdapter` to transform the vault's share into an omnichain token * a `VaultComposerSync` to orchestrate omnichain deposits and redemptions between the asset and share * an `OFT` to represent the shares on spoke chains You can review the implementation of these contracts under [Contracts Overview](#contracts-overview). ## Step 1. Project Installation To start using LayerZero OVault contracts in a new project, use the LayerZero CLI tool, [**create-lz-oapp**](../../../get-started/create-lz-oapp/start). The CLI tool allows developers to create any omnichain application in \<4 minutes! Get started by running the following from your command line: ```bash wrap theme={null} LZ_ENABLE_OVAULT_EXAMPLE=1 npx create-lz-oapp@latest --example ovault-evm ``` After running, select the directory for the scaffold project to be cloned into: ```bash wrap theme={null} ✔ Where do you want to start your project? … ./ ? Which example would you like to use as a starting point? › - Use arrow-keys. Return to submit. ❯ OVault EVM OApp OFT OFTAdapter ONFT721 ``` After the installer completes, copy and paste the `.env.example` in the project root, add your `PRIVATE_KEY`, `RPC_URL` you will be working with, and rename the file to `.env`: ```bash wrap theme={null} cp .env.example .env ``` You can find the sample codebase in [devtools/examples/ovault-evm](https://github.com/LayerZero-Labs/devtools/tree/main/examples/ovault-evm). ## Step 2. Network Configuration Update `hardhat.config.ts` to include your desired networks. Modify your `.env` file or the URL directly to change network RPCs: ```typescript wrap theme={null} const config: HardhatUserConfig = { networks: { base: { eid: EndpointId.BASESEP_V2_TESTNET, url: process.env.RPC_URL_BASESEP_TESTNET || 'https://base-sepolia.gateway.tenderly.co', accounts, }, arbitrum: { eid: EndpointId.ARBSEP_V2_TESTNET, url: process.env.RPC_URL_ARBSEP_TESTNET || 'https://arbitrum-sepolia.gateway.tenderly.co', accounts, }, optimism: { eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OPTSEP_TESTNET || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, }, // ... rest of config }; ``` ## Step 3. Deployment Configuration Configure your vault deployment in `devtools/deployConfig.ts`. This file controls which contracts to deploy and to what chains. The `deployConfig` supports several modes, depending on what contracts already have been deployed on the hub chain. If your `asset` token is already an `OFT`, you do not need to deploy a new `OFT` contract mesh. Both [Stargate Hydra](https://docs.stargate.finance/primitives/routes/stargateV2#supported-assets-hydra) assets (e.g., `USDC.e`) and [standard OFTs](../oft/quickstart) (e.g., `USDT0`) can be used as the asset inside the `ERC4626` vault. To see a list of existing OFT-compatible assets, review the [LayerZero OFT API](/v2/tools/api/oft). Pick the setup section that best aligns with your deployment needs: * [3.1a Existing AssetOFT](#31a-existing-assetoft) * [3.1b Existing AssetOFT and Vault](#31b-existing-assetoft-and-vault) * [3.1c Existing AssetOFT, Vault, and ShareOFTAdapter](#31c-existing-assetoft-vault-and-shareoft) For a completely fresh deployment of the **AssetOFT**, **OVault**, and **ShareOFT**: * [3.1d New AssetOFT, Vault, and ShareOFTAdapter](#31d-new-assetoft-vault-and-shareoft) ### 3.1a Existing AssetOFT The only requirement is that your `assetOFT` is deployed on the `hub` chain defined in the `deployConfig` file. * Update the `_hubEid` and the `_spokeEids` for the networks you plan to deploy to accordingly. * Add the `assetOFTAddress` contract for the `_hubEid` network under your `vault` config. * Add any changes necessary to your `Vault` and `ShareOFT` config `contract` or `metadata`. ```typescript wrap theme={null} // highlight-start // Hub network where ERC4626 lives const _hubEid = EndpointId.ARBSEP_V2_TESTNET; // Spoke networks where ShareOFT lives (excluding hub) const _spokeEids = [EndpointId.OPTSEP_V2_TESTNET, EndpointId.BASESEP_V2_TESTNET]; // highlight-end // ============================================ // Deployment Export // ============================================ // devtools/deployConfig.ts export const DEPLOYMENT_CONFIG: DeploymentConfig = { // highlight-start vault: { contracts: { vault: 'MyERC4626', shareAdapter: 'MyShareOFTAdapter', composer: 'MyOVaultComposer', }, deploymentEid: _hubEid, vaultAddress: undefined, // Existing ERC4626 vault assetOFTAddress: '', // Existing AssetOFT shareOFTAdapterAddress: undefined, // Deploy ShareOFTAdapter }, // highlight-end // highlight-start // Share OFT configuration (only on spoke chains) shareOFT: { contract: 'MyShareOFT', metadata: { name: 'MyShareOFT', symbol: 'SHARE', }, deploymentEids: _spokeEids, }, // highlight-end // Asset OFT configuration (deployed on specified chains) assetOFT: { contract: 'MyAssetOFT', metadata: { name: 'MyAssetOFT', symbol: 'ASSET', }, deploymentEids: [_hubEid, ..._spokeEids], }, } as const; ``` `assetOFT` and `shareOFT` networks do not need to perfectly overlap, as long as both contain deployments on the `_hubEid`. Configure based on your deployment requirements. ### 3.1b Existing AssetOFT and Vault If your `assetOFT` and `ERC4626` contracts are already deployed, you only need to deploy the `ShareOFTAdapter` and `Composer`. * Update the `_hubEid` and the `_spokeEids` for the networks you plan to deploy to accordingly. * Add the `vaultAddress` and `assetOFTAddress` for the `_hubEid` network under your `vault` config. * Add any changes necessary to your `ShareOFT` config `contract` or `metadata`. ```typescript wrap theme={null} // devtools/deployConfig.ts // highlight-start // Hub network where ERC4626 lives const _hubEid = EndpointId.ARBSEP_V2_TESTNET; // Spoke networks where ShareOFT lives (excluding hub) const _spokeEids = [EndpointId.OPTSEP_V2_TESTNET, EndpointId.BASESEP_V2_TESTNET]; // highlight-end // ============================================ // Deployment Export // ============================================ export const DEPLOYMENT_CONFIG: DeploymentConfig = { vault: { contracts: { vault: 'MyERC4626', shareAdapter: 'MyShareOFTAdapter', composer: 'MyOVaultComposer', }, deploymentEid: _hubEid, // highlight-start vaultAddress: '', // Existing ERC4626 vault assetOFTAddress: '', // Existing AssetOFT token shareOFTAdapterAddress: undefined, // Deploy ShareOFTAdapter // highlight-end }, // highlight-start // Share OFT configuration (only on spoke chains) shareOFT: { contract: 'MyShareOFT', metadata: { name: 'MyShareOFT', symbol: 'SHARE', }, deploymentEids: _spokeEids, }, // highlight-end // Asset OFT configuration (deployed on specified chains) assetOFT: { contract: 'MyAssetOFT', metadata: { name: 'MyAssetOFT', symbol: 'ASSET', }, deploymentEids: [_hubEid, ..._spokeEids], }, } as const; ``` This configuration will skip deploying the `AssetOFT` and `ERC4626 Vault` contracts, deploying only the `ShareOFTAdapter` and `Composer`. ### 3.1c Existing AssetOFT, Vault, and ShareOFT If your `assetOFT`, `ERC4626`, and `ShareOFTAdapter` have already been deployed, you only need to deploy the `Composer`. * Update the `_hubEid` and the `_spokeEids` for the networks you plan to deploy to accordingly. * Add the `vaultAddress`, `assetOFTAddress`, and `shareOFTAdapterAddress` for the `_hubEid` network under your `vault` config. * Add any changes necessary to your `composer` config under `vault`. The `Vault`, `ShareOFT`, and `AssetOFT` configs and deployments will be **skipped**. ```typescript wrap theme={null} // devtools/deployConfig.ts // highlight-start // Hub network where ERC4626 lives const _hubEid = EndpointId.ARBSEP_V2_TESTNET; // Spoke networks where ShareOFT lives (excluding hub) const _spokeEids = [EndpointId.OPTSEP_V2_TESTNET, EndpointId.BASESEP_V2_TESTNET]; // highlight-end // ============================================ // Deployment Export // ============================================ export const DEPLOYMENT_CONFIG: DeploymentConfig = { vault: { contracts: { vault: 'MyERC4626', shareAdapter: 'MyShareOFTAdapter', // highlight-start composer: 'MyOVaultComposer', // highlight-end }, deploymentEid: _hubEid, // highlight-start vaultAddress: '', // Existing ERC4626 vault assetOFTAddress: '', // Existing AssetOFT shareOFTAdapterAddress: <'YOUR_SHARE_OFT_ADAPTER_ADDRESS'>, // Existing ShareOFTAdapter // highlight-end }, // Share OFT configuration (only on spoke chains) shareOFT: { contract: 'MyShareOFT', metadata: { name: 'MyShareOFT', symbol: 'SHARE', }, deploymentEids: _spokeEids, }, // Asset OFT configuration (deployed on specified chains OR use existing address) assetOFT: { contract: 'MyAssetOFT', metadata: { name: 'MyAssetOFT', symbol: 'ASSET', }, deploymentEids: [_hubEid, ..._spokeEids], }, } as const; ``` This configuration will skip deployment of the `AssetOFT`, `ERC4626` vault, and `ShareOFT` contracts. Only the `Composer` will be deployed. ### 3.1d New AssetOFT, Vault, and ShareOFT If you have **no existing** OVault contracts, this configuration will deploy `AssetOFT`, `ERC4626` vault, `ShareOFTAdapter`, and the `Composer`. ```typescript wrap theme={null} const _hubEid = EndpointId.ARBSEP_V2_TESTNET; const _spokeEids = [EndpointId.OPTSEP_V2_TESTNET, EndpointId.BASESEP_V2_TESTNET]; // ============================================ // Deployment Export // ============================================ export const DEPLOYMENT_CONFIG: DeploymentConfig = { vault: { contracts: { vault: 'MyERC4626', shareAdapter: 'MyShareOFTAdapter', composer: 'MyOVaultComposer', }, deploymentEid: _hubEid, // highlight-start vaultAddress: undefined, assetOFTAddress: undefined, shareOFTAdapterAddress: undefined, // highlight-end }, // Share OFT configuration (only on spoke chains) ShareOFT: { contract: 'MyShareOFT', metadata: { name: 'MyShareOFT', symbol: 'SHARE', }, deploymentEids: _spokeEids, }, // Asset OFT configuration (deployed on specified chains) AssetOFT: { contract: 'MyAssetOFT', metadata: { name: 'MyAssetOFT', symbol: 'ASSET', }, deploymentEids: [_hubEid, ..._spokeEids], }, } as const; ``` This configuration will deploy all core OVault contracts for a full fresh setup. ### 3.2 Build Compile your contracts: ```bash wrap theme={null} pnpm compile ``` If you're deploying the asset OFT from scratch for testing purposes, you'll need to mint an initial supply. Uncomment the `_mint` line in the `MyAssetOFT` constructor to provide initial liquidity. This ensures you have tokens to test deposit and crosschain transfer functionality. Do NOT mint share tokens directly in `MyShareOFT`. Share tokens must only be minted by the vault contract during deposits to maintain the correct share-to-asset ratio. Manually minting share tokens breaks the vault's accounting and can lead to incorrect redemption values. The mint line in `MyShareOFT` should only be uncommented for UI/integration testing, never in production. ### 3.3 Deploy Deploy all vault contracts across all configured chains: ```bash wrap theme={null} pnpm hardhat lz:deploy --tags ovault ``` Based on your `deployConfig.ts`, this single command will begin deploying the defined contracts on your target `_hubEid` and `_spokeEids`. The deployment scripts automatically skip existing deployments, so you can safely run this command when expanding to new chains. Simply add the new chain endpoints to your `deployConfig.ts` and run the deploy command again. > **Tip**: To deploy to specific networks only, use the `--networks` flag: > > ```bash theme={null} > pnpm hardhat lz:deploy --tags ovault --networks arbitrum,optimism > ``` ## Step 4. Wiring New Mesh This establishes the peer relationships between each OFT deployment, enabling crosschain token transfers. See the [OFT Wiring Step](../oft/quickstart#2-wire-messaging-libraries-and-configurations) for more information. Depending on your deployment configuration in [Step 3](#step-3-deployment-configuration), you will have to wire either your newly deployed `ShareOFT`, `AssetOFT`, or **both**. ### 4.1 Existing Asset After modifying your `layerzero.share.config.ts`: ```bash wrap theme={null} pnpm hardhat lz:oapp:wire --oapp-config layerzero.share.config.ts ``` ### 4.2 Existing Asset & Share No action needed. ### 4.3 New Asset & Share After modifying your `layerzero.asset.config.ts` and `layerzero.share.config.ts`: ```bash wrap theme={null} # Configure LayerZero connections pnpm hardhat lz:oapp:wire --oapp-config layerzero.asset.config.ts pnpm hardhat lz:oapp:wire --oapp-config layerzero.share.config.ts ``` ## Step 5: Usage OVault enables two main operation patterns: **deposits** and **redemptions**. Each uses the standard `OFT.send()` interface with the `composer` handling vault operations automatically. The provided project scaffold demonstrates how to create send calls in [devtools/examples/ovault-evm/tasks/sendOVaultComposer.ts](https://github.com/LayerZero-Labs/devtools/blob/main/examples/ovault-evm/tasks/sendOVaultComposer.ts). ### Deposit Assets → Receive Shares **Scenario**: Deposit `asset` from a `_spokeEid`, receive vault `shares` on **the same** `_spokeEid` ```bash wrap theme={null} # Using the CLI task (recommended) npx hardhat lz:ovault:send \ --src-eid 30110 --dst-eid 30110 \ --amount 100.0 --to 0xRecipient \ --token-type asset ``` **Flow**: ```mermaid wrap theme={null} sequenceDiagram participant U as User(Arbitrum) participant A_SRC as AssetOFT(Arbitrum) participant A_HUB as AssetOFT(Hub) participant C as Composer(Hub) participant V as Vault(Hub) participant S_HUB as ShareOFTAdapter(Hub) rect rgb(40, 169, 225) Note over U,A_SRC: Arbitrum Chain end rect rgb(243, 244, 246) Note over A_HUB,S_HUB: Hub Chain end U->>A_SRC: send(asset, dstEid, composer, composeMsg) A_SRC->>A_HUB: LayerZero transfer A_HUB->>C: lzReceive() → lzCompose() Note over C: Detects asset deposit operation C->>V: deposit(assets) V-->>C: shares minted C->>S_HUB: send(shares, arbitrum, recipient) S_HUB->>A_SRC: LayerZero transfer (shares) A_SRC->>U: shares delivered Note over U,S_HUB: Single transaction: Assets → Shares (same chain) ```
**Scenario**: Deposit `asset` from a `_spokeEid`, receive vault `shares` on **a different** `_spokeEid` ```bash wrap theme={null} # Using the CLI task (recommended) npx hardhat lz:ovault:send \ --src-eid 30110 --dst-eid 30111 \ --amount 100.0 --to 0xRecipient \ --token-type asset ``` **Flow**: ```mermaid wrap theme={null} sequenceDiagram participant U as User(Arbitrum) participant A_SRC as AssetOFT(Arbitrum) participant A_HUB as AssetOFT(Hub) participant C as Composer(Hub) participant V as Vault(Hub) participant S_HUB as ShareOFTAdapter(Hub) participant S_DST as ShareOFT(Optimism) participant R as Recipient rect rgb(40, 169, 225) Note over U,A_SRC: Arbitrum Chain end rect rgb(243, 244, 246) Note over A_HUB,S_HUB: Hub Chain end rect rgb(255, 4, 32) Note over S_DST,R: Optimism Chain end U->>A_SRC: send(asset, dstEid, composer, composeMsg) A_SRC->>A_HUB: LayerZero transfer A_HUB->>C: lzReceive() → lzCompose() Note over C: Detects asset deposit operation C->>V: deposit(assets) V-->>C: shares minted C->>S_HUB: send(shares, optimism, recipient) S_HUB->>S_DST: LayerZero transfer S_DST->>R: shares delivered Note over U,R: Single transaction: Assets(Arbitrum) → Shares(Optimism) ```
**Scenario**: Deposit `asset` from `_spokeEid`, receive vault `shares` on the `_hubEid` chain ```bash wrap theme={null} npx hardhat lz:ovault:send \ --src-eid 30110 --dst-eid 30184 \ --amount 100.0 --to 0xRecipient \ --token-type asset ``` **Flow**: ```mermaid wrap theme={null} sequenceDiagram participant U as User(Arbitrum) participant A_SRC as AssetOFT(Arbitrum) participant A_HUB as AssetOFT(Hub) participant C as Composer(Hub) participant V as Vault(Hub) participant R as Recipient(Hub) rect rgb(40, 169, 225) Note over U,A_SRC: Arbitrum Chain end rect rgb(243, 244, 246) Note over A_HUB,R: Hub Chain end U->>A_SRC: send(asset, dstEid, composer, composeMsg) A_SRC->>A_HUB: LayerZero transfer A_HUB->>C: lzReceive() → lzCompose() Note over C: Detects asset deposit operation C->>V: deposit(assets) V-->>C: shares minted Note over C: dstEid == hubEid (local delivery) C->>R: Direct ERC20 transfer (shares) Note over U,R: Single transaction: Assets(Arbitrum) → Shares(Hub) ``` ### Redeem Shares → Receive Assets **Scenario**: Redeem vault `shares` from `_spokeEid`, receive `asset` on different `_spokeEid` ```bash wrap theme={null} npx hardhat lz:ovault:send \ --src-eid 30111 --dst-eid 30110 \ --amount 50.0 --to 0xRecipient \ --token-type share ``` **Flow**: ```mermaid wrap theme={null} sequenceDiagram participant U as User(Optimism) participant S_SRC as ShareOFT(Optimism) participant S_HUB as ShareOFTAdapter(Hub) participant C as Composer(Hub) participant V as Vault(Hub) participant A_HUB as AssetOFT(Hub) participant A_DST as AssetOFT(Arbitrum) participant R as Recipient(Arbitrum) rect rgb(255, 4, 32) Note over U,S_SRC: Optimism Chain end rect rgb(243, 244, 246) Note over S_HUB,A_HUB: Hub Chain end rect rgb(40, 169, 225) Note over A_DST,R: Arbitrum Chain end U->>S_SRC: send(shares, dstEid, composer, composeMsg) S_SRC->>S_HUB: LayerZero transfer S_HUB->>C: lzReceive() → lzCompose() Note over C: Detects share redeem operation C->>V: redeem(shares) V-->>C: assets returned C->>A_HUB: send(assets, arbitrum, recipient) A_HUB->>A_DST: LayerZero transfer A_DST->>R: assets delivered Note over U,R: Single transaction: Shares(Optimism) → Assets(Arbitrum) ```
**Scenario**: Redeem `vault` shares from `_spokeEid`, receive `asset` on the `_hubEid` chain ```bash wrap theme={null} npx hardhat lz:ovault:send \ --src-eid 30111 --dst-eid 30184 \ --amount 50.0 --to 0xRecipient \ --token-type share ``` **Flow**: ```mermaid wrap theme={null} sequenceDiagram participant U as User(Optimism) participant S_SRC as ShareOFT(Optimism) participant S_HUB as ShareOFTAdapter(Hub) participant C as Composer(Hub) participant V as Vault(Hub) participant R as Recipient(Hub) rect rgb(255, 4, 32) Note over U,S_SRC: Optimism Chain end rect rgb(243, 244, 246) Note over S_HUB,R: Hub Chain end U->>S_SRC: send(shares, dstEid, composer, composeMsg) S_SRC->>S_HUB: LayerZero transfer S_HUB->>C: lzReceive() → lzCompose() Note over C: Detects share redeem operation C->>V: redeem(shares) V-->>C: assets returned Note over C: dstEid == hubEid (local delivery) C->>R: Direct ERC20 transfer (assets) Note over U,R: Single transaction: Shares(Optimism) → Assets(Hub) ``` ### SDK Integration For programmatic integration, use the official SDK [`@layerzerolabs/ovault-evm/src`](https://github.com/LayerZero-Labs/devtools/tree/main/packages/ovault-evm/src) which simplifies OVault operations by using [viem](https://viem.sh/) to generate the necessary calldata for calling `OFT.send()` with the proper `composeMsg` for the hub composer. The SDK's `OVaultMessageBuilder.generateOVaultInputs()` method handles all the complex message encoding and returns ready-to-use transaction parameters for `viem` wallet clients. ```typescript wrap theme={null} const input = { srcEid: 40245, // eid for base-sepolia hubEid: 40231, // eid for arbitrum-sepolia dstEid: 40245, // eid for base-sepolia // Optional. If dstAddress is not specified it will default to the walletAddress on the dst chain dstAddress: '0x0000000000000000000000000000000000000000', walletAddress: '0x0000000000000000000000000000000000000000', vaultAddress: '0x0000000000000000000000000000000000000000', // Address of the OVault Composer on the Hub Chain. Should implement IVaultComposerSync composerAddress: '0x0000000000000000000000000000000000000000', // Supply the Viem Chain Definitions for the hub and source chain. This is so the sdk can // quote fees and perform read operations hubChain: arbitrumSepolia, sourceChain: baseSepolia, operation: OVaultOperations.DEPOSIT, amount: 100000000000000000n, slippage: 0.01, // 1% slippage // Address of the token/oft. The token is an ERC20. They can be the same address. // If tokenAddress isn't specified it defaults to the oftAddress tokenAddress: '0x0000000000000000000000000000000000000000', oftAddress: '0x0000000000000000000000000000000000000000', } as const; const inputs = await OVaultMessageBuilder.generateOVaultInputs(input); ``` For complete usage examples, API reference, and advanced configuration, see the [SDK repository](https://github.com/LayerZero-Labs/devtools/blob/main/packages/ovault-evm/README). For manual integration and advanced usage, see the [Technical Reference](#technical-reference) section below. ## Technical Reference ### Contracts Overview OVault uses a **hub-and-spoke model**: * **Hub Chain**: Hosts the `OFT` asset, `ERC4626` vault, the `VaultComposerSync`, and the share's `OFTAdapter` (lockbox) * **Spoke Chains**: Host `OFT` assets and `OFT` shares that connect to the hub implementations These connections enable a user to transfer an amount of the asset or share `OFT` from a **source** blockchain, **deposit** or **redeem** the token amount in the `ERC4626` vault, and receive the corresponding output token amount back on the **source** network. ```mermaid wrap theme={null} sequenceDiagram participant U as User(Arbitrum) participant A_SRC as AssetOFT(Arbitrum) participant A_HUB as AssetOFT(Hub) participant C as Composer(Hub) participant V as ERC4626(Hub) participant S_HUB as ShareOFTAdapter(Hub) rect rgb(40, 169, 225) Note over U,A_SRC: Arbitrum Chain end rect rgb(243, 244, 246) Note over A_HUB,S_HUB: Hub Chain end U->>A_SRC: send(asset, dstEid, composer, composeMsg) A_SRC->>A_HUB: LayerZero transfer A_HUB->>C: lzReceive() → lzCompose() Note over C: Detects asset deposit operation C->>V: deposit(assets) V-->>C: shares minted C->>S_HUB: send(shares, arbitrum, recipient) S_HUB->>A_SRC: LayerZero transfer (shares) A_SRC->>U: shares delivered Note over U,S_HUB: Single transaction: Assets → Shares (source chain) ```
If you have an existing `assetOFT`, `vault`, or `ShareOFT` implementation, you may only need to deploy some of the contracts provided in the `ovault-evm` example repo: ##### Share OFT (Spoke Chains) ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.20; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol"; /** * @title MyShareOFT * @notice ERC20 representation of the vault's share token on a spoke chain for crosschain functionality * @dev This contract represents the vault's share tokens on spoke chains. It inherits from * LayerZero's OFT (Omnichain Fungible Token) to enable seamless crosschain transfers of * vault shares between the hub chain and spoke chains. This contract is designed to work * with ERC4626-compliant vaults, enabling standardized crosschain vault interactions. * * Share tokens represent ownership in the vault and can be redeemed for the underlying * asset on the hub chain. The OFT mechanism ensures that shares maintain their value and can be freely * moved across supported chains while preserving the vault's accounting integrity. */ contract MyShareOFT is OFT { /** * @notice Constructs the Share OFT contract * @dev Initializes the OFT with LayerZero endpoint and sets up ownership * @param _name The name of the share token * @param _symbol The symbol of the share token * @param _lzEndpoint The address of the LayerZero endpoint on this chain * @param _delegate The address that will have owner privileges */ constructor( string memory _name, string memory _symbol, address _lzEndpoint, address _delegate ) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) { // WARNING: Do NOT mint share tokens directly as this breaks the vault's share-to-asset ratio // Share tokens should only be minted by the vault contract during deposits to maintain // the correct relationship between shares and underlying assets // _mint(msg.sender, 1 ether); // ONLY uncomment for testing UI/integration, never in production } } ``` Similar to `MyAssetOFT`, `MyShareOFT` is a standard `OFT` representation of the `share` token from the `ERC4626` vault to be used on other spoke chains. This contract requires `MyShareOFTAdapter` to be deployed on the hub chain using the `share` address as the `_token` argument. If your intended vault `share` is already an `OFT` (e.g., [sUSDe](https://docs.ethena.fi/solution-overview/usde-overview)), you do not need to deploy this contract, and will only need to deploy `MyOVaultComposer`. You should **NEVER** implement `_mint()` in the constructor or externally in `share` tokens. Since `shares` can be redeemed for `assets` on the hub chain, minting new supply breaks the conversion rate inside the `ERC4626` vault. ##### Asset OFT (All Chains) ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.20; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol"; /** * @title MyAssetOFT * @notice ERC20 representation of the vault's asset token on a spoke chain for crosschain functionality * @dev This contract represents the vault's underlying asset on spoke chains. It inherits from * LayerZero's OFT (Omnichain Fungible Token) to enable seamless crosschain transfers of the * vault's asset tokens between the hub chain and spoke chains. * * The asset OFT acts as a bridgeable ERC20 representation of the vault's collateral asset, allowing * users to move their assets across supported chains while maintaining fungibility. */ contract MyAssetOFT is OFT { /** * @notice Constructs the Asset OFT contract * @dev Initializes the OFT with LayerZero endpoint and sets up ownership * @param _name The name of the asset token * @param _symbol The symbol of the asset token * @param _lzEndpoint The address of the LayerZero endpoint on this chain * @param _delegate The address that will have owner privileges */ constructor( string memory _name, string memory _symbol, address _lzEndpoint, address _delegate ) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) { // NOTE: Uncomment the line below if you need to mint initial supply // This can be useful for testing or if the asset needs initial liquidity // _mint(msg.sender, 1 ether); } } ``` `MyAssetOFT` is an example of a standard `OFT` and `ERC20` token that will be the `asset` inside the `ERC4626` vault. The `asset` token must be deployed on at least the `hub` and one `spoke` chain. If your intended vault `asset` is already an `OFT` (e.g., [USDT0](https://docs.usdt0.to/technical-documentation/developer), [USDe](https://docs.ethena.fi/solution-overview/usde-overview)), you do not need to deploy this contract. if your vault `asset` is not an `OFT` (e.g., USDC via [CCTP](https://developers.circle.com/cctp)), you will need to convert the `asset` into an `OFT` compatible asset (e.g., USDC via [Stargate Hydra](https://docs.stargate.finance/primitives/routes/stargateV2#the-hydra-mechanism), [OFTAdapter](../oft/quickstart)). See the [OFT API `/list`](/v2/tools/api/oft) endpoint for a detailed list of all known tokens using the OFT standard. ##### Vault + Share Adapter (Hub Chain) ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ERC4626 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; import { OFTAdapter } from "@layerzerolabs/oft-evm/contracts/OFTAdapter.sol"; /** * @title MyERC4626 * @notice ERC4626 tokenized vault implementation for crosschain vault operations * @dev SECURITY CONSIDERATIONS: * - Donation/inflation attacks on empty or low-liquidity vaults * - Share price manipulation via large donations before first deposit * - Slippage during deposit/redeem operations in low-liquidity conditions * - First depositor advantage scenarios * * See OpenZeppelin ERC4626 documentation for full risk analysis: * https://docs.openzeppelin.com/contracts/4.x/erc4626#inflation-attack * * MITIGATIONS: * - OpenZeppelin v4.9+ includes virtual assets/shares to mitigate inflation attacks * - Deployers should consider initial deposits to prevent manipulation */ contract MyERC4626 is ERC4626 { /** * @notice Creates a new ERC4626 vault * @dev Initializes the vault with virtual assets/shares protection against inflation attacks * @param _name The name of the vault token * @param _symbol The symbol of the vault token * @param _asset The underlying asset that the vault accepts */ constructor(string memory _name, string memory _symbol, IERC20 _asset) ERC20(_name, _symbol) ERC4626(_asset) {} } /** * @title MyShareOFTAdapter * @notice OFT adapter for vault shares enabling crosschain transfers * @dev The share token MUST be an OFT adapter (lockbox). * @dev A mint-burn adapter would not work since it transforms `ShareERC20::totalSupply()` */ contract MyShareOFTAdapter is OFTAdapter { /** * @notice Creates a new OFT adapter for vault shares * @dev Sets up crosschain token transfer capabilities for vault shares * @param _token The vault share token to adapt for crosschain transfers * @param _lzEndpoint The LayerZero endpoint for this chain * @param _delegate The account with administrative privileges */ constructor( address _token, address _lzEndpoint, address _delegate ) OFTAdapter(_token, _lzEndpoint, _delegate) Ownable(_delegate) {} } ``` `MyERC4626` is the standard tokenized vault contract. Given an `_asset` address for a valid `ERC20` contract in the constructor, the vault will create a corresponding `share` token using the vanilla `ERC4626` implementation. This `share` must then be transformed into an **Omnichain Fungible Token** using `MyShareOFTAdapter`. If you have an existing `ERC4626` vault deployed, you will only need to deploy `MyShareOFTAdapter` using the `share` token address as the `address _token` argument in the constructor. ##### Composer (Hub Chain) ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { VaultComposerSync } from "@layerzerolabs/ovault-evm/contracts/VaultComposerSync.sol"; /** * @title MyOVaultComposer * @notice Crosschain vault composer enabling omnichain vault operations via LayerZero */ contract MyOVaultComposer is VaultComposerSync { /** * @notice Creates a new crosschain vault composer * @dev Initializes the composer with vault and OFT contracts for omnichain operations * @param _vault The vault contract implementing ERC4626 for deposit/redeem operations * @param _assetOFT The OFT contract for crosschain asset transfers * @param _shareOFT The OFT contract for crosschain share transfers */ constructor( address _vault, address _assetOFT, address _shareOFT ) VaultComposerSync(_vault, _assetOFT, _shareOFT) {} } ``` `VaultComposerSync` is the orchestrator contract that enables crosschain vault operations between the **OFT standard** and **ERC-4626 vaults**, automatically handling deposits and redemptions based on incoming token transfers. The "Sync" in `VaultComposerSync` refers to **synchronous vault operations** - meaning the vault must support immediate, single-transaction deposits and redemptions without delays or waiting periods. ### **For Asynchronous Vaults** For asynchronous vaults that require multi-transaction redemptions, you will need to modify the `MyOVaultComposer` contract. ### VaultComposerSync Contract The `VaultComposerSync` contract is the core orchestrator for omnichain vault operations. It handles deposits, redemptions, and automatic refunds for crosschain vault interactions. #### Key Methods ##### depositAndSend() ##### redeemAndSend() ##### quoteSend() ##### lzCompose() ##### handleCompose() #### State Variables ##### VAULT() ##### ASSET\_OFT() ##### ASSET\_ERC20() ##### SHARE\_OFT() ##### SHARE\_ERC20() ##### ENDPOINT() ##### VAULT\_EID() #### Events ##### Sent - Vault Operation Success ##### Refunded - Operation Failed with Refund ##### Deposited - Assets Deposited ##### Redeemed - Shares Redeemed #### Error Messages ##### ShareOFTNotAdapter - Invalid Share Configuration ##### ShareTokenNotVault - Share Token Mismatch ##### AssetTokenNotVaultAsset - Asset Token Mismatch ##### OnlyEndpoint - Unauthorized Endpoint Call ##### OnlySelf - Invalid Self-Call ##### OnlyValidComposeCaller - Invalid Compose Caller ##### InsufficientMsgValue - Not Enough Gas for Delivery ##### NoMsgValueExpected - Unexpected Payment ##### SlippageExceeded - Slippage Protection Triggered #### Integration Notes When integrating with VaultComposerSync: 1. **Always use quoteSend()** before operations to get accurate fee estimates and preview vault conversions 2. **Set appropriate slippage** in the compose message's `minAmountLD` parameter (2-5% recommended) 3. **Monitor events** to track operation success (`Sent`) or refunds (`Refunded`) 4. **Handle refund scenarios** by monitoring both source and hub chains for refunded tokens 5. **Check vault limits** before operations using `vault.maxDeposit()` and `vault.maxRedeem()` For direct contract interaction without the SDK, encode the compose message as: ```solidity wrap theme={null} bytes memory composeMsg = abi.encode( SendParam({ dstEid: targetChainEid, to: bytes32(recipientAddress), amountLD: 0, // Will be updated by composer minAmountLD: minOutputAmount, // Critical slippage protection extraOptions: optionsBuilder.addExecutorLzReceiveOption(200000, 0).build(), composeMsg: "", oftCmd: "" }), minMsgValue // Minimum msg.value for destination delivery ); ``` ### Two-Phase Operation Flow OVault operations follow a **two-phase architecture** where failures and slippage protection occur in distinct stages: **Phase 1: Source → Hub (Standard OFT)** * User calls `OFT.send()` targeting the hub composer * Standard LayerZero transfer with compose message * Reliable transfer with minimal failure modes **Phase 2: Hub Operations + Output Routing** * Composer executes vault operations (`deposit`/`redeem`) * **Critical slippage point**: Vault conversion rates may have changed * Output tokens routed to final destination (local or crosschain) #### Operation Detection The composer automatically determines the vault operation based on which OFT sent the tokens: * **AssetOFT caller** → Triggers `deposit` operation (`assets` → `shares`) * **ShareOFT caller** → Triggers `redeem` operation (`shares` → `assets`) #### Slippage Protection Strategy Since the real slippage occurs during vault operations on the hub, the `composeMsg` contains the critical slippage parameters: * **Phase 1** `minAmountLD`: Set for source token (not critical for vault rates) * **Phase 2** `minAmountLD`: Set in `composeMsg` for vault output (critical protection) #### 1. Standard OFT Transfer Initiation Users call the standard OFT interface with compose instructions: ```solidity wrap theme={null} // Standard OFT send with compose message assetOFT.send( SendParam({ dstEid: hubEid, // Always send to hub first to: bytes32(composer), // VaultComposerSync address amountLD: depositAmount, minAmountLD: minDepositAmount, // Slippage protection extraOptions: "...", // Gas for compose + second hop composeMsg: composeMsg, // Second SendParam + minMsgValue oftCmd: "" }), MessagingFee(msg.value, 0), refundAddress ); ``` #### 2a. Composer Message Reception When tokens arrive at the hub via `lzReceive()`, the composer is triggered via `lzCompose()`: ```solidity wrap theme={null} function lzCompose( address _composeCaller, // Either ASSET_OFT or SHARE_OFT bytes32 _guid, bytes calldata _message, // Contains routing instructions address _executor, bytes calldata _extraData ) external payable ``` The compose message contains the **Phase 2 routing instructions** with critical slippage protection: ```solidity wrap theme={null} // Decoded in handleCompose() - this controls Phase 2 behavior (SendParam memory sendParam, uint256 minMsgValue) = abi.decode( _composeMsg, (SendParam, uint256) ); // SendParam for vault output routing: // - dstEid: Target chain for output tokens // - to: Final recipient address // - amountLD: Updated by composer to actual vault output // - minAmountLD: CRITICAL - protects against vault rate slippage // - extraOptions: Gas settings for destination transfer // - composeMsg: Empty (no nested compose) // - oftCmd: Empty (no OFT commands) ``` #### 2b. Operation Detection & Execution The composer automatically determines the vault operation based on which OFT sent the tokens: **Asset Deposit Flow (AssetOFT → Composer):** ```solidity wrap theme={null} function _depositAndSend() { // 1. Deposit assets into vault uint256 shareAmount = VAULT.deposit(_assetAmount, address(this)); // 2. Verify slippage protection _assertSlippage(shareAmount, _sendParam.minAmountLD); // 3. Route shares to final destination _send(SHARE_OFT, shareAmount, _refundAddress); } ``` **Share Redemption Flow (ShareOFT → Composer):** ```solidity wrap theme={null} function _redeemAndSend() { // 1. Redeem shares from vault uint256 assetAmount = VAULT.redeem(_shareAmount, address(this), address(this)); // 2. Verify slippage protection _assertSlippage(assetAmount, _sendParam.minAmountLD); // 3. Route assets to final destination _send(ASSET_OFT, assetAmount, _refundAddress); } ``` #### 2c. Smart Output Routing The `_send()` function handles both local and crosschain delivery: ```solidity wrap theme={null} function _send(address _oft, SendParam memory _sendParam, address _refundAddress) { if (_sendParam.dstEid == VAULT_EID) { // Same chain: Direct ERC20 transfer (no LayerZero fees) address erc20 = _oft == ASSET_OFT ? ASSET_ERC20 : SHARE_ERC20; IERC20(erc20).safeTransfer(_sendParam.to.bytes32ToAddress(), _sendParam.amountLD); } else { // Crosschain: Standard OFT send IOFT(_oft).send{ value: msg.value }(_sendParam, MessagingFee(msg.value, 0), _refundAddress); } } ``` #### Key Implementation Tips 1. **Start Simple**: Deploy a basic vault first, add yield strategies later 2. **Test Thoroughly**: Each operation type has different gas requirements 3. **Monitor Closely**: Set up alerts for failed compose messages 4. **Plan Recovery**: Document procedures for each failure scenario 5. **Optimize Gas**: Use the task's automatic optimization, adjust as needed ## Troubleshooting OVault operations have only **two possible final outcomes**: `Success` or `Failed` (but Refunded). Understanding the failure flow helps determine appropriate recovery actions. ```mermaid wrap theme={null} flowchart LR Start([User calls OFT.send
with composeMsg]) --> OFTTransfer[LayerZero OFT transfer
Source → Hub chain] OFTTransfer --> LayerZeroCheck{LayerZero transfer
successful?} LayerZeroCheck -->|Yes| LzReceive[LZ Executor calls lzReceive
OFT credits tokens to composer] LayerZeroCheck -->|No| TxRevert[Transaction reverts
No tokens moved] LzReceive --> SendCompose[OFT calls endpoint.sendCompose
Stores composeMsg for execution] SendCompose --> LzCompose[LZ Executor calls lzCompose
on VaultComposerSync] LzCompose --> TryCatch[try-catch around handleCompose
Protects against composer failures] TryCatch --> MsgValueCheck{msg.value sufficient
for destination delivery?} MsgValueCheck -->|Yes| VaultExecution[Vault operation executes
vault.deposit or vault.redeem] MsgValueCheck -->|No| AutoRefund[InsufficientMsgValue revert
try-catch triggers _refund] VaultExecution --> ActualAmount[actualAmount from vault output
shares or assets received] ActualAmount --> SlippageCheck{actualAmount >= minAmountLD
from composeMsg?} SlippageCheck -->|Yes| UpdateParam[Update SendParam.amountLD
Reset minAmountLD to zero] SlippageCheck -->|No| SlippageRevert[revert SlippageExceeded
try-catch triggers _refund] UpdateParam --> OutputDelivery[_send executes
Hub → Destination or local transfer] OutputDelivery --> Success[✓ Operation complete
Tokens delivered to recipient] TxRevert --> RetryState[User retries with
correct gas/fees] AutoRefund --> RetryState SlippageRevert --> RetryState[User retries with
adjusted slippage tolerance] ``` ### Refund Scenarios and Recovery The `VaultComposerSync` uses a try-catch pattern around `handleCompose()` to ensure robust error handling: ```solidity wrap theme={null} try this.handleCompose{ value: msg.value }(/*...*/) { emit Sent(_guid); } catch (bytes memory _err) { // Automatic refund for any handleCompose failures _refund(_composeCaller, _message, amount, tx.origin); emit Refunded(_guid); } ``` **Common scenarios caught by try-catch**: * `InsufficientMsgValue` - insufficient gas for destination delivery → Auto refund * `SlippageExceeded` - vault output below minimum → Manual refund available * Vault operational errors (paused, insufficient liquidity) → Manual refund available ##### Transaction Revert: Gas or Fee Issues **What happens**: OFT transfer fails on source chain before any tokens move **Common causes**: * Insufficient native tokens for LayerZero fees * Invalid destination endpoint configuration * Gas estimation errors **User experience**: Transaction reverts immediately, no tokens transferred **Recovery**: User can retry immediately after fixing the issue * Use `quoteSend()` to get accurate fee estimation * Verify destination chain configuration * Ensure sufficient native tokens for crosschain fees ##### Automatic Refund: Insufficient msg.value for Second Hop **What happens**: LayerZero completes lzReceive and lzCompose successfully, but insufficient gas for destination delivery **Technical flow**: 1. LZ Executor calls `lzReceive()` - tokens credited to composer ✓ 2. OFT calls `endpoint.sendCompose()` - composeMsg stored ✓ 3. LZ Executor calls `lzCompose()` on VaultComposerSync ✓ 4. Try-catch around `handleCompose()` catches `InsufficientMsgValue` revert 5. Automatic `_refund()` triggered back to source chain **Common causes**: * Underestimated gas for second hop during `quoteSend()` * Gas price fluctuations between quote and execution * Complex destination chain operations requiring more gas **User experience**: * Crosschain transfer appears successful initially * Composer automatically triggers refund to source chain * Original tokens returned within minutes **Recovery**: Automatic - no user action required * Monitor source chain for refunded tokens * Retry with higher gas estimate from `quoteSend()` ##### Manual Refund: Vault Operation Issues **What happens**: LayerZero flow completes successfully, but vault operation fails slippage check **Technical flow**: 1. LZ Executor calls `lzReceive()` - tokens credited to composer ✓ 2. OFT calls `endpoint.sendCompose()` - composeMsg stored ✓ 3. LZ Executor calls `lzCompose()` on VaultComposerSync ✓ 4. Try-catch around `handleCompose()` executes vault operation ✓ 5. Vault returns `actualAmount` (shares or assets) 6. Slippage check: `actualAmount >= minAmountLD` **FAILS** 7. `SlippageExceeded` revert caught by try-catch 8. Manual `_refund()` available (user must trigger) **Common causes**: * Vault share/asset price changed during crosschain transfer * Vault hit deposit/withdrawal limits between quote and execution * `minAmountLD` set too high based on stale `previewDeposit/previewRedeem` data **User experience**: * Crosschain transfer succeeds * Vault operation fails on hub with slippage error * Tokens held by composer awaiting user action **Recovery**: User must manually trigger refund from hub chain 1. Switch wallet to hub chain network 2. Call composer refund function 3. Original tokens returned to source chain 4. Retry operation with adjusted slippage tolerance **Prevention**: * Use wider slippage tolerance (2-5% for volatile vaults) * Check vault limits: `vault.maxDeposit()`, `vault.maxRedeem()` * Monitor vault state with `vault.previewDeposit()` before large operations * Account for time delays in crosschain operations when setting `minAmountLD` # LayerZero V2 Solidity Contract Standards Source: https://docs.layerzero.network/v2/developers/evm/overview Overview of Solidity Contract Standards on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure... LayerZero enables seamless crosschain messaging, configurations for security, and other quality of life improvements to simplify crosschain development. ## LayerZero Solidity Contract Standards The push-based message passing standard, enabling crosschain data transfer and external function calls. Extension of OApp, combining the ERC20 token standard with core bridge logic to make Omnichain Fungible Tokens. Combines the ERC721 token standard with core bridge logic to make Omnichain Non-Fungible Tokens. Pull contract state information from other networks to a source blockchain using Omnichain Queries. Trigger additional contract calls and logic after push or pull based messages finish executing. Enable crosschain deposits and withdrawals with unified vault interfaces for omnichain liquidity management.
To find all of LayerZero's contract standards visit the [**LayerZero Devtools**](https://github.com/LayerZero-Labs/devtools). To see the core protocol contracts, visit the [**LayerZero V2**](https://github.com/LayerZero-Labs/layerzero-v2) repository. You can also ask for help or follow development in the [Discord](https://discord.com/invite/ktbvm8Nkcr). # LayerZero V2 EVM Protocol Overview Source: https://docs.layerzero.network/v2/developers/evm/protocol-contracts-overview Overview of EVM Protocol Overview on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure crosschain... 1. A user calls a smart contract `OApp` on the source chain and pays a fee to send a crosschain message to the `Endpoint`. 2. The `Endpoint` check the validity of the crosschain message and assigns each job to the `OApp` configured `DVNs` (Decentralized Verifier Networks) and `Executor` to execute the crosschain message. 3. The `DVNs` verify the message on the destination chain. After the required and optional DVNs have verified the message, the message is to be inserted (committed) in the message channel of the `Endpoint` on the destination chain. 4. After the message has been inserted in the Endpoint's message channel, the `Executor` calls `Endpoint.lzReceive` to trigger the execution of the crosschain message on the destination chain. 5. The `Endpoint` calls the payable `ReceiverOApp.lzReceive` to pass the message and execute the internal receive logic. You can modify the internal execution logic inside `ReceiverOApp._lzReceive` to trigger any intended outcome from the crosschain message.
You can find all of the above contracts by visiting [**Supported Chains**](../../deployments/deployed-contracts) and [**Supported DVNs**](../../deployments/dvn-addresses). ### Send Overview The `OApp` calls `EndpointV2.send` to send the crosschain message and pays a fee to each configured `DVN` and `Executor`. #### EndpointV2.sol Inside the `send` call: * emit event to each `DVN` and `Executor` according to the `OApp` send configuration for the crosschain message. Also calculate and record the fee that should be paid to each `DVN` and `Executor`. * check whether the fees the user is willing to pay can cover the fees required by the `DVNs` and `Executor`. * transfer fee to `_sendLibrary` (which records fee allocation). ```solidity wrap theme={null} // LayerZero/V2/protocol/contracts/EndpointV2.sol address public lzToken; struct MessagingParams { uint32 dstEid; // destination chain endpoint id bytes32 receiver; // receiver on destination chain bytes message; // crosschain message bytes options; // settings for executor and dvn bool payInLzToken; // whether to pay in ZRO token } struct MessagingReceipt { bytes32 guid; // unique identifier for the message uint64 nonce; // message nonce MessagingFee fee; // the message fee paid } /// @dev MESSAGING STEP 1 - OApp need to transfer the fees to the endpoint before sending the message /// @param _params the messaging parameters /// @param _refundAddress the address to refund both the native and lzToken function send( MessagingParams calldata _params, address _refundAddress ) external payable sendContext(_params.dstEid, msg.sender) returns (MessagingReceipt memory) { if (_params.payInLzToken && lzToken == address(0x0)) revert Errors.LZ_LzTokenUnavailable(); // send message (MessagingReceipt memory receipt, address _sendLibrary) = _send(msg.sender, _params); // OApp can simulate with 0 native value it will fail with error including the required fee, which can be provided in the actual call // this trick can be used to avoid the need to write the quote() function // however, without the quote view function it will be hard to compose an oapp on chain uint256 suppliedNative = _suppliedNative(); uint256 suppliedLzToken = _suppliedLzToken(_params.payInLzToken); // check fee sender has provided enough fee _assertMessagingFee(receipt.fee, suppliedNative, suppliedLzToken); // handle lz token fees to _sendLibrary _payToken(lzToken, receipt.fee.lzTokenFee, suppliedLzToken, _sendLibrary, _refundAddress); // handle native fees to _sendLibrary _payNative(receipt.fee.nativeFee, suppliedNative, _sendLibrary, _refundAddress); return receipt; } /// @dev Assert the required fees and the supplied fees are enough function _assertMessagingFee( MessagingFee memory _required, uint256 _suppliedNativeFee, uint256 _suppliedLzTokenFee ) internal pure { if (_required.nativeFee > _suppliedNativeFee || _required.lzTokenFee > _suppliedLzTokenFee) { revert Errors.LZ_InsufficientFee( _required.nativeFee, _suppliedNativeFee, _required.lzTokenFee, _suppliedLzTokenFee ); } } // pay lzToken function _payToken( address _token, uint256 _required, uint256 _supplied, address _receiver, address _refundAddress ) internal { if (_required > 0) { Transfer.token(_token, _receiver, _required); } if (_required < _supplied) { unchecked { // refund the excess Transfer.token(_token, _refundAddress, _supplied - _required); } } } // pay native token function _payNative( uint256 _required, uint256 _supplied, address _receiver, address _refundAddress ) internal virtual { if (_required > 0) { Transfer.native(_receiver, _required); } if (_required < _supplied) { unchecked { // refund the excess Transfer.native(_refundAddress, _supplied - _required); } } } ``` Inside the internal `_send` call: * get the `nonce` of this packet according to the path: **\[sender, destination chain, receiver]**. * generate `guid` of the packet (global unique identifier). * get the `_sendLibrary` of the OApp (OApp can set their specific send library of each destination chain). * call `_sendLibrary` to emit events to notify `Executor` and `DVNs`, also calculate and record the `fee` that should be paid to each. ```solidity wrap theme={null} // LayerZero/V2/protocol/contracts/EndpointV2.sol mapping(address sender => mapping(uint32 dstEid => mapping(bytes32 receiver => uint64 nonce))) public outboundNonce; /// @dev increase and return the next outbound nonce function _outbound(address _sender, uint32 _dstEid, bytes32 _receiver) internal returns (uint64 nonce) { unchecked { nonce = ++outboundNonce[_sender][_dstEid][_receiver]; } } address private constant DEFAULT_LIB = address(0); mapping(uint32 dstEid => address lib) public defaultSendLibrary; /// @notice The Send Library is the Oapp specified library that will be used to send the message to the destination /// endpoint. If the Oapp does not specify a Send Library, the default Send Library will be used. /// @dev If the Oapp does not have a selected Send Library, this function will resolve to the default library /// configured by LayerZero /// @return lib address of the Send Library /// @param _sender The address of the Oapp that is sending the message /// @param _dstEid The destination endpoint id function getSendLibrary(address _sender, uint32 _dstEid) public view returns (address lib) { lib = sendLibrary[_sender][_dstEid]; if (lib == DEFAULT_LIB) { lib = defaultSendLibrary[_dstEid]; if (lib == address(0x0)) revert Errors.LZ_DefaultSendLibUnavailable(); } } struct MessagingFee { uint256 nativeFee; uint256 lzTokenFee; } /// @dev internal function for sending the messages used by all external send methods /// @param _sender the address of the application sending the message to the destination chain /// @param _params the messaging parameters function _send( address _sender, MessagingParams calldata _params ) internal returns (MessagingReceipt memory, address) { // get the correct outbound nonce uint64 latestNonce = _outbound(_sender, _params.dstEid, _params.receiver); // construct the packet with a GUID Packet memory packet = Packet({ nonce: latestNonce, srcEid: eid, sender: _sender, dstEid: _params.dstEid, receiver: _params.receiver, guid: GUID.generate(latestNonce, eid, _sender, _params.dstEid, _params.receiver), message: _params.message }); // get the send library by sender and dst eid address _sendLibrary = getSendLibrary(_sender, _params.dstEid); // messageLib always returns encodedPacket with guid (MessagingFee memory fee, bytes memory encodedPacket) = ISendLib(_sendLibrary).send( packet, _params.options, _params.payInLzToken ); // Emit packet information for DVNs, Executors, and any other offchain infrastructure to only listen // for this one event to perform their actions. emit PacketSent(encodedPacket, _params.options, _sendLibrary); return (MessagingReceipt(packet.guid, latestNonce, fee), _sendLibrary); } ``` The `guid` is generated using the following parameters: ```solidity wrap theme={null} // LayerZero/V2/protocol/contracts/libs/GUID.sol function generate( uint64 _nonce, uint32 _srcEid, address _sender, uint32 _dstEid, bytes32 _receiver ) internal pure returns (bytes32) { return keccak256(abi.encodePacked(_nonce, _srcEid, _sender.toBytes32(), _dstEid, _receiver)); } ``` #### SendUln302.sol Next, the message is handled by the `OApp` selected Send Library. For example, `SendUln302.send`: * pay workers (`DVNs` and `Executor`) and treasury. In the send process, the `fee` is not directly paid to the workers, but recorded in the send library (`SendUln302.sol`) for workers to claim later. * call `DVNs` and `Executor`'s contract to emit event to notify them to send crosschain message. ```solidity wrap theme={null} // LayerZero/V2/messagelib/contracts/uln/uln302/SendUln302.sol struct Packet { uint64 nonce; uint32 srcEid; address sender; uint32 dstEid; bytes32 receiver; bytes32 guid; bytes message; } function send( Packet calldata _packet, bytes calldata _options, bool _payInLzToken ) public virtual onlyEndpoint returns (MessagingFee memory, bytes memory) { // assign job to Executor and DVN, calculate fees (bytes memory encodedPacket, uint256 totalNativeFee) = _payWorkers(_packet, _options); // calculate and pay the treasury fee, if enabled (uint256 treasuryNativeFee, uint256 lzTokenFee) = _payTreasury( _packet.sender, _packet.dstEid, totalNativeFee, _payInLzToken ); totalNativeFee += treasuryNativeFee; return (MessagingFee(totalNativeFee, lzTokenFee), encodedPacket); } ``` Inside the `SendUln302._payWorkers`, the contract: * splits options to get `executorOptions` (`Executor`) and `validationOptions` (`DVN`). * get the `OApp` set `Executor` and corresponding `maxMessageSize` (If not set, then a default `maxMessageSize` of 10000 bytes is used), and checks that the size of the message to send is less than than the max. * calls `_payExecutor` to assign job to corresponding `Executor` and record the fee paid. * calls `_payVerifier` to assign job to specified `DVNs` and record fee paid. ```solidity wrap theme={null} // LayerZero/V2/messagelib/contracts/uln/uln302/SendUln302.sol /// 1/ handle executor /// 2/ handle other workers function _payWorkers( Packet calldata _packet, bytes calldata _options ) internal returns (bytes memory encodedPacket, uint256 totalNativeFee) { // split workers options (bytes memory executorOptions, WorkerOptions[] memory validationOptions) = _splitOptions(_options); // handle executor ExecutorConfig memory config = getExecutorConfig(_packet.sender, _packet.dstEid); uint256 msgSize = _packet.message.length; _assertMessageSize(msgSize, config.maxMessageSize); totalNativeFee += _payExecutor(config.executor, _packet.dstEid, _packet.sender, msgSize, executorOptions); // handle other workers (uint256 verifierFee, bytes memory packetBytes) = _payVerifier(_packet, validationOptions); //for ULN, it will be dvns totalNativeFee += verifierFee; encodedPacket = packetBytes; } // @dev get the executor config and if not set, return the default config function getExecutorConfig(address _oapp, uint32 _remoteEid) public view returns (ExecutorConfig memory rtnConfig) { ExecutorConfig storage defaultConfig = executorConfigs[DEFAULT_CONFIG][_remoteEid]; ExecutorConfig storage customConfig = executorConfigs[_oapp][_remoteEid]; uint32 maxMessageSize = customConfig.maxMessageSize; rtnConfig.maxMessageSize = maxMessageSize != 0 ? maxMessageSize : defaultConfig.maxMessageSize; address executor = customConfig.executor; rtnConfig.executor = executor != address(0x0) ? executor : defaultConfig.executor; } function _assertMessageSize(uint256 _actual, uint256 _max) internal pure { if (_actual > _max) revert LZ_MessageLib_InvalidMessageSize(_actual, _max); } ``` Inside the `SendUln302._payExecutor`: * calls `Executor` (default or set by OApp) to assign job and calculate the fee needed. * record the `Executor`’s fee inside the send library. ```solidity wrap theme={null} // LayerZero/V2/messagelib/contracts/uln/uln302/SendUln302.sol function _payExecutor( address _executor, uint32 _dstEid, address _sender, uint256 _msgSize, bytes memory _executorOptions ) internal returns (uint256 executorFee) { executorFee = ILayerZeroExecutor(_executor).assignJob(_dstEid, _sender, _msgSize, _executorOptions); if (executorFee > 0) { fees[_executor] += executorFee; } emit ExecutorFeePaid(_executor, executorFee); } ``` Inside the `SendUln302._payVerifier`: * calculate `payloadHash` and `payload`, which will be used to emit event to notify `DVN` to send the crosschain message. * `payloadHash` is a digest including information about the version and path of the crosschain message; * `payload` includes information of the `guid` and the body of the crosschain message. * get the sender `OApp` config about which `DVNs` to use. * assign job for each `DVN`, including both required and optional. ```solidity wrap theme={null} // LayerZero/V2/messagelib/contracts/uln/uln302/SendUln302.sol function _payVerifier( Packet calldata _packet, WorkerOptions[] memory _options ) internal override returns (uint256 otherWorkerFees, bytes memory encodedPacket) { (otherWorkerFees, encodedPacket) = _payDVNs(fees, _packet, _options); } struct WorkerOptions { uint8 workerId; bytes options; } // accumulated fees for workers and treasury mapping(address worker => uint256) public fees; struct AssignJobParam { uint32 dstEid; bytes packetHeader; bytes32 payloadHash; uint64 confirmations; // source chain block confirmations before message being verified on the destination address sender; } struct UlnConfig { uint64 confirmations; // we store the length of required DVNs and optional DVNs instead of using DVN.length directly to save gas uint8 requiredDVNCount; // 0 indicate DEFAULT, NIL_DVN_COUNT indicate NONE (to override the value of default) uint8 optionalDVNCount; // 0 indicate DEFAULT, NIL_DVN_COUNT indicate NONE (to override the value of default) uint8 optionalDVNThreshold; // (0, optionalDVNCount] address[] requiredDVNs; // no duplicates. sorted an an ascending order. allowed overlap with optionalDVNs address[] optionalDVNs; // no duplicates. sorted an an ascending order. allowed overlap with requiredDVNs } /// ---------- pay and assign jobs ---------- function _payDVNs( mapping(address => uint256) storage _fees, Packet memory _packet, WorkerOptions[] memory _options ) internal returns (uint256 totalFee, bytes memory encodedPacket) { // calculate packetHeader and payload bytes memory packetHeader = PacketV1Codec.encodePacketHeader(_packet); bytes memory payload = PacketV1Codec.encodePayload(_packet); bytes32 payloadHash = keccak256(payload); uint32 dstEid = _packet.dstEid; address sender = _packet.sender; // get user’s config about DVN UlnConfig memory config = getUlnConfig(sender, dstEid); // if options is not empty, it must be dvn options bytes memory dvnOptions = _options.length == 0 ? bytes("") : _options[0].options; uint256[] memory dvnFees; // assign job for each DVN includes those required and optional (totalFee, dvnFees) = _assignJobs( _fees, config, ILayerZeroDVN.AssignJobParam(dstEid, packetHeader, payloadHash, config.confirmations, sender), dvnOptions ); encodedPacket = abi.encodePacked(packetHeader, payload); emit DVNFeePaid(config.requiredDVNs, config.optionalDVNs, dvnFees); } ``` ```solidity wrap theme={null} // LayerZero/V2/protocol/contracts/messagelib/libs/PacketV1Codec.sol function encodePacketHeader(Packet memory _packet) internal pure returns (bytes memory) { return abi.encodePacked( PACKET_VERSION, _packet.nonce, _packet.srcEid, _packet.sender.toBytes32(), _packet.dstEid, _packet.receiver ); } function encodePayload(Packet memory _packet) internal pure returns (bytes memory) { return abi.encodePacked(_packet.guid, _packet.message); } ``` Inside the `SendUln302._assignJobs`: * call each required and optional `DVN` to notify them to verify the crosschain message on the destination chain. * update each `DVN`'s fee. * return the `totalFee` used by all `DVNs`. ```solidity wrap theme={null} // LayerZero/V2/messagelib/contracts/uln/uln302/SendUln302.sol function _assignJobs( mapping(address => uint256) storage _fees, UlnConfig memory _ulnConfig, ILayerZeroDVN.AssignJobParam memory _param, bytes memory dvnOptions ) internal returns (uint256 totalFee, uint256[] memory dvnFees) { (bytes[] memory optionsArray, uint8[] memory dvnIds) = DVNOptions.groupDVNOptionsByIdx(dvnOptions); uint8 dvnsLength = _ulnConfig.requiredDVNCount + _ulnConfig.optionalDVNCount; dvnFees = new uint256[](dvnsLength); for (uint8 i = 0; i < dvnsLength; ++i) { address dvn = i < _ulnConfig.requiredDVNCount ? _ulnConfig.requiredDVNs[i] : _ulnConfig.optionalDVNs[i - _ulnConfig.requiredDVNCount]; bytes memory options = ""; for (uint256 j = 0; j < dvnIds.length; ++j) { if (dvnIds[j] == i) { options = optionsArray[j]; break; } } dvnFees[i] = ILayerZeroDVN(dvn).assignJob(_param, options); if (dvnFees[i] > 0) { _fees[dvn] += dvnFees[i]; totalFee += dvnFees[i]; } } } ``` #### Assign Job to Executor `Executor.assignJob` calls `ExecutorFeeLib.getFeeOnSend` to calculate the fee that should be paid to the `Executor`, and emit an event to notify. In the `ExecutorFeeLib.getFeeOnSend`, it will check the `msg.value` specified by the message sender and enforce that it should be smaller than the `DstConfig.nativeCap` of the destination chain. This is because the supply of native tokens (e.g., Ether) must be maintained by the `Executor`, and is not controlled by the OApp unless running a custom `Executor`. ```solidity wrap theme={null} // LayerZero/V2/messagelib/contracts/Executor.sol struct FeeParams { address priceFeed; uint32 dstEid; address sender; uint256 calldataSize; uint16 defaultMultiplierBps; } struct DstConfig { uint64 baseGas; // for verifying / fixed calldata overhead uint16 multiplierBps; uint128 floorMarginUSD; // uses priceFeed PRICE_RATIO_DENOMINATOR uint128 nativeCap; // maximum native gas token cap } function assignJob( uint32 _dstEid, address _sender, uint256 _calldataSize, bytes calldata _options ) external onlyRole(MESSAGE_LIB_ROLE) onlyAcl(_sender) returns (uint256 fee) { IExecutorFeeLib.FeeParams memory params = IExecutorFeeLib.FeeParams( priceFeed, _dstEid, _sender, _calldataSize, defaultMultiplierBps ); fee = IExecutorFeeLib(workerFeeLib).getFeeOnSend(params, dstConfig[_dstEid], _options); } ``` #### Assign Job to DVNs `DVN.assignJob` calls `DVNFeeLib.getFeeOnSend` to calculate the fee that should be paid to the `DVNs`, and emit events to notify them. ```solidity wrap theme={null} // LayerZero/V2/messagelib/contracts/uln/dvn/DVN.sol /// @dev for ULN301, ULN302 and more to assign job /// @dev dvn network can reject job from _sender by adding/removing them from allowlist/denylist /// @param _param assign job param /// @param _options dvn options function assignJob( AssignJobParam calldata _param, bytes calldata _options ) external payable onlyRole(MESSAGE_LIB_ROLE) onlyAcl(_param.sender) returns (uint256 totalFee) { IDVNFeeLib.FeeParams memory feeParams = IDVNFeeLib.FeeParams( priceFeed, _param.dstEid, _param.confirmations, _param.sender, quorum, defaultMultiplierBps ); totalFee = IDVNFeeLib(workerFeeLib).getFeeOnSend(feeParams, dstConfig[_param.dstEid], _options); } ``` ### Send Limitations #### Max Message Bytes Size The `maxMessageSize` depends on the Send Library. In `SendUln302`, the default max is 10000 bytes, but this value can be configured per OApp. #### Max Native Gas Token Requests In the `ExecutorFeeLib._decodeExecutorOptions`, it limits the maximum native gas token amount that can be requested from the `Executor` for the destination chain transaction. This config is set in `Executor.dstConfig`: ```solidity wrap theme={null} // LayerZero/V2/messagelib/contracts/Executor.sol struct DstConfig { uint64 baseGas; // for verifying / fixed calldata overhead uint16 multiplierBps; uint128 floorMarginUSD; // uses priceFeed PRICE_RATIO_DENOMINATOR uint128 nativeCap; // maximum native gas token amount to request from Executor for destination chain transaction } ``` ### Verification Workflow After the crosschain message has been sent on the source chain (event has been emitted to notify `DVNs` and `Executor`), `DVN` will first verify the message on the destination chain, after which `Executor` will execute the message. #### DVN Verification `DVNs` call `ReceiveUln302.verify` to submit their witness of the source crosschain message using the `_payloadHash`. ```solidity wrap theme={null} // LayerZero/V2/messagelib/contracts/uln/ReceiveUlnBase.sol function verify(bytes calldata _packetHeader, bytes32 _payloadHash, uint64 _confirmations) external { _verify(_packetHeader, _payloadHash, _confirmations); } mapping(bytes32 headerHash => mapping(bytes32 payloadHash => mapping(address dvn => Verification))) public hashLookup; function _verify(bytes calldata _packetHeader, bytes32 _payloadHash, uint64 _confirmations) internal { hashLookup[keccak256(_packetHeader)][_payloadHash][msg.sender] = Verification(true, _confirmations); emit PayloadVerified(msg.sender, _packetHeader, _confirmations, _payloadHash); } ``` #### Commit Verification After the `OApp`'s required `DVNs` have all verified, and the threshold of optional `DVNs` has been reached, `ReceiveUln302.commitVerification` can be called by any address to commit the verification to the `Endpoint`'s message channel. ```solidity wrap theme={null} // LayerZero/V2/messagelib/contracts/uln/uln302/ReceiveUln302.sol struct UlnConfig { uint64 confirmations; // we store the length of required DVNs and optional DVNs instead of using DVN.length directly to save gas uint8 requiredDVNCount; // 0 indicate DEFAULT, NIL_DVN_COUNT indicate NONE (to override the value of default) uint8 optionalDVNCount; // 0 indicate DEFAULT, NIL_DVN_COUNT indicate NONE (to override the value of default) uint8 optionalDVNThreshold; // (0, optionalDVNCount] address[] requiredDVNs; // no duplicates. sorted an an ascending order. allowed overlap with optionalDVNs address[] optionalDVNs; // no duplicates. sorted an an ascending order. allowed overlap with requiredDVNs } /// @dev dont need to check endpoint verifiable here to save gas, as it will reverts if not verifiable. function commitVerification(bytes calldata _packetHeader, bytes32 _payloadHash) external { // check packet header validity _assertHeader(_packetHeader, localEid); // decode the receiver and source Endpoint Id address receiver = _packetHeader.receiverB20(); uint32 srcEid = _packetHeader.srcEid(); // get receiver's config UlnConfig memory config = getUlnConfig(receiver, srcEid); _verifyAndReclaimStorage(config, keccak256(_packetHeader), _payloadHash); Origin memory origin = Origin(srcEid, _packetHeader.sender(), _packetHeader.nonce()); // call endpoint to verify payload hash // endpoint will revert if nonce <= lazyInboundNonce ILayerZeroEndpointV2(endpoint).verify(origin, receiver, _payloadHash); } function _assertHeader(bytes calldata _packetHeader, uint32 _localEid) internal pure { // assert packet header is of right size 81 if (_packetHeader.length != 81) revert LZ_ULN_InvalidPacketHeader(); // assert packet header version is the same as ULN if (_packetHeader.version() != PacketV1Codec.PACKET_VERSION) revert LZ_ULN_InvalidPacketVersion(); // assert the packet is for this endpoint if (_packetHeader.dstEid() != _localEid) revert LZ_ULN_InvalidEid(); } ``` `_verifyAndReclaimStorage` verifies that the required and optional `DVNs` have submitted witness. ```solidity wrap theme={null} function _verifyAndReclaimStorage(UlnConfig memory _config, bytes32 _headerHash, bytes32 _payloadHash) internal { if (!_checkVerifiable(_config, _headerHash, _payloadHash)) { revert LZ_ULN_Verifying(); } // iterate the required DVNs if (_config.requiredDVNCount > 0) { for (uint8 i = 0; i < _config.requiredDVNCount; ++i) { delete hashLookup[_headerHash][_payloadHash][_config.requiredDVNs[i]]; } } // iterate the optional DVNs if (_config.optionalDVNCount > 0) { for (uint8 i = 0; i < _config.optionalDVNCount; ++i) { delete hashLookup[_headerHash][_payloadHash][_config.optionalDVNs[i]]; } } } ``` #### Insert Hash to Endpoint's Message Channel Inside the `verify`: * check `msg.sender` is valid `ReceiveLibrary` configured by the `OApp`. * get the `lazyNonce` of the OApp. * check the crosschain message path is valid for the `receiver`. * check the message represented by the `nonce` has not been executed before. * insert the message into the `Endpoint`'s message channel. `lazyNonce` is the latest executed message’s `nonce`. To execute a transaction, LayerZero requires all messages before the current message has been verified. So all messages before the message with `lazyNonce` has been verified. ```solidity wrap theme={null} // LayerZero/V2/protocol/contracts/EndpointV2.sol /// @dev configured receive library verifies a message /// @param _origin a struct holding the srcEid, nonce, and sender of the message /// @param _receiver the receiver of the message /// @param _payloadHash the payload hash of the message function verify(Origin calldata _origin, address _receiver, bytes32 _payloadHash) external { // check msg.sender is valid ReceiveLibrary configured by the OApp if (!isValidReceiveLibrary(_receiver, _origin.srcEid, msg.sender)) revert Errors.LZ_InvalidReceiveLibrary(); // get the lazynonce uint64 lazyNonce = lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender]; // check whether path is valid if (!_initializable(_origin, _receiver, lazyNonce)) revert Errors.LZ_PathNotInitializable(); // check the nonce/msg hasn't been executed before if (!_verifiable(_origin, _receiver, lazyNonce)) revert Errors.LZ_PathNotVerifiable(); // insert the message into the message channel _inbound(_receiver, _origin.srcEid, _origin.sender, _origin.nonce, _payloadHash); emit PacketVerified(_origin, _receiver, _payloadHash); } ``` `isValidReceiveLibrary` checks whether the `ReceiveLib` is the expected `ReceiveLib` of the `receiver`. If not, then check whether there has been a `Timeout` set for the current `ReceiveLib`. `Timeout` is used to help improve the UX of updating a `ReceiveLib`. For example, if `OApp` decides to switch the `ReceiveLib`, it can update the address on the destination chain, but some crosschain messages may already be in-flight and not inserted in the destination chain Endpoint's message channel before the switch. Those messages depend on the previous `ReceiveLib`, so `Timeout` provides a grace period to ensure already in-flight messages have successful execution. ```solidity wrap theme={null} // LayerZero/V2/protocol/contracts/EndpointV2.sol /// @dev called when the endpoint checks if the msgLib attempting to verify the msg is the configured msgLib of the Oapp /// @dev this check provides the ability for Oapp to lock in a trusted msgLib /// @dev it will fist check if the msgLib is the currently configured one. then check if the msgLib is the one in grace period of msgLib versioning upgrade function isValidReceiveLibrary( address _receiver, uint32 _srcEid, address _actualReceiveLib ) public view returns (bool) { // early return true if the _actualReceiveLib is the currently configured one (address expectedReceiveLib, bool isDefault) = getReceiveLibrary(_receiver, _srcEid); if (_actualReceiveLib == expectedReceiveLib) { return true; } // check the timeout condition otherwise // if the Oapp is using defaultReceiveLibrary, use the default Timeout config // otherwise, use the Timeout configured by the Oapp Timeout memory timeout = isDefault ? defaultReceiveLibraryTimeout[_srcEid] : receiveLibraryTimeout[_receiver][_srcEid]; // requires the _actualReceiveLib to be the same as the one in grace period and the grace period has not expired // block.number is uint256 so timeout.expiry must > 0, which implies a non-ZERO value if (timeout.lib == _actualReceiveLib && timeout.expiry > block.number) { // timeout lib set and has not expired return true; } // returns false by default return false; } /// @dev the receiveLibrary can be lazily resolved that if not set it will point to the default configured by LayerZero function getReceiveLibrary(address _receiver, uint32 _srcEid) public view returns (address lib, bool isDefault) { lib = receiveLibrary[_receiver][_srcEid]; if (lib == DEFAULT_LIB) { lib = defaultReceiveLibrary[_srcEid]; if (lib == address(0x0)) revert Errors.LZ_DefaultReceiveLibUnavailable(); isDefault = true; } } ``` `_initializable` is used to check whether the crosschain message path is valid for the `receiver`. `_lazyInboundNonce` greater than 0 suggests a message has already been executed successfully, so no need to call `_receiver` to check the path again, which helps save gas. Otherwise, call `_receiver.allowInitializePath` to check (the `OApp` standard inherits `OAppReceiver` which has already implemented `allowInitializePath`). ```solidity wrap theme={null} // LayerZero/V2/protocol/contracts/EndpointV2.sol function _initializable( Origin calldata _origin, address _receiver, uint64 _lazyInboundNonce ) internal view returns (bool) { return _lazyInboundNonce > 0 || // allowInitializePath already checked ILayerZeroReceiver(_receiver).allowInitializePath(_origin); } ``` ```solidity wrap theme={null} // LayerZero/V2/oapp/contracts/oapp/OAppReceiver.sol /** * @notice Checks if the path initialization is allowed based on the provided origin. * @param origin The origin information containing the source endpoint and sender address. * @return Whether the path has been initialized. * * @dev This indicates to the endpoint that the OApp has enabled msgs for this particular path to be received. * @dev This defaults to assuming if a peer has been set, its initialized. * Can be overridden by the OApp if there is other logic to determine this. */ function allowInitializePath(Origin calldata origin) public view virtual returns (bool) { return peers[origin.srcEid] == origin.sender; } ``` `_verifiable` checks that the nonce / message has not been executed before. * If `_origin.nonce` > `_lazyInboundNonce`, then the nonce / message has not been executed before, otherwise `_lazyInboundNonce` ≥ `_origin.nonce`. * If `_origin.nonce` ≤ `_lazyInboundNonce`, then the nonce / message has been verified. If the payload hash is empty, which means the nonce / message has been executed (because the `Endpoint` will clear the payload hash of the nonce after successful execution), it cannot be executed again. ```solidity wrap theme={null} // LayerZero/V2/protocol/contracts/EndpointV2.sol function _verifiable( Origin calldata _origin, address _receiver, uint64 _lazyInboundNonce ) internal view returns (bool) { return _origin.nonce > _lazyInboundNonce || // either initializing an empty slot or reverifying inboundPayloadHash[_receiver][_origin.srcEid][_origin.sender][_origin.nonce] != EMPTY_PAYLOAD_HASH; // only allow reverifying if it hasn't been executed } ``` `_inbound` inserts the message into the channel (`inboundPayloadHash`). ```solidity wrap theme={null} // LayerZero/V2/protocol/contracts/MessagingChannel.sol /// @dev inbound won't update the nonce eagerly to allow unordered verification /// @dev instead, it will update the nonce lazily when the message is received /// @dev messages can only be cleared in order to preserve censorship-resistance function _inbound( address _receiver, uint32 _srcEid, bytes32 _sender, uint64 _nonce, bytes32 _payloadHash ) internal { if (_payloadHash == EMPTY_PAYLOAD_HASH) revert Errors.LZ_InvalidPayloadHash(); inboundPayloadHash[_receiver][_srcEid][_sender][_nonce] = _payloadHash; } ``` ### Receive Workflow #### Endpoint Execution After the crosschain message has been inserted into the channel (`Endpoint.inboundPayloadHash`), `Executor` will try to call `Endpoint.lzReceive` to execute the message. * clear the payload first to prevent reentrancy and double execution. * call `ILayerZeroReceiver.lzReceive` to execute the message. ```solidity wrap theme={null} // LayerZero/V2/protocol/contracts/EndpointV2.sol struct Origin { uint32 srcEid; bytes32 sender; uint64 nonce; } /// @dev execute a verified message to the designated receiver /// @dev the execution provides the execution context (caller, extraData) to the receiver. the receiver can optionally assert the caller and validate the untrusted extraData /// @dev cant reentrant because the payload is cleared before execution /// @param _origin the origin of the message /// @param _receiver the receiver of the message /// @param _guid the guid of the message /// @param _message the message /// @param _extraData the extra data provided by the executor. this data is untrusted and should be validated. function lzReceive( Origin calldata _origin, address _receiver, bytes32 _guid, bytes calldata _message, bytes calldata _extraData ) external payable { // clear the payload first to prevent reentrancy, and then execute the message _clearPayload(_receiver, _origin.srcEid, _origin.sender, _origin.nonce, abi.encodePacked(_guid, _message)); ILayerZeroReceiver(_receiver).lzReceive{ value: msg.value }(_origin, _guid, _message, msg.sender, _extraData); emit PacketDelivered(_origin, _receiver); } ``` Inside the `_clearPayload`: * update the `lazyInboundNonce`. * verify payload provided by `Executor`. * delete message in the channel to prevent double execution. ```solidity wrap theme={null} // LayerZero/V2/protocol/contracts/EndpointV2.sol /// @dev calling this function will clear the stored message and increment the lazyInboundNonce to the provided nonce /// @dev if a lot of messages are queued, the messages can be cleared with a smaller step size to prevent OOG /// @dev NOTE: this function does not change inboundNonce, it only changes the lazyInboundNonce up to the provided nonce function _clearPayload( address _receiver, uint32 _srcEid, bytes32 _sender, uint64 _nonce, bytes memory _payload ) internal returns (bytes32 actualHash) { uint64 currentNonce = lazyInboundNonce[_receiver][_srcEid][_sender]; if (_nonce > currentNonce) { unchecked { // try to lazily update the inboundNonce till the _nonce for (uint64 i = currentNonce + 1; i <= _nonce; ++i) { if (!_hasPayloadHash(_receiver, _srcEid, _sender, i)) revert Errors.LZ_InvalidNonce(i); } lazyInboundNonce[_receiver][_srcEid][_sender] = _nonce; } } // check the hash of the payload to verify the executor has given the proper payload that has been verified actualHash = keccak256(_payload); bytes32 expectedHash = inboundPayloadHash[_receiver][_srcEid][_sender][_nonce]; if (expectedHash != actualHash) revert Errors.LZ_PayloadHashNotFound(expectedHash, actualHash); // remove it from the storage delete inboundPayloadHash[_receiver][_srcEid][_sender][_nonce]; } ``` #### OApp Execution By default, the `OApp` standard inherits `OAppReceiver` which implements `lzReceive` called by `Endpoint` to execute message. * check `msg.sender` is `Endpoint`. * check the path is valid. * call internal `_lzReceive` to execute logic (developer should override to add specific use). ```solidity wrap theme={null} // LayerZero/V2/oapp/contracts/oapp/OAppReceiver.sol /** * @dev Entry point for receiving messages or packets from the endpoint. * @param _origin The origin information containing the source endpoint and sender address. * - srcEid: The source chain endpoint ID. * - sender: The sender address on the src chain. * - nonce: The nonce of the message. * @param _guid The unique identifier for the received LayerZero message. * @param _message The payload of the received message. * @param _executor The address of the executor for the received message. * @param _extraData Additional arbitrary data provided by the corresponding executor. * * @dev Entry point for receiving msg/packet from the LayerZero endpoint. */ function lzReceive( Origin calldata _origin, bytes32 _guid, bytes calldata _message, address _executor, bytes calldata _extraData ) public payable virtual { // Ensures that only the endpoint can attempt to lzReceive() messages to this OApp. if (address(endpoint) != msg.sender) revert OnlyEndpoint(msg.sender); // Ensure that the sender matches the expected peer for the source endpoint. if (_getPeerOrRevert(_origin.srcEid) != _origin.sender) revert OnlyPeer(_origin.srcEid, _origin.sender); // Call the internal OApp implementation of lzReceive. _lzReceive(_origin, _guid, _message, _executor, _extraData); } /** * @dev Internal function to implement lzReceive logic without needing to copy the basic parameter validation. */ function _lzReceive( Origin calldata _origin, bytes32 _guid, bytes calldata _message, address _executor, bytes calldata _extraData ) internal virtual; ``` In the original `_getPeerOrRevert` implementation, it can only assign one valid `sender` for each source chain, but developers can override this to allow multiple `senders` on one source chain. ```solidity wrap theme={null} // LayerZero/V2/oapp/contracts/oapp/OAppCore.sol /** * @notice Internal function to get the peer address associated with a specific endpoint; reverts if NOT set. * ie. the peer is set to bytes32(0). * @param _eid The endpoint ID. * @return peer The address of the peer associated with the specified endpoint. */ function _getPeerOrRevert(uint32 _eid) internal view virtual returns (bytes32) { bytes32 peer = peers[_eid]; if (peer == bytes32(0)) revert NoPeer(_eid); return peer; } ``` Developers should also override `OAppReceiver.allowInitializePath` so that the message can be successfully inserted into the `Endpoint`'s message channel (the Endpoint will call to check whether the path is valid). Special thanks to community member [**SennHanami**](https://x.com/HanamiSenn) for their contribution to this documentation page. You can read their full deep-dive at: [**Decode LayerZero V2**](https://senn.fun/decode-layerzero-v2#af1b7e67e9ad48c5aae184928aa4d209). # Architecture Source: https://docs.layerzero.network/v2/developers/evm/stablecoin-oft/architecture System design, inheritance hierarchy, and cross-chain message flow. ## Design Philosophy Security modules (fee, rate limiting, pause-by-destination) are independent, upgradeable layers that compose through inheritance without modifying core OFT logic. Allowlist controls live on the token (`ERC20Plus`), not in the extension stack. Token logic (`ERC20Plus`) is fully decoupled from cross-chain OFT logic (OFT variants). The token can exist without an OFT, and the OFT works with any ERC20 that implements `IERC20Metadata` and the necessary mint/burn functions with appropriate role-based access control. All storage uses EIP-7201 namespaced storage slots, preventing storage collisions across inheritance chains and making proxy upgrades safe by construction. ## Four-Layer Architecture ``` +----------------------------------------------------------+ IOFTExtended | Cross-Chain Extensions | | Fee | RateLimiter | PauseByID | CreditRedirect | | (composable, per-destination / global configuration) | +----------------------------------------------------------+ IOFT | Cross-Chain Transport | | BurnMint | LockUnlock | Native | BurnSelfMint | | (send/receive via LayerZero V2) | +==========================================================+ IERC20Plus | Token Controls | | Allowlist | Pause | Fund Recovery | | (apply to all token operations, not just cross-chain) | +----------------------------------------------------------+ IERC20 | Token Core | | ERC20 + ERC20Permit (transfer, approve, permit) | | (OpenZeppelin ERC20Upgradeable) | +----------------------------------------------------------+ ``` The system is two independent deployment units — a **Token Stack** and an **OFT Stack** — connected by the `IERC20Plus` interface. **Token Core (Layer 1):** Standard ERC20 functionality built on OpenZeppelin's `ERC20Upgradeable` and `ERC20PermitUpgradeable`. Transfers, approvals, balances, and gasless permit signatures. **Token Controls (Layer 2):** Allowlist (three-mode address restrictions), global pause, fund recovery, and role-based mint/burn access control. These apply to **all** token operations — a paused token blocks local transfers as well as cross-chain sends. Deployed as the `ERC20Plus` contract independently of any OFT. **Cross-Chain Transport (Layer 3):** Manages cross-chain send/receive. Each OFT variant implements a different transfer model (burn/mint, burn-self/mint, lock/unlock, native). **Cross-Chain Extensions (Layer 4):** Per-destination fee collection, rate limiting, pause-by-destination controls, and credit redirect to escrow for non-allowlisted inbound recipients. These modules intercept `_debit` and `_credit` to enforce policies before the underlying transfer executes. ## Inheritance Hierarchy ```mermaid theme={null} graph TB OAppUpgradeable["OAppUpgradeable
(LayerZero)"] OFTCoreBase["OFTCoreBaseUpgradeable
Core OFT send/receive logic"] OFTCoreRBAC["OFTCoreRBACUpgradeable
+ RBAC for OApp config"] OFTCoreExtended["OFTCoreExtendedRBACUpgradeable
+ Fee + RateLimiter + PauseByID + CreditRedirect"] BurnMintExtended["OFTBurnMintExtendedRBACUpgradeable
+ burn/mint with custom selectors"] LockUnlockExtended["OFTLockUnlockExtendedRBACUpgradeable
+ lock/unlock token transfers"] NativeExtended["OFTNativeExtendedRBACUpgradeable
+ native token wrapping"] OFTBurnMint["OFTBurnMint"] OFTBurnSelfMint["OFTBurnSelfMint"] OFTLockUnlock["OFTLockUnlock"] OFTNative["OFTNative"] OAppUpgradeable --> OFTCoreBase OFTCoreBase --> OFTCoreRBAC OFTCoreRBAC --> OFTCoreExtended OFTCoreExtended --> BurnMintExtended OFTCoreExtended --> LockUnlockExtended OFTCoreExtended --> NativeExtended BurnMintExtended --> OFTBurnMint BurnMintExtended --> OFTBurnSelfMint LockUnlockExtended --> OFTLockUnlock NativeExtended --> OFTNative ACEnumerable["AccessControlEnumerableUpgradeable
(OpenZeppelin)"] AC2Step["AccessControl2StepUpgradeable"] FeeRBAC["FeeRBACUpgradeable"] RateLimiterRBAC["RateLimiterRBACUpgradeable"] PauseByIDRBAC["PauseByIDRBACUpgradeable"] CreditRedirectRBAC["CreditRedirectRBACUpgradeable"] ACEnumerable --> AC2Step AC2Step -.->|inherited via| FeeRBAC AC2Step -.->|inherited via| RateLimiterRBAC AC2Step -.->|inherited via| PauseByIDRBAC AC2Step -.->|inherited via| CreditRedirectRBAC FeeRBAC -.->|mixin| OFTCoreExtended RateLimiterRBAC -.->|mixin| OFTCoreExtended PauseByIDRBAC -.->|mixin| OFTCoreExtended CreditRedirectRBAC -.->|mixin| OFTCoreExtended style OFTBurnMint fill:#34a853,color:#fff style OFTBurnSelfMint fill:#34a853,color:#fff style OFTLockUnlock fill:#34a853,color:#fff style OFTNative fill:#34a853,color:#fff ``` Each green box is a **deployable contract**. Everything above is abstract. The `Alt` variants (`OFTBurnMintAlt`, `OFTLockUnlockAlt`, `OFTBurnSelfMintAlt`) follow the same hierarchy but target `EndpointV2Alt` for chains where ERC20 tokens are used for gas fees. ## ERC20Plus Inheritance ```mermaid theme={null} graph TB ERC20Upgradeable["ERC20Upgradeable
(OpenZeppelin)"] ERC20PermitUpgradeable["ERC20PermitUpgradeable
(OpenZeppelin)"] AllowlistRBAC["AllowlistRBACUpgradeable
3-mode allowlist + RBAC"] PauseRBAC["PauseRBACUpgradeable
Global pause + RBAC"] ACEnumerable["AccessControlEnumerableUpgradeable
(OpenZeppelin)"] AC2Step["AccessControl2StepUpgradeable"] ERC20Plus["ERC20Plus
+ mint/burn + fund recovery"] ERC20Upgradeable --> ERC20PermitUpgradeable ERC20PermitUpgradeable --> ERC20Plus AllowlistRBAC --> ERC20Plus PauseRBAC --> ERC20Plus ACEnumerable --> AC2Step AC2Step -.->|inherited via| AllowlistRBAC AC2Step -.->|inherited via| PauseRBAC style ERC20Plus fill:#34a853,color:#fff ``` ## Cross-Chain Message Flow ### Send Path (Source Chain) ``` send() | v OFTCoreBaseUpgradeable.send() | v _debit() [overridden per variant] |-- whenNotPaused(_dstEid) <-- PauseByID |-- _debitView() <-- Fee calculation | |-- getFee(_dstEid, _amountLD) | |-- _removeDust() | +-- SlippageExceeded check |-- _outflow(_dstEid, _from, amount) <-- Rate limit | |-- Address exemption check | +-- Token bucket decay +-- Transfer/burn + collect fee <-- Allowlist & global pause | v _buildMsgAndOptions() | v _lzSend() --> LayerZero Endpoint | v emit OFTSent(guid, dstEid, from, amountSentLD, amountReceivedLD) ``` ### Receive Path (Destination Chain) ``` LayerZero Endpoint delivers message | v _lzReceive() | v _credit() [overridden per variant] |-- _inflow(_srcEid, _to, _amountLD) <-- Rate limit (inbound) |-- _redirectCredit(_to, amount) <-- Escrow if configured and not allowlisted +-- Mint/unlock/transfer to recipient <-- Or escrow, if redirected | v If compose message exists: +-- endpoint.sendCompose() --> downstream contract | v emit OFTReceived(guid, srcEid, to, amountReceivedLD) ``` ## Transfer Model Mechanics ### Burn/Mint ``` Source Chain Destination Chain +--------------------+ +--------------------+ | ERC20Plus | | ERC20Plus | | | | | | burn() | LayerZero V2 | mint() | | | -----------> | | | | | | | OFTBurnMint | | OFTBurnMint | +--------------------+ +--------------------+ ``` The OFT must hold `MINTER_ROLE` and `BURNER_ROLE` on the `ERC20Plus` token. Total supply across all chains remains constant. ### Burn-Self/Mint ``` Source Chain Destination Chain +--------------------+ +--------------------+ | ERC20Plus | | ERC20Plus | | | | | | transferFrom() | LayerZero V2 | mint() | | burn() | -----------> | | | | | | | OFTBurnSelfMint | | OFTBurnSelfMint | +--------------------+ +--------------------+ ``` First transfers tokens from user to OFT via ERC20 allowance, then burns using `burn(uint256)` selector. The OFT must hold `MINTER_ROLE` on the destination token. ### Lock/Unlock ``` Source Chain Destination Chain +--------------------+ +--------------------+ | ERC20Plus | | ERC20Plus | | | | | | transferFrom() | LayerZero V2 | transfer() | | (lock in OFT) | -----------> | | | | | | | OFTLockUnlock | | OFTLockUnlock | +--------------------+ +--------------------+ ``` Tokens are locked in the OFT contract on the source chain and unlocked from the OFT contract on the destination. Only one lock/unlock OFT should exist per mesh to prevent supply fragmentation. ### Native ``` Source Chain Destination Chain +--------------------+ +--------------------+ | | | | | msg.value | LayerZero V2 | .call{value} | | (ETH sent | -----------> | (ETH sent to | | with tx) | | recipient) | | | | | | OFTNative | | OFTNative | +--------------------+ +--------------------+ ``` The OFT wraps/unwraps native tokens. `msg.value` must include both the transfer amount and the LayerZero messaging fee. ## Upgradeability Model All contracts support Transparent Upgradeable Proxy (TUP) and Beacon proxy patterns: * **Proxy contract** holds state (storage) and delegates calls to the implementation * **Implementation contract** holds logic and is stateless * **EIP-7201 namespaced storage** ensures each module's storage is isolated at a deterministic slot, preventing collisions ## Next Steps * [ERC20Plus](/v2/developers/evm/stablecoin-oft/erc20plus) for the token core (layer 1) * [OFTs](/v2/developers/evm/stablecoin-oft/ofts) for the cross-chain transport (layer 3) * [Extensions](/v2/developers/evm/stablecoin-oft/extensions) for token controls and cross-chain extensions (layers 2 and 4) # ERC20Plus Source: https://docs.layerzero.network/v2/developers/evm/stablecoin-oft/erc20plus Enhanced upgradeable ERC20 token with enterprise controls: RBAC mint/burn, three-mode allowlist, global pause, ERC20Permit, and fund recovery. `ERC20Plus` is an enhanced, upgradeable ERC20 token that works consistently across EVM chains supported by LayerZero, without relying on native token standard quirks that vary by chain. It provides administrative controls directly at the token level. ## Feature Comparison | Feature | Standard ERC20 | `ERC20Plus` | | ------------------------------ | -------------- | ---------------------------------------------- | | Transfer / Approve / Allowance | Yes | Yes | | Role-based mint | No | Yes (`MINTER_ROLE`) | | Role-based burn | No | Yes (`BURNER_ROLE`) | | Allowlist (3-mode) | No | Yes (Open / Blacklist / Whitelist) | | Global pause | No | Yes (separate `PAUSER_ROLE` / `UNPAUSER_ROLE`) | | Fund recovery | No | Yes (`DEFAULT_ADMIN_ROLE`) | | ERC20Permit (EIP-2612) | Optional | Built-in | | Enumerable address lists | No | Yes (paginated blacklist/whitelist queries) | | Upgradeable | Depends | Yes (EIP-7201 namespaced storage) | | RBAC (AccessControl2Step) | No | Yes (OpenZeppelin) | ## Features ### Role-Based Minting and Burning `ERC20Plus` defines two roles for token supply management: ```solidity theme={null} bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); ``` * **MINTER\_ROLE**: Can call `mint(address _to, uint256 _amount)` to create new tokens * **BURNER\_ROLE**: Can call `burn(address _from, uint256 _amount)` to destroy tokens When used with OFTs, the OFT contract must be granted both `MINTER_ROLE` and `BURNER_ROLE` so it can mint tokens on the receive path and burn tokens on the send path. ### Allowlist, Pause, and Fund Recovery `ERC20Plus` includes a three-mode allowlist (Open / Blacklist / Whitelist), a global pause with separate pauser/unpauser roles, and a fund recovery function for compliance seizures from non-allowlisted addresses. Allowlist and pause apply to `transfer`, `transferFrom`, `mint`, and `burn`. These are documented in detail on the [Extensions](/v2/developers/evm/stablecoin-oft/extensions#token-level-controls-on-erc20plus) page alongside the OFT-level extensions. ### ERC20Permit (EIP-2612) Built-in support for gasless approvals via signed messages. Users can authorize token spending by signing an off-chain message instead of submitting an on-chain `approve` transaction: ```solidity theme={null} function permit( address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external; ``` Nonces are available via `nonces(address owner)`. ## Working with OFTs When `ERC20Plus` is used with the OFT Burn/Mint variant: 1. Deploy `ERC20Plus` on each chain 2. Deploy `OFTBurnMint` on each chain, pointing to the `ERC20Plus` token address 3. Grant `MINTER_ROLE` and `BURNER_ROLE` to the `OFTBurnMint` address on each chain's `ERC20Plus` 4. Set peers and LZ messaging configurations between `OFTBurnMint` contracts across chains The OFT uses configurable function selectors to call the token's mint/burn functions. For `ERC20Plus`, the default selectors are: * Mint: `0x40c10f19` (`mint(address,uint256)`) * Burn: `0x9dc29fac` (`burn(address,uint256)`) ## Next Steps * [OFTs](/v2/developers/evm/stablecoin-oft/ofts) for choosing the right variant * [RBAC Reference](/v2/developers/evm/stablecoin-oft/rbac-reference) for roles and access-controlled functions on `ERC20Plus` # Extensions Source: https://docs.layerzero.network/v2/developers/evm/stablecoin-oft/extensions Cross-chain extensions (Credit Redirect, Fee, Rate Limiter, PauseByID) and token-level controls (Allowlist, Pause). Stablecoin OFT enforces security policies at two levels (see [Architecture](/v2/developers/evm/stablecoin-oft/architecture)): * **Cross-chain extensions** (Credit Redirect, Fee, Rate Limiter, PauseByID) — mixed into the OFT via `OFTCoreExtendedRBACUpgradeable`. Fee, rate limit, and pause are configured per destination (EID); credit redirect is global. Together they intercept `_debit` and/or `_credit` on cross-chain sends and receives. * **Token controls** (Allowlist, Pause) — built into `ERC20Plus`, applied to **all** token operations including local transfers and mint/burn. Documented in detail on the [ERC20Plus](/v2/developers/evm/stablecoin-oft/erc20plus) page; summarized here for completeness. ## Overview | Module | Layer | Purpose | Granularity | Where Applied | | ------------------- | ----- | ----------------------------------------------------------------- | ------------------------- | ----------------------------------------------------- | | **Fee** | OFT | Collect basis-point fees on outbound transfers | Per-destination + default | `_debitView()` | | **Rate Limiter** | OFT | Token bucket rate limits with linear decay | Per-destination + default | `_debit()` and `_credit()` | | **PauseByID** | OFT | Halt transfers to/from specific destinations | Per-destination + default | `_debit()` modifier | | **Credit Redirect** | OFT | Redirect inbound credits for non-allowlisted recipients to escrow | Global (on OFT) | `_credit()` | | **Allowlist** | Token | Restrict token holders by address | Global (on `ERC20Plus`) | `transfer()` / `transferFrom()` / `mint()` / `burn()` | | **Pause** | Token | Halt all token operations globally | Global (on `ERC20Plus`) | `transfer()` / `transferFrom()` / `mint()` / `burn()` | ## Fee Module Collects fees in basis points (BPS) on outbound cross-chain transfers. Fees are deducted from the transfer amount before it reaches the destination chain. ### How It Works 1. User calls `send()` with `amountLD` 2. `_debitView()` calculates the fee: `fee = (amountLD * feeBps) / 10_000` 3. `amountReceivedLD = removeDust(amountLD - fee)` 4. `amountSentLD = amountLD` (the user pays the full amount) 5. The difference (`amountSentLD - amountReceivedLD`) is retained as fee ### Configuration **Default fee** applies to all destinations unless overridden: ```solidity theme={null} function setDefaultFeeBps(uint16 _feeBps) external; // FEE_CONFIG_MANAGER_ROLE ``` **Per-destination override:** ```solidity theme={null} function setFeeBps( uint256 _id, // Destination EID uint16 _feeBps, // Fee in BPS (0-10000) bool _enabled // true = use this override; false = fall back to default ) external; // FEE_CONFIG_MANAGER_ROLE ``` When `enabled` is `false`, the destination falls back to the default fee. **Fee settlement is push-based across all OFT variants.** When fees are collected during `_debit`, they are transferred immediately to the fee deposit address supplied at initialization. Treasury accounting should follow normal ERC20 `Transfer` (or native) inflows to that address rather than a separate withdrawal action. ### Constants ```solidity theme={null} uint16 public constant BPS_DENOMINATOR = 10_000; ``` A fee of 50 BPS = 0.50%. Setting `_feeBps > BPS_DENOMINATOR` reverts with `InvalidBps(feeBps)`. ### Roles | Role | Functions | | ------------------------- | ----------------------------------- | | `FEE_CONFIG_MANAGER_ROLE` | `setDefaultFeeBps()`, `setFeeBps()` | ## Rate Limiter Module Enforces transfer volume limits using a token bucket algorithm with linear decay. Supports per-destination limits for both outbound (send) and inbound (receive) directions. ### How It Works The rate limiter uses a token bucket model: 1. Outbound transfers consume capacity; inbound transfers replenish it (when net accounting is enabled) 2. Capacity regenerates linearly over the configured time window 3. If a transfer would exceed available capacity, it reverts with `RateLimitExceeded` **Example:** With a limit of 1,000,000 tokens and a window of 3,600 seconds (1 hour), the regeneration rate is \~277.78 tokens/second: * At t=0: Available = 1,000,000 * User sends 800,000 tokens. Available = 200,000 * At t=1800 (30 min): 500,000 regenerated. Available = 700,000 * At t=3600 (1 hr): Fully regenerated. Available = 1,000,000 * If 300,000 tokens arrive inbound at any point, available increases by 300,000 (capped at limit) ### Configuration **Global configuration:** ```solidity theme={null} function setRateLimitGlobalConfig(RateLimitGlobalConfig memory _globalConfig) external; // RATE_LIMITER_MANAGER_ROLE struct RateLimitGlobalConfig { bool useGlobalState; // Use single bucket for all destinations bool isGloballyDisabled; // Disable all rate limiting } ``` **Per-destination configuration:** ```solidity theme={null} function setRateLimitConfigs(SetRateLimitConfigParam[] calldata _params) external; // RATE_LIMITER_MANAGER_ROLE struct RateLimitConfig { bool overrideDefaultConfig; // true = use this config; false = use default bool outboundEnabled; // Enable outbound rate limit bool inboundEnabled; // Enable inbound rate limit bool netAccountingEnabled; // Offset outflow with inflows bool addressExemptionEnabled; // Allow per-address exemptions uint96 outboundLimit; // Max outbound tokens in window uint96 inboundLimit; // Max inbound tokens in window uint32 outboundWindow; // Outbound decay window (seconds) uint32 inboundWindow; // Inbound decay window (seconds) } ``` **Manual state override** (for emergency adjustments): ```solidity theme={null} function setRateLimitStates(SetRateLimitStateParam[] calldata _params) external; // RATE_LIMITER_MANAGER_ROLE struct RateLimitState { uint96 outboundUsage; // Current outbound usage uint96 inboundUsage; // Current inbound usage uint40 lastUpdated; // Timestamp (cannot be in the future) } ``` **Address exemptions:** ```solidity theme={null} function setRateLimitAddressExemptions( SetRateLimitAddressExceptionParam[] calldata _exemptions ) external; // RATE_LIMITER_MANAGER_ROLE struct SetRateLimitAddressExceptionParam { address user; bool isExempt; } ``` Exemptions only apply when `addressExemptionEnabled` is `true` in the destination's config. **Checkpoint** (call before changing limits or windows): ```solidity theme={null} function checkpointRateLimits(uint256[] calldata _ids) external; // RATE_LIMITER_MANAGER_ROLE ``` Writes decayed usages to storage so new config applies to the current state rather than stale values. ### Net vs Gross Accounting When `netAccountingEnabled` is `true`: * Outbound transfers reduce inbound usage (and vice versa) * This allows "round-trip" capacity: if 500k tokens leave and 500k arrive, net usage is zero When `netAccountingEnabled` is `false`: * Outbound and inbound are tracked independently * Each direction has its own separate bucket ### Scaling Rate limit amounts are stored as `uint96`. For tokens with amounts exceeding `type(uint96).max` (\~79 billion with 18 decimals), use the `SCALE_DECIMALS` constructor parameter to downscale amounts. For example, `SCALE_DECIMALS = 6` divides all amounts by `10^6` before storing. ### Roles | Role | Functions | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `RATE_LIMITER_MANAGER_ROLE` | `setRateLimitGlobalConfig()`, `setRateLimitConfigs()`, `setRateLimitStates()`, `setRateLimitAddressExemptions()`, `checkpointRateLimits()` | ## PauseByID Module Per-destination pause controls that halt outbound transfers to specific chains without affecting the rest of the network. ### How It Works The `_debit()` function includes a `whenNotPaused(_dstEid)` modifier. If the destination EID is paused, the transaction reverts with `Paused(uint256 id)`. Pause logic evaluates: 1. Is there a specific `PauseConfig` for this destination with `enabled = true`? 2. If yes, use that config's `paused` value 3. If no, use the `defaultPaused` value ### Configuration **Default pause** (applies to all destinations without specific config): ```solidity theme={null} function setDefaultPaused(bool _paused) external; // PAUSER_ROLE or UNPAUSER_ROLE ``` **Per-destination pause:** ```solidity theme={null} function setPaused(SetPausedParam[] calldata _params) external; // PAUSER_ROLE or UNPAUSER_ROLE struct SetPausedParam { uint256 id; // Destination EID bool paused; // Whether to pause bool enabled; // true = use this config; false = fall back to default } ``` ### Use Cases * **Chain compromise:** Pause a single destination without halting all cross-chain operations * **Maintenance:** Temporarily halt transfers to a chain during upgrades * **Regulatory action:** Block transfers to/from a specific chain ### Interaction with quoteOFT When a destination is paused, `quoteOFT()` returns `maxAmountLD = 0` in the `OFTLimit`, signaling to UIs that no transfer is possible. ## Credit Redirect When configured, inbound `_credit` checks an allowlist and, if the recipient is not allowlisted, credits an escrow address instead. The message still completes; the OFT emits `CreditRedirected(from, to, amountLD)` (`from` = intended recipient, `to` = escrow). `_credit` returns `0` for `amountReceivedLD` when redirected, so `OFTReceived` / compose are not treated as payment to the intended recipient. ```solidity theme={null} function setCreditRedirectConfig(CreditRedirectConfig calldata _config) external; // DEFAULT_ADMIN_ROLE // allowlist and escrow must both be set, or both address(0) to disable struct CreditRedirectConfig { address allowlist; // Must implement isAllowlisted(address) address escrow; // Receives redirected credits } ``` Point `allowlist` at the token (`ERC20Plus`) or another `isAllowlisted` contract. Keep `escrow` allowlisted so the mint or transfer succeeds. With redirect disabled, inbound credits follow normal token allowlist and pause rules on `mint` / transfer. Failed receives can be cleared with messaging channel ops (`nilify` / `skip`) — see [RBAC Reference](/v2/developers/evm/stablecoin-oft/rbac-reference). ## Token-Level Controls (on `ERC20Plus`) The following controls live on the token itself, not on the OFT. They apply to **all** token operations — local transfers and cross-chain sends alike. ### Allowlist The allowlist system operates in one of three modes at any time: | Mode | Behavior | Who Can Transfer | | ------------- | ----------------------------- | ------------------------------------- | | **Open** | No restrictions | Everyone | | **Blacklist** | Block specific addresses | Everyone except blacklisted addresses | | **Whitelist** | Allow only specific addresses | Only whitelisted addresses | **Mode transitions** are controlled by `DEFAULT_ADMIN_ROLE`. Switching modes does not clear existing lists — the blacklist and whitelist are maintained independently and apply only when their respective mode is active. Both lists are implemented as OpenZeppelin `EnumerableSet.AddressSet`, supporting: * `blacklistedCount()` / `whitelistedCount()` for total counts * `getBlacklist(offset, limit)` / `getWhitelist(offset, limit)` for paginated enumeration * `isBlacklisted(address)` / `isWhitelisted(address)` for individual queries The allowlist check (`isAllowlisted`) is enforced on `transfer` and `transferFrom` (sender and recipient), `mint` (recipient), and `burn` (source). | Role | Functions | | -------------------- | -------------------- | | `DEFAULT_ADMIN_ROLE` | `setAllowlistMode()` | | `BLACKLISTER_ROLE` | `setBlacklisted()` | | `WHITELISTER_ROLE` | `setWhitelisted()` | ### Fund Recovery For compliance scenarios (e.g., court orders, sanctions enforcement), addresses holding `DEFAULT_ADMIN_ROLE` can transfer tokens away from non-allowlisted addresses: ```solidity theme={null} function recoverFunds(address _from, address _to, uint256 _amount) external; ``` `_from` must NOT be allowlisted under the current mode. Attempting to recover from an allowlisted address reverts with `CannotRecoverFromAllowlisted(address user)`. ### Pause (Global) Halts all `transfer`, `transferFrom`, `mint`, and `burn` calls. Unlike `PauseByID` (which targets individual destinations), global pause blocks everything — local and cross-chain. | Role | Functions | | --------------- | ----------- | | `PAUSER_ROLE` | `pause()` | | `UNPAUSER_ROLE` | `unpause()` | The pause/unpause split ensures a compromised pauser key cannot also undo a legitimate security pause. ## Execution Order On an outbound `send()`, modules execute in this order: ``` 1. PauseByID → whenNotPaused(dstEid) modifier on _debit() 2. Fee → _debitView() calculates fee, reduces amountReceivedLD 3. Rate Limiter → _outflow() checks and updates outbound bucket 4. Token Transfer → burn/lock/wrap the tokens ``` On an inbound receive (`_lzReceive`): ``` 1. Rate Limiter → _inflow() checks and updates inbound bucket 2. Credit Redirect → _redirectCredit() (escrow if configured and recipient not allowlisted) 3. Token Transfer → mint/unlock/unwrap to recipient (or escrow) ``` ## Next Steps * [RBAC Reference](/v2/developers/evm/stablecoin-oft/rbac-reference) for the complete role-to-function mapping * [OFTs](/v2/developers/evm/stablecoin-oft/ofts) for deployable variants and initialization (including fee deposit) # OFT Variants Source: https://docs.layerzero.network/v2/developers/evm/stablecoin-oft/ofts Cross-chain transfer OFTs: BurnMint, BurnSelfMint, LockUnlock, and Native variants. Stablecoin OFT provides multiple OFT variants to support different token economics and deployment scenarios. Each variant implements a different cross-chain transfer model. All share the same extension stack (fee, rate limiting, pause). ## Decision Matrix | Scenario | OFT | Why | | --------------------------------------------------------- | ----------------- | --------------------------------------------------------------------- | | Token with permissioned burn and mint | `OFTBurnMint` | Burn on source, mint on destination. No locked collateral. | | Token with permissionless self-burn and permissioned mint | `OFTBurnSelfMint` | Transfers to OFT via allowance, then self-burns. Mint on destination. | | Existing token with supply on one chain | `OFTLockUnlock` | Lock on source, unlock on destination. No supply change. | | Native gas token (ETH, MATIC, etc.) | `OFTNative` | Wraps `msg.value` for cross-chain transfer. | | Target chain uses ERC20 for gas fees | **Alt variants** | Same logic but built for `EndpointV2Alt`. | ## OFTBurnMint Burns tokens on the source chain and mints them on the destination chain. Supports any token that exposes mint and burn functions with `(address, uint256)` parameters. The OFT must be granted the required roles (e.g., `MINTER_ROLE`, `BURNER_ROLE`) to call these functions. **Key feature:** Configurable function selectors allow the OFT to call non-standard mint/burn function names. ### Constructor Parameters | Parameter | Type | Description | | --------------------------- | --------- | -------------------------------------------------------------------------- | | `_token` | `address` | Underlying ERC20 token address (must implement `IERC20Metadata`) | | `_burnerMinter` | `address` | Contract with burn/mint capabilities (can differ from `_token`) | | `_endpoint` | `address` | LayerZero EndpointV2 address | | `_approvalRequired` | `bool` | Whether the OFT needs ERC20 approval to burn | | `_burnSelector` | `bytes4` | Function selector for burn, e.g., `0x9dc29fac` for `burn(address,uint256)` | | `_mintSelector` | `bytes4` | Function selector for mint, e.g., `0x40c10f19` for `mint(address,uint256)` | | `_rateLimiterScaleDecimals` | `uint8` | Decimals to scale rate limit amounts (usually `0`) | ### Common Selector Values | Function Signature | Selector | | ------------------------- | ------------ | | `mint(address,uint256)` | `0x40c10f19` | | `burn(address,uint256)` | `0x9dc29fac` | | `issue(address,uint256)` | `0x867904b4` | | `redeem(address,uint256)` | `0x1e9a6950` | ### Initialization Parameters | Parameter | Type | Description | | --------------- | --------- | ---------------------------------------------------------------- | | `_initialAdmin` | `address` | Address to be granted `DEFAULT_ADMIN_ROLE` and endpoint delegate | | `_feeDeposit` | `address` | Address that will receive any accrued fees | ```solidity theme={null} function initialize(address _initialAdmin, address _feeDeposit) public initializer; ``` ## OFTBurnSelfMint For tokens with a permissionless self-burn function (e.g., OpenZeppelin's `ERC20Burnable.burn(uint256)`) and a permissioned mint function. On the send path, the OFT transfers tokens from the user to itself via ERC20 allowance, then calls the self-burn selector to destroy them. On the receive path, it mints via the mint selector like `OFTBurnMint`. ### Constructor Parameters | Parameter | Type | Description | | --------------------------- | --------- | -------------------------------------------- | | `_token` | `address` | Token address that is also the burner/minter | | `_endpoint` | `address` | LayerZero EndpointV2 address | | `_burnSelector` | `bytes4` | Function selector for burn | | `_mintSelector` | `bytes4` | Function selector for mint | | `_rateLimiterScaleDecimals` | `uint8` | Decimals to scale rate limit amounts | ### Common Selector Values for OFTBurnSelfMint | Function Signature | Selector | | ------------------------ | ------------ | | `mint(address,uint256)` | `0x40c10f19` | | `burn(uint256)` | `0x42966c68` | | `issue(address,uint256)` | `0x867904b4` | | `redeem(uint256)` | `0xdb006a75` | ## OFTLockUnlock Locks tokens in the OFT contract on the source chain and unlocks (transfers) them from the OFT on the destination chain. No minting or burning occurs. ### Constructor Parameters | Parameter | Type | Description | | --------------------------- | --------- | ---------------------------------------------------------------- | | `_token` | `address` | Underlying ERC20 token address (must implement `IERC20Metadata`) | | `_endpoint` | `address` | LayerZero EndpointV2 address | | `_rateLimiterScaleDecimals` | `uint8` | Decimals to scale rate limit amounts | `approvalRequired` is always `true` for lock/unlock since users must approve the OFT to transfer their tokens. **Only one lock/unlock OFT should exist per OFT mesh.** If multiple lock/unlock OFTs exist on different chains, locked supply becomes fragmented and the system cannot guarantee solvency. ## OFTNative Wraps native tokens (ETH, MATIC, AVAX, etc.) for cross-chain transfer. Users send native tokens as `msg.value` along with the `send()` call. ### Constructor Parameters | Parameter | Type | Description | | --------------------------- | --------- | ----------------------------------------- | | `_localDecimals` | `uint8` | Decimals of the native token (18 for ETH) | | `_endpoint` | `address` | LayerZero EndpointV2 address | | `_rateLimiterScaleDecimals` | `uint8` | Decimals to scale rate limit amounts | ### Send Behavior The overridden `send()` function validates that `msg.value` equals exactly `_fee.nativeFee + _sendParam.amountLD`: ```solidity theme={null} uint256 requiredMsgValue = _fee.nativeFee + _sendParam.amountLD; if (msg.value != requiredMsgValue) { revert IncorrectMessageValue(msg.value, requiredMsgValue); } ``` ### Receive Behavior Credits are sent as native token transfers using low-level `.call{value}()`. If the transfer fails (e.g., recipient is a contract without a `receive` function), the transaction reverts with `CreditFailed(to, amountLD, revertData)`. ### Properties * `token()` returns `address(0)` since there is no ERC20 token * `approvalRequired()` returns `false` since `msg.value` is used directly * Dust removal is not applied to `amountLD` since native token amounts always equal `amountSentLD` ## Alt Variants Some chains use an ERC20 token for gas fees instead of a native token. Because gas fees are paid in an ERC20, the sender must approve the endpoint to spend the fee token before sending a message. This changes the standard `send()` workflow: users need an additional ERC20 approval step for the fee token, and the endpoint pulls fees via `transferFrom` rather than accepting `msg.value`. These chains use `EndpointV2Alt` instead of `EndpointV2` to handle this difference. The Alt OFT variants are identical to their standard counterparts except they target `EndpointV2Alt`: | Standard Variant | Alt Variant | | ----------------- | -------------------- | | `OFTBurnMint` | `OFTBurnMintAlt` | | `OFTBurnSelfMint` | `OFTBurnSelfMintAlt` | | `OFTLockUnlock` | `OFTLockUnlockAlt` | Alt variants inherit from the corresponding `ExtendedRBACAltUpgradeable` contracts. Constructor parameters and behavior are otherwise the same. ## Multi-Chain Topology ### Burn/Mint ``` +--------------+ +--------------+ | Chain A |<--->| Chain B | | OFTBurnMint | | OFTBurnMint | +------+-------+ +------+-------+ | | +---------+----------+ | +--------------+ | Chain C | | OFTBurnMint | +--------------+ ``` All chains are equal peers. Each OFT burns on send and mints on receive. Total supply across all chains stays constant. Works the same way with `OFTBurnSelfMint`. ### Lock/Unlock ``` +--------------+ +--------------+ | Chain B | | Chain C | | OFTBurnMint | | OFTBurnMint | +------+-------+ +------+-------+ | | +----------+----------+ | +----------------+ | Chain A | | OFTLockUnlock | +----------------+ ``` One chain (the token's home chain) deploys `OFTLockUnlock`, which locks tokens on send and unlocks on receive. All other chains deploy `OFTBurnMint`, which burns on send and mints on receive. This keeps a single pool of locked collateral on Chain A backing all circulating supply on remote chains. ## Next Steps * [Extensions](/v2/developers/evm/stablecoin-oft/extensions) for fee, rate limiting, and pause configuration * [RBAC Reference](/v2/developers/evm/stablecoin-oft/rbac-reference) for roles on extended OFTs # Stablecoin OFT Overview Source: https://docs.layerzero.network/v2/developers/evm/stablecoin-oft/overview Fine-grained, institutional-grade control for cross-chain stablecoin flows. Stablecoin OFT is a LayerZero V2–based contract system designed for organizations that need to move tokens across blockchains while maintaining controls suited for regulated environments. ## What It Does Stablecoin OFT enables an organization to deploy or extend tokens that operate natively across multiple EVM blockchains. A user on Ethereum can send tokens to Arbitrum, Base, or any supported chain in a single transaction. Stablecoin OFT handles the cross-chain messaging, token accounting, and security enforcement. Unlike standard bridge solutions, Stablecoin OFT gives token issuers direct control over: * **Who** can transfer (allowlist/blocklist per address) * **Where** tokens can move (per-destination pause controls) * **How much** can move (per-destination rate limits) * **What it costs** (configurable fee collection per destination) * **Who manages what** (granular role separation across all operations) ## Comparison: Standard OFT vs Stablecoin OFT | Capability | Standard OFT | Stablecoin OFT | | -------------------------------- | ----------------------- | -------------------------------------------------------- | | Cross-chain transfers | Yes | Yes | | Transfer models | Native, OFT (burn/mint) | Burn/Mint, Burn-Self/Mint, Lock/Unlock, Native | | Access control | Owner-based | Role-based | | Fee collection | No | Per-destination BPS fees | | Rate limiting | No | Per-destination token bucket with inbound/outbound | | Pause controls | No | Per-destination + global | | Allowlist/Blocklist | No | Three-mode (Open, Blacklist, Whitelist); mint gated | | Credit redirect | No | Escrow for non-allowlisted inbound credits | | Messaging channel RBAC | Delegate-only | `clear` / `skip` / `burn` / `nilify` via dedicated roles | | Fund recovery | No | Yes (compliance seizure from non-allowlisted) | | Upgradeability | Optional | Built-in (EIP-7201 namespaced storage) | | Configurable mint/burn interface | No | Yes (custom function selectors) | | ERC20Permit (gasless approvals) | Depends on token | Built-in | | Enumerable address lists | No | Yes (paginated blacklist/whitelist queries) | ## Supported Transfer Models | Model | How It Works | Use Case | | ------------------ | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | **Burn/Mint** | Burns tokens on source chain, mints on destination. Requires `MINTER_ROLE` and `BURNER_ROLE` permissions. | New tokens with no pre-existing supply on any chain | | **Burn-Self/Mint** | Transfers tokens from user to contract using ERC20 allowance, then burns using `burn(uint256)` selector | Tokens that follow OpenZeppelin's `ERC20Burnable.burn(uint256)` or similar pattern | | **Lock/Unlock** | Locks tokens on source chain, unlocks on destination | Existing tokens with supply already on one chain | | **Native** | Wraps native tokens (e.g., ETH) for cross-chain transfer | Moving native gas tokens across chains | **Fee-on-transfer and rebasing tokens are not supported.** The OFT debit/credit accounting assumes lossless ERC20 transfers. ## Security Posture All contracts extend OpenZeppelin's audited upgradeable libraries and are [independently audited](/v2/resources/audits) by multiple firms. Role separation ensures no single key can both configure a policy and extract value — pausing and unpausing use different roles, fee configuration is separate from mint/burn authority, and cross-chain fees are pushed to a configured deposit address during sends. Every state-changing operation emits an indexed event for off-chain monitoring. ## Next Steps * **Technical leaders:** Continue to [Architecture](/v2/developers/evm/stablecoin-oft/architecture) for system design details * **Security teams:** See [Security and Compliance](/v2/developers/evm/stablecoin-oft/security-compliance) for the full threat model * **Integration engineers:** See [OFTs](/v2/developers/evm/stablecoin-oft/ofts) for deployment guidance, [`ERC20Plus`](/v2/developers/evm/stablecoin-oft/erc20plus) for the token layer, and [Extensions](/v2/developers/evm/stablecoin-oft/extensions) for fee, rate limiting, pause, and credit redirect configuration # RBAC Reference Source: https://docs.layerzero.network/v2/developers/evm/stablecoin-oft/rbac-reference Complete role-based access control reference for Stablecoin OFT and ERC20Plus with role definitions, function matrices, and recommended assignment strategies. Complete role-based access control reference for Stablecoin OFT and `ERC20Plus`. All roles use `AccessControl2StepUpgradeable` with two-step admin transfer and `keccak256` identifiers. ## Stablecoin OFT Applies to `OFTBurnMint`, `OFTBurnSelfMint`, `OFTLockUnlock`, `OFTNative`, and the alt variants of `OFTBurnMint`, `OFTBurnSelfMint`, and `OFTLockUnlock`. | Role | Source | Used For | | -------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `DEFAULT_ADMIN_ROLE` | `AccessControl2StepUpgradeable` | `setPeer`, `setEnforcedOptions`, `setMsgInspector`, `setFeeDeposit`, `setCreditRedirectConfig`, delegate operations | | `FEE_CONFIG_MANAGER_ROLE` | `FeeConfigRBACUpgradeable` | `setDefaultFeeBps`, `setFeeBps` | | `RATE_LIMITER_MANAGER_ROLE` | `RateLimiterRBACUpgradeable` | `setRateLimitGlobalConfig`, `setRateLimitConfigs`, `setRateLimitStates`, `setRateLimitAddressExemptions`, `checkpointRateLimits` | | `PAUSER_ROLE` | `PauseByIDRBACUpgradeable` | `setDefaultPaused` (when pausing), `setPaused` (when effectively pausing) | | `UNPAUSER_ROLE` | `PauseByIDRBACUpgradeable` | `setDefaultPaused` (when unpausing), `setPaused` (when effectively unpausing or no-op) | | `MESSAGING_CHANNEL_MANAGER_ROLE` | `OAppMessagingChannelRBACUpgradeable` | `clear`, `skip`, `burn` (non-reversible channel ops) | | `MESSAGE_NILIFIER_ROLE` | `OAppMessagingChannelRBACUpgradeable` | `nilify` (reversible; can be re-verified) | `DEFAULT_ADMIN_ROLE` is synchronized with `delegate`. The `setDelegate` function always reverts. ## ERC20Plus | Role | Source | Used For | | -------------------- | ------------------------------- | -------------------------------------------------- | | `DEFAULT_ADMIN_ROLE` | `AccessControl2StepUpgradeable` | Admin transfer, set allowlist mode, `recoverFunds` | | `MINTER_ROLE` | Declared locally | `mint` | | `BURNER_ROLE` | Declared locally | `burn` | | `BLACKLISTER_ROLE` | `AllowlistRBACUpgradeable` | Blacklist addresses | | `WHITELISTER_ROLE` | `AllowlistRBACUpgradeable` | Whitelist addresses | | `PAUSER_ROLE` | `PauseRBACUpgradeable` | Pause | | `UNPAUSER_ROLE` | `PauseRBACUpgradeable` | Unpause | ## Role-to-Function Matrix ### ERC20Plus Functions | Function | Required Role | | ----------------------------------------- | -------------------- | | `mint(address, uint256)` | `MINTER_ROLE` | | `burn(address, uint256)` | `BURNER_ROLE` | | `recoverFunds(address, address, uint256)` | `DEFAULT_ADMIN_ROLE` | | `setAllowlistMode(AllowlistMode)` | `DEFAULT_ADMIN_ROLE` | | `setBlacklisted(SetAllowlistParam[])` | `BLACKLISTER_ROLE` | | `setWhitelisted(SetAllowlistParam[])` | `WHITELISTER_ROLE` | | `pause()` | `PAUSER_ROLE` | | `unpause()` | `UNPAUSER_ROLE` | ### OFT Functions | Function | Required Role | | -------------------------------------------------------------------- | -------------------------------- | | `setDefaultFeeBps(uint16)` | `FEE_CONFIG_MANAGER_ROLE` | | `setFeeBps(uint256, uint16, bool)` | `FEE_CONFIG_MANAGER_ROLE` | | `setRateLimitGlobalConfig(RateLimitGlobalConfig)` | `RATE_LIMITER_MANAGER_ROLE` | | `setRateLimitConfigs(SetRateLimitConfigParam[])` | `RATE_LIMITER_MANAGER_ROLE` | | `setRateLimitStates(SetRateLimitStateParam[])` | `RATE_LIMITER_MANAGER_ROLE` | | `setRateLimitAddressExemptions(SetRateLimitAddressExceptionParam[])` | `RATE_LIMITER_MANAGER_ROLE` | | `checkpointRateLimits(uint256[])` | `RATE_LIMITER_MANAGER_ROLE` | | `setDefaultPaused(bool)` | `PAUSER_ROLE` or `UNPAUSER_ROLE` | | `setPaused(SetPausedParam[])` | `PAUSER_ROLE` or `UNPAUSER_ROLE` | | `setPeer(uint32, bytes32)` | `DEFAULT_ADMIN_ROLE` | | `setEnforcedOptions(EnforcedOptionParam[])` | `DEFAULT_ADMIN_ROLE` | | `setMsgInspector(address)` | `DEFAULT_ADMIN_ROLE` | | `setFeeDeposit(address)` | `DEFAULT_ADMIN_ROLE` | | `setCreditRedirectConfig(CreditRedirectConfig)` | `DEFAULT_ADMIN_ROLE` | | `clear(Origin, bytes32, bytes)` | `MESSAGING_CHANNEL_MANAGER_ROLE` | | `skip(uint32, bytes32, uint64)` | `MESSAGING_CHANNEL_MANAGER_ROLE` | | `burn(uint32, bytes32, uint64, bytes32)` | `MESSAGING_CHANNEL_MANAGER_ROLE` | | `nilify(uint32, bytes32, uint64, bytes32)` | `MESSAGE_NILIFIER_ROLE` | ### Permissionless Functions These functions can be called by anyone: | Function | Contract | | ----------------------------------------- | ------------------------------------------ | | `send(SendParam, MessagingFee, address)` | OFTs | | `quoteSend(SendParam, bool)` | OFTs | | `quoteOFT(SendParam)` | OFTs | | `token()` | OFTs | | `approvalRequired()` | OFTs | | `oftVersion()` | OFTs | | `sharedDecimals()` | OFTs | | `transfer(address, uint256)` | `ERC20Plus` (subject to allowlist + pause) | | `transferFrom(address, address, uint256)` | `ERC20Plus` (subject to allowlist + pause) | | `approve(address, uint256)` | `ERC20Plus` | | `permit(...)` | `ERC20Plus` | | `defaultFeeBps()` | OFTs | | `feeBps(uint256)` | OFTs | | `getRateLimitGlobalConfig()` | OFTs | | `rateLimits(uint256)` | OFTs | | `getRateLimitUsages(uint256)` | OFTs | | `isRateLimitAddressExempt(address)` | OFTs | | `isPaused(uint256)` | OFTs | | `defaultPaused()` | OFTs | | `pauseConfig(uint256)` | OFTs | | `creditRedirectConfig()` | OFTs | | `allowlistMode()` | `ERC20Plus` | | `isAllowlisted(address)` | `ERC20Plus` | | `isBlacklisted(address)` | `ERC20Plus` | | `isWhitelisted(address)` | `ERC20Plus` | | `blacklistedCount()` | `ERC20Plus` | | `whitelistedCount()` | `ERC20Plus` | | `getBlacklist(uint256, uint256)` | `ERC20Plus` | | `getWhitelist(uint256, uint256)` | `ERC20Plus` | ## Role Separation Principles **Pauser / Unpauser** — A compromised pauser key can halt the system (disruptive, but funds remain safe). If that same key could also unpause, an attacker could undo a legitimate security pause. Keeping them separate means the security team can pause fast, and unpausing requires a different authorization path (typically a governance multisig). **Fee config manager** — A compromised `FEE_CONFIG_MANAGER_ROLE` can set high fee rates, but fee proceeds are pushed to the fee deposit address during `_debit` as part of the send path; the fee config manager cannot retarget that destination. Use a multisig for fee administration and monitor `DefaultFeeBpsSet` / `FeeBpsSet` events. **Minter / Burner** — Different risk profiles. A compromised minter inflates supply; a compromised burner destroys user funds. In practice, both roles should only be granted to OFT contracts, never to EOAs. **Blacklister / Whitelister** — Different compliance functions and different teams. Blacklisting is reactive (sanctions, fraud), typically handled by compliance or security. Whitelisting is proactive (KYC onboarding), typically handled by an onboarding team. Separating them matches the org chart. **Messaging channel manager / Message nilifier** — `clear`, `skip`, and `burn` are permanent channel advances; `nilify` can be reversed by re-verification. Separating them lets ops wallets handle blocked or rejected messages without irreversible channel control. Use `nilify` for policy rejects (e.g. extra-context DVNs); use `skip` / `clear` / `burn` to advance or permanently dispose of a nonce. ## Next Steps * [ERC20Plus](/v2/developers/evm/stablecoin-oft/erc20plus) for token-layer behavior and interfaces * [Extensions](/v2/developers/evm/stablecoin-oft/extensions) for fee, rate limit, pause, and credit redirect configuration on OFTs * [Security and Compliance](/v2/developers/evm/stablecoin-oft/security-compliance) for operational security recommendations # Security and Compliance Source: https://docs.layerzero.network/v2/developers/evm/stablecoin-oft/security-compliance Security model, compliance features, threat mitigations, and operational security recommendations. ## Security Model Overview All core contracts extend OpenZeppelin's audited upgradeable libraries (AccessControl2StepUpgradeable, ERC20Upgradeable, ERC20PermitUpgradeable, Initializable). Stablecoin OFT contracts are [independently audited](/v2/resources/audits). Distinct roles enforce separation of duties. Critical pairs are split by design (pauser vs unpauser, minter vs burner, fee admin vs token mint authority, messaging channel manager vs message nilifier). EIP-7201 namespaced storage eliminates storage collision risks during upgrades. Proxy contracts must be deployed and initialized atomically to prevent front-running. Multiple independent enforcement layers (pause, rate limit, allowlist, fee, credit redirect) operate simultaneously. Compromise of one layer does not disable the others. ## Compliance Features ### KYC/AML Enforcement via Allowlist The three-mode allowlist system directly supports compliance workflows: | Compliance Requirement | Implementation | | --------------------------- | ----------------------------------------------------------------------------------------- | | **KYC-only transfers** | Whitelist mode: only verified addresses can send/receive | | **Sanctions screening** | Blacklist mode: block sanctioned addresses while allowing everyone else | | **Unrestricted operations** | Open mode: no address restrictions | | **Gradual rollout** | Start in Whitelist mode for controlled launch, switch to Blacklist after onboarding phase | Mode transitions can be performed instantly by `DEFAULT_ADMIN_ROLE` without requiring contract upgrades. ### Credit Redirect for Non-Allowlisted Recipients `setCreditRedirectConfig` on the OFT redirects inbound credits for non-allowlisted recipients to an escrow address and emits `CreditRedirected`, so the message can complete without crediting a restricted address. When redirected, `_credit` reports `amountReceivedLD = 0` to `OFTReceived` / compose. When redirect is disabled, those credits follow normal token allowlist and pause rules on mint/transfer. Messaging channel roles (`nilify` / `skip`) can clear paths that fail or should be ignored. ### Fund Recovery for Regulatory Actions The `recoverFunds()` function enables compliance-mandated seizures: ```solidity theme={null} function recoverFunds(address _from, address _to, uint256 _amount) external; // Requires: DEFAULT_ADMIN_ROLE // Constraint: _from must NOT be allowlisted ``` **Prerequisite:** Blacklist mode must be active (set by `DEFAULT_ADMIN_ROLE`). Workflow: 1. Compliance team blacklists the target address via `BLACKLISTER_ROLE` 2. Default admin calls `recoverFunds()` to move tokens to a designated custody address The constraint that `_from` must not be allowlisted ensures recovery cannot be used against compliant users. The function reverts with `CannotRecoverFromAllowlisted` if attempted. ### Per-Destination Controls Per-destination pause (`PauseByID`) enables targeted responses when a specific chain requires isolation — whether for regulatory reasons, security incidents, or maintenance: * Pause transfers to/from a specific destination chain * Maintain normal operations on all other chains * No contract upgrade required ### Audit Trail via Events Every state-changing operation emits an indexed event: | Category | Events | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | **Transfers** | `Transfer`, `OFTSent`, `OFTReceived` | | **Allowlist** | `AllowlistModeUpdated`, `BlacklistUpdated`, `WhitelistUpdated` | | **Credit Redirect** | `CreditRedirectConfigSet`, `CreditRedirected` | | **Pause** | `PauseSet`, `DefaultPauseSet` | | **Fees** | `DefaultFeeBpsSet`, `FeeBpsSet` (fee proceeds also appear as standard ERC20 `Transfer` or native transfers to the configured fee deposit address) | | **Rate Limits** | `RateLimitConfigUpdated`, `RateLimitStateUpdated`, `RateLimitAddressExemptionUpdated` | | **Access Control** | `RoleGranted`, `RoleRevoked` | | **Messaging Channel** | Endpoint channel events from `clear` / `skip` / `burn` / `nilify` (via OFT wrappers) | All events are indexed where applicable, for off-chain monitoring and reporting. ## Threat Model | Threat | Impact | Mitigation | | ---------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Admin key compromise** | Attacker grants themselves all roles, drains funds | Use governance multisig for `DEFAULT_ADMIN_ROLE`. Monitor `RoleGranted` events. Consider renouncing admin after initial setup. | | **Pauser key compromise** | Attacker pauses all operations (DoS) | Separate `PAUSER_ROLE` and `UNPAUSER_ROLE`. Assign unpauser to a different multisig. Pausing is disruptive but not catastrophic — funds remain safe. | | **Unpauser key compromise** | Attacker reverses a legitimate security pause | Assign `UNPAUSER_ROLE` to a governance multisig, separate from the pauser key. Monitor `PauseSet` / `DefaultPauseSet` events for unexpected unpauses. | | **Fee admin compromise** | Attacker sets fees to 100% | Fees are pushed to the fee deposit address during `_debit`; the fee admin cannot redirect proceeds. Ensure integrations use `minAmountLD` to reject unexpectedly small received amounts. Monitor `DefaultFeeBpsSet` / `FeeBpsSet`. High fees are visible on-chain and reversible. | | **Minter key compromise** | Attacker mints unlimited tokens, inflating supply | Only grant `MINTER_ROLE` to the OFT contract, never to EOAs. The OFT can only mint via `_credit()` after receiving a verified LayerZero message. Outbound rate limits cap the contagion risk to other chains. | | **Sanctioned address transfer** | Compliance violation | Enable Blacklist mode. Monitor OFAC/sanctions lists. Automate blacklist updates via `BLACKLISTER_ROLE`. For inbound credits to restricted addresses, use credit redirect to escrow, or allow the mint to revert and `nilify` / `skip` the message. | | **Blocked inbound nonce** | Receives on that path pause until the nonce is handled | Assign messaging channel roles to ops wallets so they can `skip` / `nilify` / `clear` / `burn` without the admin delegate. | | **Chain compromise** | Malicious messages from a compromised chain | Per-destination pause to isolate the chain. Inbound rate limits cap damage. DVN verification provides message integrity. | | **Proxy storage collision** | Upgrade corrupts storage | EIP-7201 namespaced storage with deterministic slots. Each module has an isolated storage location. | | **Non-atomic proxy deployment** | Attacker front-runs `initialize()` between proxy deploy and initialization | Deploy proxy and call `initialize()` atomically in the same transaction (e.g., via a deployer contract or `TransparentUpgradeableProxy` constructor data). | | **Dust exploitation** | Attacker sends dust amounts to avoid fees | Fee calculation uses `(_amount * bps) / BPS_DENOMINATOR`. Amounts where `amount * bps < BPS_DENOMINATOR` produce zero fee. This is by design — dust amounts are meant to be economically insignificant. | | **Rate limit bypass via many small txs** | Attacker splits large transfer into many small ones | Each transaction updates the bucket. Aggregate usage is tracked regardless of individual transaction size. With net accounting enabled, rounding can slightly favour the user, but the amounts are insignificant relative to the cost of running each `send()` transaction. | ## Monitoring Deploy off-chain monitoring for: | Event | Source | Indicates | | -------------------------------------------------- | ---------------- | ------------------------------------------------- | | `RoleGranted` / `RoleRevoked` | OFT, `ERC20Plus` | Permission changes | | `DefaultAdminTransferScheduled` | OFT, `ERC20Plus` | Admin transfer initiated | | `PauseSet` / `DefaultPauseSet` | OFT | Pause state changes | | `DefaultFeeBpsSet` / `FeeBpsSet` | OFT | Fee rate changes | | `RateLimitConfigUpdated` / `RateLimitStateUpdated` | OFT | Rate limit config changes | | `RateLimitAddressExemptionUpdated` | OFT | Rate limit exemption changes | | `AllowlistModeUpdated` | `ERC20Plus` | Allowlist mode transitions | | `BlacklistUpdated` / `WhitelistUpdated` | `ERC20Plus` | Address list changes | | `CreditRedirectConfigSet` / `CreditRedirected` | OFT | Escrow redirect config or inbound credit redirect | | `OFTSent` / `OFTReceived` | OFT | Cross-chain transfers (alert on large amounts) | | `Transfer` (to fee deposit) | `ERC20Plus` | Fee deposit inflows | ## Next Steps * [RBAC Reference](/v2/developers/evm/stablecoin-oft/rbac-reference) for the complete role-to-function matrix * [Architecture](/v2/developers/evm/stablecoin-oft/architecture) for the system design overview # Integrating Stargate Transfers Source: https://docs.layerzero.network/v2/developers/evm/stargate/overview Overview of Integrating Stargate Transfers on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure... Stargate protocol contracts (StargatePool and StargateOFT) implement the standard **IOFT interface** for [Omnichain Fungible Tokens (OFTs)](../oft/quickstart), making crosschain transfers straightforward with just two methods: `quoteSend()` and `send()`. For architecture and concepts, see [Stargate Finance](/v2/concepts/applications/stargate-finance). ### Stargate Asset Deployments View all available Stargate pools and OFT deployments across chains on the [**OFT Ecosystem & Stargate Assets**](/v2/deployments/oft-ecosystem-stargate-assets) page. Find contract addresses, supported chains, and asset types for seamless integration. ## The IOFT Interface All Stargate pool and HydraOFT contracts implement the same interface: ```solidity wrap theme={null} interface IStargate is IOFT { // Get underlying token address function token() external view returns (address); // Check if approval is required before sending function approvalRequired() external view returns (bool); // Quote the crosschain transfer fee function quoteSend( SendParam calldata _sendParam, bool _payInLzToken ) external view returns (MessagingFee memory); // Send tokens crosschain function send( SendParam calldata _sendParam, MessagingFee calldata _fee, address _refundAddress ) external payable returns (MessagingReceipt memory, OFTReceipt memory); } ``` **Helper Methods**: * `token()`: Returns the underlying ERC20 token address (or `address(0)` for native assets like ETH) * `approvalRequired()`: Returns `true` if you need to approve tokens before sending, `false` for mint/burn or native assets These methods allow you to write generic code that works with any Stargate asset (USDC pools, ETH pools, Hydra OFTs) without hardcoding token addresses or approval logic. ### Interactive Interface Methods #### quoteSend() - Get Transfer Fees #### quoteOFT() - Get Detailed Transfer Quote #### send() - Transfer Tokens ### SendParam Structure The key parameters for Stargate transfers is identical to OFT transfers: ```solidity wrap theme={null} struct SendParam { uint32 dstEid; // Destination endpoint ID bytes32 to; // Recipient address uint256 amountLD; // Amount to send (local decimals) uint256 minAmountLD; // Minimum amount (slippage protection) bytes extraOptions; // Execution options for LayerZero bytes composeMsg; // For composability (Taxi mode only) bytes oftCmd; // "" for Taxi, bytes(1) for Bus } ``` ## How send() Works When you call `send()` on a Stargate contract, it triggers a chain of calls through LayerZero's infrastructure: **Call Flow**: 1. **Stargate Contract** → Debits tokens (lock or burn depending on contract type) 2. **LayerZero Endpoint** → Routes the message to the configured MessageLib 3. **Message Library (SendUln302)** → Requests quotes from DVNs and Executor 4. **Workers (DVNs + Executor)** → Provide fee quotes for verification and execution 5. **Fee Aggregation** → Returns total `nativeFee` from all workers This multi-step process is why `quoteSend()` exists - it aggregates fees from all components in the security stack. ### Quote Freshness Call `quoteSend()` as close as possible to `send()` execution. Fee quotes can become stale due to: * Changing gas prices on source/destination chains * Price feed updates for crosschain gas estimation * DVN fee adjustments In production, quote and send in the same transaction or block to ensure accurate fees. ## Transfer Modes Stargate supports two transfer modes with different characteristics: ### Taxi Mode (Immediate) **Use when**: You need immediate transfer or composability **Set**: `oftCmd: ""` **Supports**: Composability via `composeMsg` to trigger actions on destination ```solidity wrap theme={null} SendParam memory sendParam = SendParam({ dstEid: dstEid, to: bytes32(uint256(uint160(recipient))), amountLD: amount, minAmountLD: amount * 995 / 1000, // 0.5% slippage extraOptions: "", // Or compose options composeMsg: "", // Or encoded compose message oftCmd: "" // Empty for Taxi mode }); ``` ### Bus Mode (Batched) **Use when**: You want gas savings and don't need composability **Set**: `oftCmd: new bytes(1)` **Does NOT support**: Composability - no `lzCompose()` will be triggered ```solidity wrap theme={null} SendParam memory sendParam = SendParam({ dstEid: dstEid, to: bytes32(uint256(uint160(recipient))), amountLD: amount, minAmountLD: amount * 995 / 1000, extraOptions: new bytes(0), composeMsg: new bytes(0), oftCmd: new bytes(1) // bytes(1) for Bus mode }); ``` ### Composability Requirement Composable strategies (e.g., [Omnichain Vaults](../ovault/overview)) **require Taxi mode**. Bus mode will not trigger `lzCompose()` calls. ## Basic Transfer Example **Note**: These examples assume you're using existing Stargate contracts (pre-configured by the Stargate team). If deploying your own OFT, you must complete the configuration steps first - see [OFT Quickstart - Deployment and Wiring](/v2/developers/evm/oft/quickstart#deployment-and-wiring). Since Stargate implements IOFT, sending tokens works exactly like any OFT - the only Stargate-specific aspect is the `oftCmd` field for Taxi vs Bus mode: ```solidity wrap theme={null} // Taxi Mode (immediate, supports composability) SendParam memory sendParam = SendParam({ dstEid: dstEid, to: bytes32(uint256(uint160(recipient))), amountLD: amount, minAmountLD: amount * 995 / 1000, extraOptions: "", composeMsg: "", oftCmd: "" // Empty for Taxi mode }); // Bus Mode (batched, no composability) SendParam memory sendParam = SendParam({ dstEid: dstEid, to: bytes32(uint256(uint160(recipient))), amountLD: amount, minAmountLD: amount * 995 / 1000, extraOptions: new bytes(0), composeMsg: new bytes(0), oftCmd: new bytes(1) // bytes(1) for Bus mode }); // Then call: IOFT(stargateAddress).send(sendParam, fee, refundAddress) ``` **For complete send implementation**: See [OFT Quickstart - Send Tokens](/v2/developers/evm/oft/quickstart#send-tokens) for detailed examples using CLI, Foundry scripts, or Hardhat tasks. ## Composability Taxi mode supports composability - triggering additional actions on the destination chain after Stargate assets arrive. **Key Points**: * Set `composeMsg` to your encoded data * Set `to` address to your composer contract * Add `addExecutorLzComposeOption()` for composer gas * Your composer receives via `lzCompose()` and decodes with `OFTComposeMsgCodec` ```solidity wrap theme={null} // Quick example: Send USDC with compose bytes memory composeMsg = abi.encode(finalRecipient, action, params); bytes memory extraOptions = OptionsBuilder.newOptions() .addExecutorLzComposeOption(0, 200_000, 0); SendParam memory sendParam = SendParam({ to: bytes32(uint256(uint160(composerAddress))), // Your composer composeMsg: composeMsg, // Your data extraOptions: extraOptions, // Compose gas oftCmd: "" // Must use Taxi mode // ... other fields }); ``` **For complete composability implementation**: * [OFT Quickstart - Composer](/v2/developers/evm/oft/quickstart#send-tokens--call-composer) - Full examples and code * [Composer Overview](/v2/developers/evm/composer/overview) - Deep dive on horizontal composability * [Composer Pattern](/v2/concepts/applications/composer-standard) - Architecture and concepts ## Finding Stargate Contracts ### Method 1: Deployed Contracts Page 1. Visit [Deployed Contracts](/v2/deployments/deployed-contracts) 2. Search for your chain or asset (e.g., "USDC", "Ethereum") 3. Stargate contracts appear at the top of each chain's list 4. Copy the address you need ### Method 2: Stargate API ```typescript wrap theme={null} // Mainnet const response = await fetch('https://mainnet.stargate-api.com/v1/metadata?version=v2'); const data = await response.json(); // Find USDC on Ethereum const asset = data.data.v2.find((a) => a.chainKey === 'ethereum' && a.token.symbol === 'USDC'); console.log('StargatePool USDC:', asset.address); ``` ## Next Steps **Learn More**: * [Stargate Finance Concepts](/v2/concepts/applications/stargate-finance) - Architecture and how it works * [OFT Standard](/v2/concepts/applications/oft-standard) - Understanding IOFT interface * [Stargate Protocol Docs](https://docs.stargate.finance) - Full protocol reference **Build**: * [OVault Overview](/v2/developers/evm/ovault/overview) - Build omnichain vaults with Stargate assets * [Composer Overview](/v2/developers/evm/composer/overview) - Advanced composability patterns **Get Help**: * [LayerZero Discord](https://discord.com/invite/ktbvm8Nkcr) - Technical support * [Stargate Discord](https://discord.com/invite/eG5TgNpUE7) - Stargate-specific questions # Solidity API Source: https://docs.layerzero.network/v2/developers/evm/technical-reference/api Technical reference for Solidity API. Complete API documentation with functions, parameters, and usage examples. LayerZero enables secure crosschain messaging. ## EndpointV2 ### lzToken ```solidity wrap theme={null} address lzToken ``` This stores the address of the LayerZero token, which may be used for paying messaging fees. It enables applications to settle crosschain communication costs using LayerZero's native token, where applicable. ### delegates ```solidity wrap theme={null} mapping(address => address) delegates ``` A mapping that allows address-based delegation. Applications (OApps) can delegate certain privileges to another address, authorizing the delegate to perform tasks on behalf of the original sender. ### constructor ```solidity wrap theme={null} constructor(uint32 _eid, address _owner) public ``` The constructor initializes the LayerZero endpoint on a specific chain. It assigns a unique Endpoint ID (`_eid`) to this instance, ensuring each chain has a distinct identifier for crosschain messaging. #### Parameters | Name | Type | Description | | ------- | ------- | ------------------------------------------------------------------------------------- | | \_eid | uint32 | the unique Endpoint Id for this deploy that all other Endpoints can use to send to it | | \_owner | address | | ### quote ```solidity wrap theme={null} function quote(struct MessagingParams _params, address _sender) external view returns (struct MessagingFee) ``` This function returns a fee estimate for sending a crosschain message, based on the parameters specified in `_params`. The fee quote takes into account the current messaging cost, which might vary over time. Note that the actual messaging cost could differ if the fees change between the quote and the message send operation. *MESSAGING STEP 0* #### Parameters | Name | Type | Description | | -------- | ---------------------- | ------------------------- | | \_params | struct MessagingParams | the messaging parameters | | \_sender | address | the sender of the message | ### send ```solidity wrap theme={null} function send(struct MessagingParams _params, address _refundAddress) external payable returns (struct MessagingReceipt) ``` This function sends a message to a destination chain through the LayerZero network. It also processes the associated fees, which can be either in native tokens or LayerZero tokens (`lzToken`). If excess fees are supplied, the surplus is refunded to the provided `_refundAddress`. *MESSAGING STEP 1 - OApp need to transfer the fees to the endpoint before sending the message* #### Parameters | Name | Type | Description | | --------------- | ---------------------- | ------------------------------------------------- | | \_params | struct MessagingParams | the messaging parameters | | \_refundAddress | address | the address to refund both the native and lzToken | ### \_send ```solidity wrap theme={null} function _send(address _sender, struct MessagingParams _params) internal returns (struct MessagingReceipt, address) ``` An internal version of the send function that handles the underlying mechanics of sending a message. This function is called by external message-sending methods and ensures the message is routed to the appropriate destination with the correct fee management. *internal function for sending the messages used by all external send methods* #### Parameters | Name | Type | Description | | -------- | ---------------------- | --------------------------------------------------------------------------- | | \_sender | address | the address of the application sending the message to the destination chain | | \_params | struct MessagingParams | the messaging parameters | ### verify ```solidity wrap theme={null} function verify(struct Origin _origin, address _receiver, bytes32 _payloadHash) external ``` On the destination chain, the message needs to be verified before being processed. This function checks the validity of the incoming message by comparing its origin and payload hash with the expected values. *MESSAGING STEP 2 - on the destination chain configured receive library verifies a message* #### Parameters | Name | Type | Description | | ------------- | ------------- | ------------------------------------------------------------- | | \_origin | struct Origin | a struct holding the srcEid, nonce, and sender of the message | | \_receiver | address | the receiver of the message | | \_payloadHash | bytes32 | the payload hash of the message | ### lzReceive ```solidity wrap theme={null} function lzReceive(struct Origin _origin, address _receiver, bytes32 _guid, bytes _message, bytes _extraData) external payable ``` This is the final step in the message execution process. After the message has been verified, it is delivered to the intended recipient address. The function can pass additional `extraData` if needed for execution. *MESSAGING STEP 3 - the last step execute a verified message to the designated receiver the execution provides the execution context (caller, extraData) to the receiver. the receiver can optionally assert the caller and validate the untrusted extraData cant reentrant because the payload is cleared before execution* #### Parameters | Name | Type | Description | | ----------- | ------------- | ---------------------------------------------------------------------------------------- | | \_origin | struct Origin | the origin of the message | | \_receiver | address | the receiver of the message | | \_guid | bytes32 | the guid of the message | | \_message | bytes | the message | | \_extraData | bytes | the extra data provided by the executor. this data is untrusted and should be validated. | ### lzReceiveAlert ```solidity wrap theme={null} function lzReceiveAlert(struct Origin _origin, address _receiver, bytes32 _guid, uint256 _gas, uint256 _value, bytes _message, bytes _extraData, bytes _reason) external ``` This function handles a failure in message delivery and provides an alert to the application. It logs the reason for the failure and the state of the message, allowing developers to debug message processing errors. #### Parameters | Name | Type | Description | | ----------- | ------------- | ---------------------------------------- | | \_origin | struct Origin | the origin of the message | | \_receiver | address | the receiver of the message | | \_guid | bytes32 | the guid of the message | | \_gas | uint256 | | | \_value | uint256 | | | \_message | bytes | the message | | \_extraData | bytes | the extra data provided by the executor. | | \_reason | bytes | the reason for failure | ### clear ```solidity wrap theme={null} function clear(address _oapp, struct Origin _origin, bytes32 _guid, bytes _message) external ``` This function allows an OApp (Omnichain Application) to clear a pending message manually. Instead of pushing the message through the standard delivery flow, the message is cleared from the queue, effectively marking it as processed or ignored. `_Oapp` uses this interface to clear a message. this is a PULL mode versus the PUSH mode of `lzReceive` the cleared message can be ignored by the app (effectively burnt) authenticated by oapp\_ #### Parameters | Name | Type | Description | | --------- | ------------- | ------------------------- | | \_oapp | address | | | \_origin | struct Origin | the origin of the message | | \_guid | bytes32 | the guid of the message | | \_message | bytes | the message | ### setLzToken ```solidity wrap theme={null} function setLzToken(address _lzToken) public virtual ``` This function allows the owner to set or change the LayerZero token (`lzToken`). This token may be used to pay for messaging fees. The function is designed to provide flexibility in case the initial configuration of the token was incorrect or needs to be updated. It should only be called by the contract owner. Users should avoid approving non-LayerZero tokens to be spent by the `EndpointV2` contract, as this function can override the token used for fees. *allows reconfiguration to recover from wrong configurations users should never approve the EndpointV2 contract to spend their non-layerzero tokens override this function if the endpoint is charging ERC20 tokens as native only owner* #### Parameters | Name | Type | Description | | --------- | ------- | -------------------------------- | | \_lzToken | address | the new layer zero token address | ### recoverToken ```solidity wrap theme={null} function recoverToken(address _token, address _to, uint256 _amount) external ``` This function allows the owner to recover tokens that were mistakenly sent to the `EndpointV2` contract. It supports both native tokens (if `_token` is set to `0x0`) and `ERC20` tokens. This ensures that tokens accidentally locked in the contract can be safely retrieved by the owner. *recover the token sent to this contract by mistake only owner* #### Parameters | Name | Type | Description | | -------- | ------- | ---------------------------------------------------- | | \_token | address | the token to recover. if 0x0 then it is native token | | \_to | address | the address to send the token to | | \_amount | uint256 | the amount to send | ### \_payToken ```solidity wrap theme={null} function _payToken(address _token, uint256 _required, uint256 _supplied, address _receiver, address _refundAddress) internal ``` This internal function handles payments in ERC20 tokens. It ensures that the sender has approved the endpoint to spend the specified tokens and processes the payment. If the supplied token amount exceeds the required amount, the excess is refunded to the specified `_refundAddress`. *handling token payments on endpoint. the sender must approve the endpoint to spend the token internal function* #### Parameters | Name | Type | Description | | --------------- | ------- | ------------------------- | | \_token | address | the token to pay | | \_required | uint256 | the amount required | | \_supplied | uint256 | the amount supplied | | \_receiver | address | the receiver of the token | | \_refundAddress | address | | ### \_payNative ```solidity wrap theme={null} function _payNative(uint256 _required, uint256 _supplied, address _receiver, address _refundAddress) internal virtual ``` This internal function manages payments in native tokens (such as ETH). It processes the payment and refunds any excess amount to the `_refundAddress`. If the endpoint charges ERC20 tokens as native, this function can be overridden. *handling native token payments on endpoint override this if the endpoint is charging ERC20 tokens as native internal function* #### Parameters | Name | Type | Description | | --------------- | ------- | ----------------------------------- | | \_required | uint256 | the amount required | | \_supplied | uint256 | the amount supplied | | \_receiver | address | the receiver of the native token | | \_refundAddress | address | the address to refund the excess to | ### \_suppliedLzToken ```solidity wrap theme={null} function _suppliedLzToken(bool _payInLzToken) internal view returns (uint256 supplied) ``` This internal view function returns the amount of LayerZero tokens (`lzToken`) supplied for payment, but only if `_payInLzToken` is set to true. It checks the balance of the `lzToken` used to pay for the messaging fee. *get the balance of the lzToken as the supplied lzToken fee if payInLzToken is true* ### \_suppliedNative ```solidity wrap theme={null} function _suppliedNative() internal view virtual returns (uint256) ``` This internal function returns the amount of native tokens supplied for the payment. If the endpoint charges ERC20 tokens as native tokens, this function can be overridden to handle such cases. *override this if the endpoint is charging ERC20 tokens as native* ### \_assertMessagingFee ```solidity wrap theme={null} function _assertMessagingFee(struct MessagingFee _required, uint256 _suppliedNativeFee, uint256 _suppliedLzTokenFee) internal pure ``` This internal function verifies that the supplied fees (both native and `lzToken`) are sufficient to cover the required messaging fees. If the supplied fees are insufficient, the function will assert an error. *Assert the required fees and the supplied fees are enough* ### nativeToken ```solidity wrap theme={null} function nativeToken() external view virtual returns (address) ``` This external view function returns the address of the native ERC20 token used by the endpoint if it charges ERC20 tokens as native tokens. If the contract uses actual native tokens (like ETH), it returns `0x0`. *override this if the endpoint is charging ERC20 tokens as native* #### Return Values | Name | Type | Description | | ---- | ------- | -------------------------------------------------------------------- | | \[0] | address | 0x0 if using native. otherwise the address of the native ERC20 token | ### setDelegate ```solidity wrap theme={null} function setDelegate(address _delegate) external ``` This function allows an OApp to authorize a delegate to act on its behalf. The delegate can configure settings or perform other operations related to the LayerZero endpoint, effectively giving another address certain administrative permissions over the OApp's endpoint interaction. delegate is authorized by the oapp to configure anything in layerzero ### \_initializable ```solidity wrap theme={null} function _initializable(struct Origin _origin, address _receiver, uint64 _lazyInboundNonce) internal view returns (bool) ``` This internal view function checks whether a message from a specific origin can be initialized for delivery to the receiver. The function verifies that the message can be safely processed based on the `lazyInboundNonce`, which controls the message order and flow. ### \_verifiable ```solidity wrap theme={null} function _verifiable(struct Origin _origin, address _receiver, uint64 _lazyInboundNonce) internal view returns (bool) ``` This internal function checks whether a message from the given origin is `verifiable` for the receiver. It ensures that the message payload is valid and ready for execution based on the provided nonce and other checks. A payload with a hash of `bytes(0)` can never be submitted. *bytes(0) payloadHash can never be submitted* ### \_assertAuthorized ```solidity wrap theme={null} function _assertAuthorized(address _oapp) internal view ``` This internal function ensures that the caller is either the OApp or its authorized delegate. It acts as an access control check to verify that only trusted entities can configure or interact with the OApp's LayerZero-related settings. *assert the caller to either be the oapp or the delegate* ### initializable ```solidity wrap theme={null} function initializable(struct Origin _origin, address _receiver) external view returns (bool) ``` This external view function checks whether a message from the given origin is ready to be initialized and processed for the specified receiver. It returns true if the message can be initialized. ### verifiable ```solidity wrap theme={null} function verifiable(struct Origin _origin, address _receiver) external view returns (bool) ``` This external view function checks whether a message from the given origin is verifiable for the receiver. It confirms that the message payload has been received and validated. ## EndpointV2Alt `EndpointV2Alt` is the LayerZero V2 endpoint contract designed for blockchain networks where `ERC20` tokens are used as native tokens (instead of standard native tokens like ETH or BNB). This contract supports altFeeTokens, which are ERC20 tokens that can be used for paying messaging fees. The architecture is optimized to reduce gas costs by making certain configurations immutable. ### LZ\_OnlyAltToken ```solidity wrap theme={null} error LZ_OnlyAltToken() ``` This error is thrown when a non-ERC20 token is used in a context where only the `altFeeToken` (an ERC20 token) is allowed. It enforces that only the designated ERC20 token is used for certain operations in the contract. ### nativeErc20 ```solidity wrap theme={null} address nativeErc20 ``` This holds the address of the ERC20 token used as the native currency in this contract. The `nativeErc20` token is immutable, meaning that once it's set, it cannot be changed. This saves gas by preventing unnecessary updates and checks. This token is used for paying fees when the chain doesn't have a standard native token. *the altFeeToken is used for fees when the native token has no value it is immutable for gas saving. only 1 endpoint for such chains* ### constructor ```solidity wrap theme={null} constructor(uint32 _eid, address _owner, address _altToken) public ``` The constructor initializes the `EndpointV2Alt` contract, associating it with a unique Endpoint ID (`_eid`). It also specifies the owner of the contract and the `altFeeToken` (an ERC20 token) used for fees on the chain. ### \_payNative ```solidity wrap theme={null} function _payNative(uint256 _required, uint256 _supplied, address _receiver, address _refundAddress) internal ``` This internal function handles native payments in the context of this contract. Since the contract operates on chains using ERC20 tokens as native tokens, `_payNative` processes payments in those tokens. If the supplied amount exceeds the required amount, the excess is refunded to the `_refundAddress`. *handling native token payments on endpoint internal function* #### Parameters | Name | Type | Description | | --------------- | ------- | ----------------------------------- | | \_required | uint256 | the amount required | | \_supplied | uint256 | the amount supplied | | \_receiver | address | the receiver of the native token | | \_refundAddress | address | the address to refund the excess to | ### \_suppliedNative ```solidity wrap theme={null} function _suppliedNative() internal view returns (uint256) ``` This internal view function returns the amount of native tokens (ERC20 tokens, in this case) supplied for the payment. It is used to track the exact amount of tokens provided for a transaction, ensuring that the necessary fees are met. *return the balance of the native token* ### setLzToken ```solidity wrap theme={null} function setLzToken(address _lzToken) public ``` This function allows the contract owner to set or change the LayerZero token (`lzToken`). The function checks if the new token address matches the current one before applying changes. The lzToken can be used for paying fees when applicable. *check if lzToken is set to the same address* ### nativeToken ```solidity wrap theme={null} function nativeToken() external view returns (address) ``` This external view function returns the address of the native ERC20 token used by the contract. If the contract uses actual native tokens, it returns `0x0`. Otherwise, it returns the address of the ERC20 token acting as the native currency on the chain. *override this if the endpoint is charging ERC20 tokens as native* #### Return Values | Name | Type | Description | | ---- | ------- | -------------------------------------------------------------------- | | \[0] | address | 0x0 if using native. otherwise the address of the native ERC20 token | ## EndpointV2View `EndpointV2View` is a contract used for viewing the state of LayerZero V2 messages, particularly related to whether a message is verifiable, executable, or initializable. This contract is typically used by other contracts or off-chain services that need to check the status of crosschain messages. ### initialize ```solidity wrap theme={null} function initialize(address _endpoint) external ``` The initialize function sets the reference to the LayerZero endpoint (`_endpoint`). This endpoint is used for all subsequent verifications and message status checks. This function must be called before the contract can be used. ## ExecutionState ```solidity wrap theme={null} enum ExecutionState { NotExecutable, VerifiedButNotExecutable, Executable, Executed } ``` This enum defines the possible execution states for a message within the LayerZero system: `NotExecutable`: The message is not yet ready for execution. `VerifiedButNotExecutable`: The message has been verified, but something is preventing its execution (e.g., not enough gas). `Executable`: The message is ready to be executed. `Executed`: The message has been successfully executed. ## EndpointV2ViewUpgradeable `EndpointV2ViewUpgradeable` is an upgradeable version of the `EndpointV2View`, adding support for certain upgrades while maintaining compatibility with existing state. ### EMPTY\_PAYLOAD\_HASH ```solidity wrap theme={null} bytes32 EMPTY_PAYLOAD_HASH ``` This constant represents an empty payload hash, which can be used to signal that no payload is associated with a message. ### NIL\_PAYLOAD\_HASH ```solidity wrap theme={null} bytes32 NIL_PAYLOAD_HASH ``` This constant represents a "nil" payload hash, often used to indicate that a payload has been intentionally left out or invalidated. ### endpoint ```solidity wrap theme={null} contract ILayerZeroEndpointV2 endpoint ``` This contract reference stores the LayerZero endpoint that the `EndpointV2ViewUpgradeable` is interacting with. It is used for all message-related queries and verifications. ### \_\_EndpointV2View\_init ```solidity wrap theme={null} function __EndpointV2View_init(address _endpoint) internal ``` This internal initialization function sets the reference to the LayerZero endpoint. This function must be called during the deployment process to initialize the contract. ### \_\_EndpointV2View\_init\_unchained ```solidity wrap theme={null} function __EndpointV2View_init_unchained(address _endpoint) internal ``` This is a version of the initialization function that is not chained. It can be used in scenarios where the contract needs to be initialized without triggering additional logic. ### initializable ```solidity wrap theme={null} function initializable(struct Origin _origin, address _receiver) public view returns (bool) ``` This function checks if a message from a specific origin can be initialized for the provided receiver. It returns true if the message is ready to be processed (initialized). ### verifiable ```solidity wrap theme={null} function verifiable(struct Origin _origin, address _receiver, address _receiveLib, bytes32 _payloadHash) public view returns (bool) ``` This function checks if a message from the given origin is verifiable for the provided receiver. It ensures that the payload hash matches and the message has been validated by the correct messaging library. *check if a message is verifiable.* ### executable ```solidity wrap theme={null} function executable(struct Origin _origin, address _receiver) public view returns (enum ExecutionState) ``` This function checks the execution state of a message for a given origin and receiver. It returns the current execution state, whether the message is `NotExecutable`, `VerifiedButNotExecutable`, `Executable`, or `Executed`. *check if a message is executable.* #### Return Values | Name | Type | Description | | ---- | ------------------- | -------------------------------------------------------- | | \[0] | enum ExecutionState | ExecutionState of Executed, Executable, or NotExecutable | ## MessageLibManager `MessageLibManager` manages the messaging libraries (`msgLib`) that are used for sending and receiving messages in the LayerZero protocol. It controls the libraries that each application (`OApp`) can use, either by directly assigning libraries or defaulting to LayerZero's settings. ### blockedLibrary ```solidity wrap theme={null} address blockedLibrary ``` The `blockedLibrary` is a specific library that is no longer allowed for sending or receiving messages. ### registeredLibraries ```solidity wrap theme={null} address[] registeredLibraries ``` An array storing the addresses of all libraries that are registered and can be used for message sending or receiving. ### isRegisteredLibrary ```solidity wrap theme={null} mapping(address => bool) isRegisteredLibrary ``` This mapping tracks whether a given library is registered, providing a quick way to verify if a library is eligible for use. ### sendLibrary ```solidity wrap theme={null} mapping(address => mapping(uint32 => address)) sendLibrary ``` A mapping that stores the send libraries for each OApp (`address`) and endpoint ID (`uint32`). Each OApp can specify a library it wants to use for sending messages to a specific endpoint. ### receiveLibrary ```solidity wrap theme={null} mapping(address => mapping(uint32 => address)) receiveLibrary ``` This mapping stores the receive libraries for each OApp (`address`) and endpoint ID (`uint32`). Each OApp can specify a library for handling received messages. ### receiveLibraryTimeout ```solidity wrap theme={null} mapping(address => mapping(uint32 => struct IMessageLibManager.Timeout)) receiveLibraryTimeout ``` This mapping tracks the timeout period for a receive library. After a timeout period, the receive library may need to be updated or replaced. This helps manage library versioning and ensure that OApps can handle breaking changes in a safe manner. ### defaultSendLibrary ```solidity wrap theme={null} mapping(uint32 => address) defaultSendLibrary ``` This mapping holds the default send library for each endpoint (`uint32`). If an OApp does not specify a send library, the default send library for that endpoint is used. ### defaultReceiveLibrary ```solidity wrap theme={null} mapping(uint32 => address) defaultReceiveLibrary ``` The default receive library for each endpoint is stored here. If an OApp does not specify a receive library, the system defaults to the configured library for that endpoint. ### defaultReceiveLibraryTimeout ```solidity wrap theme={null} mapping(uint32 => struct IMessageLibManager.Timeout) defaultReceiveLibraryTimeout ``` This mapping tracks the timeout period for default receive libraries. After this period, the default library may need to be updated or retired. ### constructor ```solidity wrap theme={null} constructor() internal ``` The constructor is internal and initializes the MessageLibManager. It ensures that all the necessary mappings and configurations are properly set up when the contract is deployed. ### onlyRegistered ```solidity wrap theme={null} modifier onlyRegistered(address _lib) ``` This modifier ensures that only libraries registered with `MessageLibManager` can call certain functions. It restricts access to unregistered libraries, safeguarding the system from misuse. ### isSendLib ```solidity wrap theme={null} modifier isSendLib(address _lib) ``` This modifier ensures that only valid send libraries can call specific functions. It checks if the library is properly configured for sending messages. ### isReceiveLib ```solidity wrap theme={null} modifier isReceiveLib(address _lib) ``` This modifier ensures that only valid receive libraries can call certain functions. It verifies the library's eligibility for processing received messages. ### onlyRegisteredOrDefault ```solidity wrap theme={null} modifier onlyRegisteredOrDefault(address _lib) ``` This modifier allows both registered libraries and default libraries to access certain functions, ensuring that the system works even when custom libraries are not defined. ### onlySupportedEid ```solidity wrap theme={null} modifier onlySupportedEid(address _lib, uint32 _eid) ``` This modifier ensures that a library supports a specific endpoint ID (`_eid`). It checks if the library has been configured to handle messages for that endpoint. *check if the library supported the eid.* ### getRegisteredLibraries ```solidity wrap theme={null} function getRegisteredLibraries() external view returns (address[]) ``` This function returns a list of all registered libraries. It allows users and applications to query which libraries are available for use. ### getSendLibrary ```solidity wrap theme={null} function getSendLibrary(address _sender, uint32 _dstEid) public view returns (address lib) ``` This function retrieves the send library for a specific OApp (`_sender`) and destination endpoint (`_dstEid`). If the OApp has not specified a library, the default one is used. *If the Oapp does not have a selected Send Library, this function will resolve to the default library configured by LayerZero* #### Parameters | Name | Type | Description | | -------- | ------- | --------------------------------------------------- | | \_sender | address | The address of the Oapp that is sending the message | | \_dstEid | uint32 | The destination endpoint id | #### Return Values | Name | Type | Description | | ---- | ------- | --------------------------- | | lib | address | address of the Send Library | ### isDefaultSendLibrary ```solidity wrap theme={null} function isDefaultSendLibrary(address _sender, uint32 _dstEid) public view returns (bool) ``` This function checks if the send library in use for a specific OApp and endpoint is the default one. ### getReceiveLibrary ```solidity wrap theme={null} function getReceiveLibrary(address _receiver, uint32 _srcEid) public view returns (address lib, bool isDefault) ``` This function retrieves the receive library for a specific OApp (`_receiver`) and source endpoint (`_srcEid`). If the OApp has not specified a library, the default one is used. *the receiveLibrary can be lazily resolved that if not set it will point to the default configured by LayerZero* ### isValidReceiveLibrary ```solidity wrap theme={null} function isValidReceiveLibrary(address _receiver, uint32 _srcEid, address _actualReceiveLib) public view returns (bool) ``` This function checks if the specified receive library is valid for a given OApp, ensuring that the OApp can trust the message verification and processing done by the library. *called when the endpoint checks if the msgLib attempting to verify the msg is the configured msgLib of the Oapp this check provides the ability for Oapp to lock in a trusted msgLib it will fist check if the msgLib is the currently configured one. then check if the msgLib is the one in grace period of msgLib versioning upgrade* ### registerLibrary ```solidity wrap theme={null} function registerLibrary(address _lib) public ``` This function registers a new library with the `MessageLibManager`. Only the contract owner can register new libraries. *all libraries have to implement the erc165 interface to prevent wrong configurations only owner* ### setDefaultSendLibrary ```solidity wrap theme={null} function setDefaultSendLibrary(uint32 _eid, address _newLib) external ``` The contract owner sets the default send library for a specific endpoint. The new library must be registered and have support for the endpoint. *owner setting the defaultSendLibrary can set to the blockedLibrary, which is a registered library the msgLib must enable the support before they can be registered to the endpoint as the default only owner* ### setDefaultReceiveLibrary ```solidity wrap theme={null} function setDefaultReceiveLibrary(uint32 _eid, address _newLib, uint256 _gracePeriod) external ``` The contract owner sets the default receive library for a specific endpoint and may define a grace period during which the old library can still be used. *owner setting the defaultSendLibrary must be a registered library (including blockLibrary) with the eid support enabled in version migration, it can add a grace period to the old library. if the grace period is 0, it will delete the timeout configuration. only owner* ### setDefaultReceiveLibraryTimeout ```solidity wrap theme={null} function setDefaultReceiveLibraryTimeout(uint32 _eid, address _lib, uint256 _expiry) external ``` This function allows the contract owner to set a timeout for the default receive library for a given endpoint. After the timeout, the library may need to be updated or retired. *owner setting the defaultSendLibrary must be a registered library (including blockLibrary) with the eid support enabled can used to (1) extend the current configuration (2) force remove the current configuration (3) change to a new configuration* #### Parameters | Name | Type | Description | | -------- | ------- | --------------------------------- | | \_eid | uint32 | | | \_lib | address | | | \_expiry | uint256 | the block number when lib expires | ### isSupportedEid ```solidity wrap theme={null} function isSupportedEid(uint32 _eid) external view returns (bool) ``` This function checks if an endpoint is supported, returning true only if both the default send and receive libraries are set. *returns true only if both the default send/receive libraries are set* ### setSendLibrary ```solidity wrap theme={null} function setSendLibrary(address _oapp, uint32 _eid, address _newLib) external ``` This function allows an OApp to set a custom send library for a specific endpoint. The library must be registered and support the endpoint. *Oapp setting the sendLibrary must be a registered library (including blockLibrary) with the eid support enabled authenticated by the Oapp* ### setReceiveLibrary ```solidity wrap theme={null} function setReceiveLibrary(address _oapp, uint32 _eid, address _newLib, uint256 _gracePeriod) external ``` An OApp can use this function to set a custom receive library, with an optional grace period during which the old library can still be used. *Oapp setting the receiveLibrary must be a registered library (including blockLibrary) with the eid support enabled in version migration, it can add a grace period to the old library. if the grace period is 0, it will delete the timeout configuration. authenticated by the Oapp* #### Parameters | Name | Type | Description | | ------------- | ------- | -------------------------------------------------- | | \_oapp | address | | | \_eid | uint32 | | | \_newLib | address | | | \_gracePeriod | uint256 | the number of blocks from now until oldLib expires | ### setReceiveLibraryTimeout ```solidity wrap theme={null} function setReceiveLibraryTimeout(address _oapp, uint32 _eid, address _lib, uint256 _expiry) external ``` This function allows the OApp to set a timeout for its custom receive library. After the timeout, the OApp may need to update the library. *Oapp setting the defaultSendLibrary must be a registered library (including blockLibrary) with the eid support enabled can used to (1) extend the current configuration (2) force remove the current configuration (3) change to a new configuration* #### Parameters | Name | Type | Description | | -------- | ------- | --------------------------------- | | \_oapp | address | | | \_eid | uint32 | | | \_lib | address | | | \_expiry | uint256 | the block number when lib expires | ### setConfig ```solidity wrap theme={null} function setConfig(address _oapp, address _lib, struct SetConfigParam[] _params) external ``` This function allows the OApp to configure the messaging libraries with specific parameters. *authenticated by the \_oapp* ### getConfig ```solidity wrap theme={null} function getConfig(address _oapp, address _lib, uint32 _eid, uint32 _configType) external view returns (bytes config) ``` This function retrieves the current configuration of the OApp's messaging libraries for a given endpoint and config type. *a view function to query the current configuration of the OApp* ### \_assertAuthorized ```solidity wrap theme={null} function _assertAuthorized(address _oapp) internal virtual ``` ## MessagingChannel The `MessagingChannel` contract manages the lifecycle of messages sent across different blockchains using the LayerZero protocol. It tracks the nonces, payloads, and statuses of messages to ensure censorship resistance and reliable crosschain communication. ### EMPTY\_PAYLOAD\_HASH ```solidity wrap theme={null} bytes32 EMPTY_PAYLOAD_HASH ``` A constant representing an empty payload hash. This value is used when a message has no payload associated with it. ### NIL\_PAYLOAD\_HASH ```solidity wrap theme={null} bytes32 NIL_PAYLOAD_HASH ``` A constant representing a "nil" payload hash, used to indicate that a payload is invalidated or should be ignored. ### eid ```solidity wrap theme={null} uint32 eid ``` The unique Endpoint ID associated with this deployed messaging channel. It ensures that messages are routed correctly across different endpoints in LayerZero. ### lazyInboundNonce ```solidity wrap theme={null} mapping(address => mapping(uint32 => mapping(bytes32 => uint64))) lazyInboundNonce ``` A mapping that tracks the inbound nonces for messages received. The nonces are updated lazily, meaning the nonce is incremented only when messages are processed, ensuring message order is preserved. ### inboundPayloadHash ```solidity wrap theme={null} mapping(address => mapping(uint32 => mapping(bytes32 => mapping(uint64 => bytes32)))) inboundPayloadHash ``` This mapping stores the hash of the payload for inbound messages. Each payload is uniquely identified by its `sender`, source `endpoint`, and `nonce`. ### outboundNonce ```solidity wrap theme={null} mapping(address => mapping(uint32 => mapping(bytes32 => uint64))) outboundNonce ``` This mapping tracks the next outbound nonce for a given `sender`, destination `endpoint`, and `receiver`. Nonces ensure that messages are delivered in order and without duplication. ### constructor ```solidity wrap theme={null} constructor(uint32 _eid) internal ``` The internal constructor initializes the messaging channel with the unique Endpoint ID (`_eid`). This ID is used to identify the channel in LayerZero's messaging system. #### Parameters | Name | Type | Description | | ----- | ------ | ------------------------------------------------------------- | | \_eid | uint32 | is the universally unique id (UUID) of this deployed Endpoint | ### \_outbound ```solidity wrap theme={null} function _outbound(address _sender, uint32 _dstEid, bytes32 _receiver) internal returns (uint64 nonce) ``` This internal function increments and returns the next outbound nonce for the sender. It ensures that outbound messages are properly sequenced. *increase and return the next outbound nonce* ### \_inbound ```solidity wrap theme={null} function _inbound(address _receiver, uint32 _srcEid, bytes32 _sender, uint64 _nonce, bytes32 _payloadHash) internal ``` The `_inbound` function updates the inbound message state lazily. It doesn't immediately increment the nonce, allowing for out-of-order message verification while preserving censorship resistance. *inbound won't update the nonce eagerly to allow unordered verification instead, it will update the nonce lazily when the message is received messages can only be cleared in order to preserve censorship-resistance* ### inboundNonce ```solidity wrap theme={null} function inboundNonce(address _receiver, uint32 _srcEid, bytes32 _sender) public view returns (uint64) ``` This function returns the highest contiguous verified inbound nonce. It iterates over the nonces, starting from the lazy inbound nonce, to find the last verified message. *returns the max index of the longest gapless sequence of verified msg nonces. the uninitialized value is 0. the first nonce is always 1 it starts from the lazyInboundNonce (last checkpoint) and iteratively check if the next nonce has been verified this function can OOG if too many backlogs, but it can be trivially fixed by just clearing some prior messages NOTE: Oapp explicitly skipped nonces count as "verified" for these purposes eg. \[1,2,3,4,6,7] => 4, \[1,2,6,8,10] => 2, \[1,3,4,5,6] => 1* ### \_hasPayloadHash ```solidity wrap theme={null} function _hasPayloadHash(address _receiver, uint32 _srcEid, bytes32 _sender, uint64 _nonce) internal view returns (bool) ``` This function checks if a given payload hash exists for a specific nonce. It assumes that a payload hash of zero means the payload has not been initialized. *checks if the storage slot is not initialized. Assumes computationally infeasible that payload can hash to 0* ### skip ```solidity wrap theme={null} function skip(address _oapp, uint32 _srcEid, bytes32 _sender, uint64 _nonce) external ``` The skip function allows an OApp to skip a specific nonce, preventing the message from being verified or executed. This can be useful in race conditions or when a message is flagged as malicious. After skipping, the lazy inbound nonce is updated. *the caller must provide \_nonce to prevent skipping the unintended nonce it could happen in some race conditions, e.g. to skip nonce 3, but nonce 3 was consumed first usage: skipping the next nonce to prevent message verification, e.g. skip a message when Precrime throws alerts if the Oapp wants to skip a verified message, it should call the clear() function instead after skipping, the lazyInboundNonce is set to the provided nonce, which makes the inboundNonce also the provided nonce ie. allows the Oapp to increment the lazyInboundNonce without having had that corresponding msg be verified* ### nilify ```solidity wrap theme={null} function nilify(address _oapp, uint32 _srcEid, bytes32 _sender, uint64 _nonce, bytes32 _payloadHash) external ``` This function marks a verified packet as nil, preventing it from being executed. A nilified packet cannot be verified or executed again unless it is re-verified with the correct payload hash. *Marks a packet as verified, but disallows execution until it is re-verified. Reverts if the provided \_payloadHash does not match the currently verified payload hash. A non-verified nonce can be nilified by passing EMPTY\_PAYLOAD\_HASH for \_payloadHash. Assumes the computational intractability of finding a payload that hashes to bytes32.max. Authenticated by the caller* ### burn ```solidity wrap theme={null} function burn(address _oapp, uint32 _srcEid, bytes32 _sender, uint64 _nonce, bytes32 _payloadHash) external ``` The burn function permanently marks a packet as unexecutable and un-verifiable. This action is irreversible and can only be performed on packets that have not yet been executed. *Marks a nonce as unexecutable and un-verifiable. The nonce can never be re-verified or executed. Reverts if the provided \_payloadHash does not match the currently verified payload hash. Only packets with nonces less than or equal to the lazy inbound nonce can be burned. Reverts if the nonce has already been executed. Authenticated by the caller* ### \_clearPayload ```solidity wrap theme={null} function _clearPayload(address _receiver, uint32 _srcEid, bytes32 _sender, uint64 _nonce, bytes _payload) internal returns (bytes32 actualHash) ``` This function clears the stored payload for a message and updates the lazy inbound nonce. If there are many queued messages, the payload can be cleared in smaller batches to prevent out-of-gas errors. *calling this function will clear the stored message and increment the lazyInboundNonce to the provided nonce if a lot of messages are queued, the messages can be cleared with a smaller step size to prevent OOG NOTE: this function does not change inboundNonce, it only changes the lazyInboundNonce up to the provided nonce* ### nextGuid ```solidity wrap theme={null} function nextGuid(address _sender, uint32 _dstEid, bytes32 _receiver) external view returns (bytes32) ``` The nextGuid function returns the GUID for the next message in a specific path, providing a unique identifier for the message that can be included in the payload. *returns the GUID for the next message given the path the Oapp might want to include the GUID into the message in some cases* ### \_assertAuthorized ```solidity wrap theme={null} function _assertAuthorized(address _oapp) internal virtual ``` This internal function ensures that the caller of specific messaging operations is authorized, either by being the OApp or its delegate. ## MessagingComposer The `MessagingComposer` contract is responsible for composing LayerZero messages, enabling applications (OApps) to send messages in smaller piecewise operations or add extra steps to messages. ### composeQueue ```solidity wrap theme={null} mapping(address => mapping(address => mapping(bytes32 => mapping(uint16 => bytes32)))) composeQueue ``` The composeQueue stores composed message fragments for each OApp. It maps the OApp's address, the receiver's address, a message GUID, and an index (for multi-part messages) to the hash of the composed message fragment. This ensures that messages can be composed and sent in a fragmented manner. ### sendCompose ```solidity wrap theme={null} function sendCompose(address _to, bytes32 _guid, uint16 _index, bytes _message) external ``` The `sendCompose` function allows an OApp to send a composed message fragment to the receiver. The sender must be authenticated, ensuring that only the intended OApp can send the message. Multiple fragments can be sent with the same GUID, allowing for more flexible message composition. *the Oapp sends the lzCompose message to the endpoint the composer MUST assert the sender because anyone can send compose msg with this function with the same GUID, the Oapp can send compose to multiple \_composer at the same time authenticated by the msg.sender* #### Parameters | Name | Type | Description | | --------- | ------- | --------------------------------------------------- | | \_to | address | the address which will receive the composed message | | \_guid | bytes32 | the message guid | | \_index | uint16 | | | \_message | bytes | the message | ### lzCompose ```solidity wrap theme={null} function lzCompose(address _from, address _to, bytes32 _guid, uint16 _index, bytes _message, bytes _extraData) external payable ``` The `lzCompose` function executes a composed message from the sender to the receiver. It provides the execution context (caller and extraData) to the receiver, allowing for additional validation. *execute a composed messages from the sender to the composer (receiver) the execution provides the execution context (caller, extraData) to the receiver. the receiver can optionally assert the caller and validate the untrusted extraData can not re-entrant* #### Parameters | Name | Type | Description | | ----------- | ------- | ---------------------------------------------------------------------------------------- | | \_from | address | the address which sends the composed message. in most cases, it is the Oapp's address. | | \_to | address | the address which receives the composed message | | \_guid | bytes32 | the message guid | | \_index | uint16 | | | \_message | bytes | the message | | \_extraData | bytes | the extra data provided by the executor. this data is untrusted and should be validated. | ### lzComposeAlert ```solidity wrap theme={null} function lzComposeAlert(address _from, address _to, bytes32 _guid, uint16 _index, uint256 _gas, uint256 _value, bytes _message, bytes _extraData, bytes _reason) external ``` The `lzComposeAlert` function is triggered when an issue occurs during message composition. It allows the contract to report why a composed message could not be processed. #### Parameters | Name | Type | Description | | ----------- | ------- | ----------------------------------------------- | | \_from | address | the address which sends the composed message | | \_to | address | the address which receives the composed message | | \_guid | bytes32 | the message guid | | \_index | uint16 | | | \_gas | uint256 | | | \_value | uint256 | | | \_message | bytes | the message | | \_extraData | bytes | the extra data provided by the executor | | \_reason | bytes | the reason why the message is not received | ## MessagingContext The `MessagingContext` contract acts as a guard for preventing reentrancy and also provides execution context for messages sent and received in LayerZero. this contract acts as a non-reentrancy guard and a source of messaging context the context includes the remote eid and the sender address it separates the send and receive context to allow messaging receipts (send back on `receive()`) ### sendContext ```solidity wrap theme={null} modifier sendContext(uint32 _dstEid, address _sender) ``` The `sendContext` modifier sets the execution context for the message being sent. It encodes the context as a combination of the destination endpoint ID (`_dstEid`) and the sender's address. This context helps track the message's origin and ensures that only authorized parties can interact with it. *the sendContext is set to 8 bytes 0s + 4 bytes eid + 20 bytes sender* ### isSendingMessage ```solidity wrap theme={null} function isSendingMessage() public view returns (bool) ``` The `isSendingMessage` function returns true if the contract is in the process of sending a message. It helps prevent reentrant calls during message processing. *returns true if sending message* ### getSendContext ```solidity wrap theme={null} function getSendContext() external view returns (uint32, address) ``` The `getSendContext` function retrieves the current send context, returning the destination endpoint ID and sender's address if a message is being sent. If no message is being sent, it returns `(0, 0)`. *returns (eid, sender) if sending message, (0, 0) otherwise* ### \_getSendContext ```solidity wrap theme={null} function _getSendContext(uint256 _context) internal pure returns (uint32, address) ``` The `_getSendContext` function decodes the provided \_context into its component parts: the destination endpoint ID and the sender's address. This function is used internally to reconstruct the message context when needed. ## ILayerZeroComposer `ILayerZeroComposer` defines the interface for composing messages in LayerZero. It standardizes how OApps send composed messages and ensures non-reentrancy. ### lzCompose ```solidity wrap theme={null} function lzCompose(address _from, bytes32 _guid, bytes _message, address _executor, bytes _extraData) external payable ``` The `lzCompose` function is responsible for composing LayerZero messages from an OApp. To ensure that reentrancy is avoided, this function asserts that `msg.sender` is the corresponding `EndpointV2` contract and from the correct `OApp`. *To ensure non-reentrancy, implementers of this interface MUST assert msg.sender is the corresponding EndpointV2 contract (i.e., onlyEndpointV2).* #### Parameters | Name | Type | Description | | ----------- | ------- | --------------------------------------------------------------------------------------------- | | \_from | address | The address initiating the composition, typically the OApp where the lzReceive was called. | | \_guid | bytes32 | The unique identifier for the corresponding LayerZero src/dst tx. | | \_message | bytes | The composed message payload in bytes. NOT necessarily the same payload passed via lzReceive. | | \_executor | address | The address of the executor for the composed message. | | \_extraData | bytes | Additional arbitrary data in bytes passed by the entity who executes the lzCompose. | ## MessagingParams ```solidity wrap theme={null} struct MessagingParams { uint32 dstEid; bytes32 receiver; bytes message; bytes options; bool payInLzToken; } ``` The `MessagingParams` struct is used to define the parameters required for sending a LayerZero message. These parameters specify the destination endpoint, the message's recipient, the actual message payload, and any additional options for the message. | Name | Type | Description | | ------------ | ------- | ----------------------------------------------------------------------------------------------------------------------- | | dstEid | uint32 | The destination endpoint ID for the message. This identifies the chain and endpoint to which the message is being sent. | | receiver | bytes32 | The address (in bytes32 format) of the receiver on the destination chain. | | message | bytes | The actual message payload to be transmitted. | | options | bytes | Additional options for the message, such as execution settings or gas limitations. | | payInLzToken | bool | A boolean indicating whether the fees for the message will be paid in LayerZero (LZ) tokens. | ## MessagingReceipt ```solidity wrap theme={null} struct MessagingReceipt { bytes32 guid; uint64 nonce; struct MessagingFee fee; } ``` The `MessagingReceipt` struct provides information about a successfully sent LayerZero message, including a unique identifier (`GUID`), the `nonce`, and the `fee` details. ## MessagingFee ```solidity wrap theme={null} struct MessagingFee { uint256 nativeFee; uint256 lzTokenFee; } ``` The `MessagingFee` struct details the costs involved in sending a message, specifying the native token fee and the fee in LayerZero tokens (if applicable). ## Origin ```solidity wrap theme={null} struct Origin { uint32 srcEid; bytes32 sender; uint64 nonce; } ``` The Origin struct provides details about the source of a LayerZero message, including the source endpoint ID, the sender's address, and the message's nonce. ## ILayerZeroEndpointV2 This interface defines the main interaction points for the LayerZero V2 protocol, which includes message quoting, sending, verification, and event logging for the protocol. ### PacketSent ```solidity wrap theme={null} event PacketSent(bytes encodedPayload, bytes options, address sendLibrary) ``` Emitted when a message packet is sent to a destination endpoint. ### PacketVerified ```solidity wrap theme={null} event PacketVerified(struct Origin origin, address receiver, bytes32 payloadHash) ``` Emitted when a message packet is verified on the destination endpoint. ### PacketDelivered ```solidity wrap theme={null} event PacketDelivered(struct Origin origin, address receiver) ``` Emitted when a message packet is successfully delivered to the destination receiver. ### LzReceiveAlert ```solidity wrap theme={null} event LzReceiveAlert(address receiver, address executor, struct Origin origin, bytes32 guid, uint256 gas, uint256 value, bytes message, bytes extraData, bytes reason) ``` Emitted when an issue occurs during the receipt of a message, such as insufficient gas or a failure in message execution. ### LzTokenSet ```solidity wrap theme={null} event LzTokenSet(address token) ``` Emitted when the LayerZero token address is set or updated. ### DelegateSet ```solidity wrap theme={null} event DelegateSet(address sender, address delegate) ``` Emitted when a delegate is authorized by an OApp to configure LayerZero settings. ### quote ```solidity wrap theme={null} function quote(struct MessagingParams _params, address _sender) external view returns (struct MessagingFee) ``` ### send ```solidity wrap theme={null} function send(struct MessagingParams _params, address _refundAddress) external payable returns (struct MessagingReceipt) ``` ### verify ```solidity wrap theme={null} function verify(struct Origin _origin, address _receiver, bytes32 _payloadHash) external ``` ### verifiable ```solidity wrap theme={null} function verifiable(struct Origin _origin, address _receiver) external view returns (bool) ``` ### initializable ```solidity wrap theme={null} function initializable(struct Origin _origin, address _receiver) external view returns (bool) ``` ### lzReceive ```solidity wrap theme={null} function lzReceive(struct Origin _origin, address _receiver, bytes32 _guid, bytes _message, bytes _extraData) external payable ``` ### clear ```solidity wrap theme={null} function clear(address _oapp, struct Origin _origin, bytes32 _guid, bytes _message) external ``` ### setLzToken ```solidity wrap theme={null} function setLzToken(address _lzToken) external ``` ### lzToken ```solidity wrap theme={null} function lzToken() external view returns (address) ``` ### nativeToken ```solidity wrap theme={null} function nativeToken() external view returns (address) ``` ### setDelegate ```solidity wrap theme={null} function setDelegate(address _delegate) external ``` ## ILayerZeroReceiver This interface defines the core message-receiving functionality on LayerZero to be implemented by receiver applications. ### allowInitializePath ```solidity wrap theme={null} function allowInitializePath(struct Origin _origin) external view returns (bool) ``` Returns whether the path from the origin can be initialized. ### nextNonce ```solidity wrap theme={null} function nextNonce(uint32 _eid, bytes32 _sender) external view returns (uint64) ``` Returns the next nonce for a sender on the specified endpoint. ### lzReceive ```solidity wrap theme={null} function lzReceive(struct Origin _origin, bytes32 _guid, bytes _message, address _executor, bytes _extraData) external payable ``` Processes the received message on the destination chain. ## MessageLibType ```solidity wrap theme={null} enum MessageLibType { Send, Receive, SendAndReceive } ``` The MessageLibType enum defines the possible types of messaging libraries in LayerZero. * `Send`: A library that only handles sending messages. * `Receive`: A library that only handles receiving messages. * `SendAndReceive`: A library that handles both sending and receiving messages. ## IMessageLib The `IMessageLib` interface defines functions that allow configuration of messaging libraries, checking endpoint support, and obtaining versioning and library type details. ### setConfig ```solidity wrap theme={null} function setConfig(address _oapp, struct SetConfigParam[] _config) external ``` Allows an OApp (Omnichain Application) to set configuration parameters for a specific messaging library. ### getConfig ```solidity wrap theme={null} function getConfig(uint32 _eid, address _oapp, uint32 _configType) external view returns (bytes config) ``` Fetches the configuration of an OApp for a specific endpoint and configuration type. ### isSupportedEid ```solidity wrap theme={null} function isSupportedEid(uint32 _eid) external view returns (bool) ``` Checks if the messaging library supports a specific endpoint ID (`_eid`). ### version ```solidity wrap theme={null} function version() external view returns (uint64 major, uint8 minor, uint8 endpointVersion) ``` Returns the version of the messaging library, including the major, minor, and endpoint version numbers. ### messageLibType ```solidity wrap theme={null} function messageLibType() external view returns (enum MessageLibType) ``` Returns the type of the messaging library (`Send`, `Receive`, or `SendAndReceive`) as defined in the `MessageLibType` enum. ## SetConfigParam ```solidity wrap theme={null} struct SetConfigParam { uint32 eid; uint32 configType; bytes config; } ``` The `SetConfigParam` struct defines configuration settings for registered Message Libraries. ## IMessageLibManager The `IMessageLibManager` interface manages the registration of messaging libraries, setting default libraries, and handling receive library timeouts. ### Timeout ```solidity wrap theme={null} struct Timeout { address lib; uint256 expiry; } ``` The `Timeout` struct defines the expiration settings for a messaging library that has been changed. ### LibraryRegistered ```solidity wrap theme={null} event LibraryRegistered(address newLib) ``` Emitted when a new library is registered. ### DefaultSendLibrarySet ```solidity wrap theme={null} event DefaultSendLibrarySet(uint32 eid, address newLib) ``` Emitted when the default send library is set for a specific endpoint. ### DefaultReceiveLibrarySet ```solidity wrap theme={null} event DefaultReceiveLibrarySet(uint32 eid, address newLib) ``` Emitted when the default receive library is set for a specific endpoint. ### DefaultReceiveLibraryTimeoutSet ```solidity wrap theme={null} event DefaultReceiveLibraryTimeoutSet(uint32 eid, address oldLib, uint256 expiry) ``` Emitted when a timeout is set for the default receive library. ### SendLibrarySet ```solidity wrap theme={null} event SendLibrarySet(address sender, uint32 eid, address newLib) ``` Emitted when a send library is set for an OApp. ### ReceiveLibrarySet ```solidity wrap theme={null} event ReceiveLibrarySet(address receiver, uint32 eid, address newLib) ``` Emitted when a receive library is set for an OApp. ### ReceiveLibraryTimeoutSet ```solidity wrap theme={null} event ReceiveLibraryTimeoutSet(address receiver, uint32 eid, address oldLib, uint256 timeout) ``` Emitted when a receive library timeout is set for an OApp. ### registerLibrary ```solidity wrap theme={null} function registerLibrary(address _lib) external ``` Registers a new messaging library that will be available for endpoints. ### isRegisteredLibrary ```solidity wrap theme={null} function isRegisteredLibrary(address _lib) external view returns (bool) ``` Checks if a messaging library is registered. ### getRegisteredLibraries ```solidity wrap theme={null} function getRegisteredLibraries() external view returns (address[]) ``` Returns a list of all registered libraries. ### setDefaultSendLibrary ```solidity wrap theme={null} function setDefaultSendLibrary(uint32 _eid, address _newLib) external ``` Sets the default send library for a specific endpoint. ### defaultSendLibrary ```solidity wrap theme={null} function defaultSendLibrary(uint32 _eid) external view returns (address) ``` Gets the current default send library for a specific endpoint. ### setDefaultReceiveLibrary ```solidity wrap theme={null} function setDefaultReceiveLibrary(uint32 _eid, address _newLib, uint256 _gracePeriod) external ``` Sets the default receive library for a specific endpoint and specifies a grace period for migration. ### defaultReceiveLibrary ```solidity wrap theme={null} function defaultReceiveLibrary(uint32 _eid) external view returns (address) ``` Gets the current default receive library for a specific endpoint. ### setDefaultReceiveLibraryTimeout ```solidity wrap theme={null} function setDefaultReceiveLibraryTimeout(uint32 _eid, address _lib, uint256 _expiry) external ``` Sets the timeout for a default receive library. ### defaultReceiveLibraryTimeout ```solidity wrap theme={null} function defaultReceiveLibraryTimeout(uint32 _eid) external view returns (address lib, uint256 expiry) ``` Gets the default receive library timeout for a specific endpoint. ### isSupportedEid ```solidity wrap theme={null} function isSupportedEid(uint32 _eid) external view returns (bool) ``` ### isValidReceiveLibrary ```solidity wrap theme={null} function isValidReceiveLibrary(address _receiver, uint32 _eid, address _lib) external view returns (bool) ``` ### setSendLibrary ```solidity wrap theme={null} function setSendLibrary(address _oapp, uint32 _eid, address _newLib) external ``` Sets a send library for an OApp for a specific endpoint. ### getSendLibrary ```solidity wrap theme={null} function getSendLibrary(address _sender, uint32 _eid) external view returns (address lib) ``` ### isDefaultSendLibrary ```solidity wrap theme={null} function isDefaultSendLibrary(address _sender, uint32 _eid) external view returns (bool) ``` ### setReceiveLibrary ```solidity wrap theme={null} function setReceiveLibrary(address _oapp, uint32 _eid, address _newLib, uint256 _gracePeriod) external ``` ### getReceiveLibrary ```solidity wrap theme={null} function getReceiveLibrary(address _receiver, uint32 _eid) external view returns (address lib, bool isDefault) ``` ### setReceiveLibraryTimeout ```solidity wrap theme={null} function setReceiveLibraryTimeout(address _oapp, uint32 _eid, address _lib, uint256 _expiry) external ``` ### receiveLibraryTimeout ```solidity wrap theme={null} function receiveLibraryTimeout(address _receiver, uint32 _eid) external view returns (address lib, uint256 expiry) ``` ### setConfig ```solidity wrap theme={null} function setConfig(address _oapp, address _lib, struct SetConfigParam[] _params) external ``` ### getConfig ```solidity wrap theme={null} function getConfig(address _oapp, address _lib, uint32 _eid, uint32 _configType) external view returns (bytes config) ``` ## IMessagingChannel ### InboundNonceSkipped ```solidity wrap theme={null} event InboundNonceSkipped(uint32 srcEid, bytes32 sender, address receiver, uint64 nonce) ``` ### PacketNilified ```solidity wrap theme={null} event PacketNilified(uint32 srcEid, bytes32 sender, address receiver, uint64 nonce, bytes32 payloadHash) ``` ### PacketBurnt ```solidity wrap theme={null} event PacketBurnt(uint32 srcEid, bytes32 sender, address receiver, uint64 nonce, bytes32 payloadHash) ``` ### eid ```solidity wrap theme={null} function eid() external view returns (uint32) ``` ### skip ```solidity wrap theme={null} function skip(address _oapp, uint32 _srcEid, bytes32 _sender, uint64 _nonce) external ``` ### nilify ```solidity wrap theme={null} function nilify(address _oapp, uint32 _srcEid, bytes32 _sender, uint64 _nonce, bytes32 _payloadHash) external ``` ### burn ```solidity wrap theme={null} function burn(address _oapp, uint32 _srcEid, bytes32 _sender, uint64 _nonce, bytes32 _payloadHash) external ``` ### nextGuid ```solidity wrap theme={null} function nextGuid(address _sender, uint32 _dstEid, bytes32 _receiver) external view returns (bytes32) ``` ### inboundNonce ```solidity wrap theme={null} function inboundNonce(address _receiver, uint32 _srcEid, bytes32 _sender) external view returns (uint64) ``` ### outboundNonce ```solidity wrap theme={null} function outboundNonce(address _sender, uint32 _dstEid, bytes32 _receiver) external view returns (uint64) ``` ### inboundPayloadHash ```solidity wrap theme={null} function inboundPayloadHash(address _receiver, uint32 _srcEid, bytes32 _sender, uint64 _nonce) external view returns (bytes32) ``` ### lazyInboundNonce ```solidity wrap theme={null} function lazyInboundNonce(address _receiver, uint32 _srcEid, bytes32 _sender) external view returns (uint64) ``` ## IMessagingComposer ### ComposeSent ```solidity wrap theme={null} event ComposeSent(address from, address to, bytes32 guid, uint16 index, bytes message) ``` ### ComposeDelivered ```solidity wrap theme={null} event ComposeDelivered(address from, address to, bytes32 guid, uint16 index) ``` ### LzComposeAlert ```solidity wrap theme={null} event LzComposeAlert(address from, address to, address executor, bytes32 guid, uint16 index, uint256 gas, uint256 value, bytes message, bytes extraData, bytes reason) ``` ### composeQueue ```solidity wrap theme={null} function composeQueue(address _from, address _to, bytes32 _guid, uint16 _index) external view returns (bytes32 messageHash) ``` ### sendCompose ```solidity wrap theme={null} function sendCompose(address _to, bytes32 _guid, uint16 _index, bytes _message) external ``` ### lzCompose ```solidity wrap theme={null} function lzCompose(address _from, address _to, bytes32 _guid, uint16 _index, bytes _message, bytes _extraData) external payable ``` ## IMessagingContext ### isSendingMessage ```solidity wrap theme={null} function isSendingMessage() external view returns (bool) ``` ### getSendContext ```solidity wrap theme={null} function getSendContext() external view returns (uint32 dstEid, address sender) ``` ## Packet ```solidity wrap theme={null} struct Packet { uint64 nonce; uint32 srcEid; address sender; uint32 dstEid; bytes32 receiver; bytes32 guid; bytes message; } ``` The `Packet` struct represents the data structure used for LayerZero messaging between endpoints. It includes important metadata such as `sender`, `receiver`, `nonce`, and the `message` itself. ## ISendLib The `ISendLib` interface defines the functions necessary for sending packets, estimating messaging fees, and handling fee withdrawals for LayerZero messaging. ### send ```solidity wrap theme={null} function send(struct Packet _packet, bytes _options, bool _payInLzToken) external returns (struct MessagingFee, bytes encodedPacket) ``` Sends a LayerZero message packet and returns the required fees and the encoded packet data. * `_packet`: The Packet struct containing the message to be sent. * `_options`: Byte-encoded options for the message. * `_payInLzToken`: Boolean flag indicating whether fees should be paid in LzToken. Returns: * `MessagingFee`: The fees for the message, divided into native and LzToken fees. * `encodedPacket`: The encoded message packet in bytes. ### quote ```solidity wrap theme={null} function quote(struct Packet _packet, bytes _options, bool _payInLzToken) external view returns (struct MessagingFee) ``` Estimates the messaging fee for sending a LayerZero packet. ### setTreasury ```solidity wrap theme={null} function setTreasury(address _treasury) external ``` Sets the treasury address to receive collected fees. ### withdrawFee ```solidity wrap theme={null} function withdrawFee(address _to, uint256 _amount) external ``` Withdraws native token fees collected by the contract. ### withdrawLzTokenFee ```solidity wrap theme={null} function withdrawLzTokenFee(address _lzToken, address _to, uint256 _amount) external ``` Withdraws LayerZero token fees collected by the contract. ## AddressCast The `AddressCast` library provides utility functions for casting between addresses and their byte representations. It also includes error handling for invalid address sizes. ### AddressCast\_InvalidSizeForAddress ```solidity wrap theme={null} error AddressCast_InvalidSizeForAddress() ``` Thrown when the size of the byte array for an address is invalid. ### AddressCast\_InvalidAddress ```solidity wrap theme={null} error AddressCast_InvalidAddress() ``` Thrown when an invalid address is provided. ### toBytes32 ```solidity wrap theme={null} function toBytes32(bytes _addressBytes) internal pure returns (bytes32 result) ``` Casts a byte array to a `bytes32` representation of an address. ### toBytes32 ```solidity wrap theme={null} function toBytes32(address _address) internal pure returns (bytes32 result) ``` Casts an `address` to its `bytes32` representation. ### toBytes ```solidity wrap theme={null} function toBytes(bytes32 _addressBytes32, uint256 _size) internal pure returns (bytes result) ``` Casts a `bytes32` address to its byte array form, with a specified size. ### toAddress ```solidity wrap theme={null} function toAddress(bytes32 _addressBytes32) internal pure returns (address result) ``` Casts a `bytes32` representation of an address back to an address. ### toAddress ```solidity wrap theme={null} function toAddress(bytes _addressBytes) internal pure returns (address result) ``` Casts a byte array back to an `address`. ## CalldataBytesLib The `CalldataBytesLib` provides functions to convert portions of `calldata` (byte arrays) into various Solidity types. These functions help when dealing with raw calldata. ### toU8 ```solidity wrap theme={null} function toU8(bytes _bytes, uint256 _start) internal pure returns (uint8) ``` Converts a portion of a byte array to a `uint8` starting at the given position. ### toU16 ```solidity wrap theme={null} function toU16(bytes _bytes, uint256 _start) internal pure returns (uint16) ``` Converts a portion of a byte array to a `uint16` starting at the given position. ### toU32 ```solidity wrap theme={null} function toU32(bytes _bytes, uint256 _start) internal pure returns (uint32) ``` Converts a portion of a byte array to a `uint32` starting at the given position. ### toU64 ```solidity wrap theme={null} function toU64(bytes _bytes, uint256 _start) internal pure returns (uint64) ``` Converts a portion of a byte array to a `uint64` starting at the given position. ### toU128 ```solidity wrap theme={null} function toU128(bytes _bytes, uint256 _start) internal pure returns (uint128) ``` Converts a portion of a byte array to a `uint128` starting at the given position. ### toU256 ```solidity wrap theme={null} function toU256(bytes _bytes, uint256 _start) internal pure returns (uint256) ``` Converts a portion of a byte array to a `uint256` starting at the given position. ### toAddr ```solidity wrap theme={null} function toAddr(bytes _bytes, uint256 _start) internal pure returns (address) ``` Converts a portion of a byte array to an `address` starting at the given position. ### toB32 ```solidity wrap theme={null} function toB32(bytes _bytes, uint256 _start) internal pure returns (bytes32) ``` Converts a portion of a byte array to a `bytes32` starting at the given position. ## Errors ### LZ\_LzTokenUnavailable ```solidity wrap theme={null} error LZ_LzTokenUnavailable() ``` ### LZ\_InvalidReceiveLibrary ```solidity wrap theme={null} error LZ_InvalidReceiveLibrary() ``` ### LZ\_InvalidNonce ```solidity wrap theme={null} error LZ_InvalidNonce(uint64 nonce) ``` ### LZ\_InvalidArgument ```solidity wrap theme={null} error LZ_InvalidArgument() ``` ### LZ\_InvalidExpiry ```solidity wrap theme={null} error LZ_InvalidExpiry() ``` ### LZ\_InvalidAmount ```solidity wrap theme={null} error LZ_InvalidAmount(uint256 required, uint256 supplied) ``` ### LZ\_OnlyRegisteredOrDefaultLib ```solidity wrap theme={null} error LZ_OnlyRegisteredOrDefaultLib() ``` ### LZ\_OnlyRegisteredLib ```solidity wrap theme={null} error LZ_OnlyRegisteredLib() ``` ### LZ\_OnlyNonDefaultLib ```solidity wrap theme={null} error LZ_OnlyNonDefaultLib() ``` ### LZ\_Unauthorized ```solidity wrap theme={null} error LZ_Unauthorized() ``` ### LZ\_DefaultSendLibUnavailable ```solidity wrap theme={null} error LZ_DefaultSendLibUnavailable() ``` ### LZ\_DefaultReceiveLibUnavailable ```solidity wrap theme={null} error LZ_DefaultReceiveLibUnavailable() ``` ### LZ\_PathNotInitializable ```solidity wrap theme={null} error LZ_PathNotInitializable() ``` ### LZ\_PathNotVerifiable ```solidity wrap theme={null} error LZ_PathNotVerifiable() ``` ### LZ\_OnlySendLib ```solidity wrap theme={null} error LZ_OnlySendLib() ``` ### LZ\_OnlyReceiveLib ```solidity wrap theme={null} error LZ_OnlyReceiveLib() ``` ### LZ\_UnsupportedEid ```solidity wrap theme={null} error LZ_UnsupportedEid() ``` ### LZ\_UnsupportedInterface ```solidity wrap theme={null} error LZ_UnsupportedInterface() ``` ### LZ\_AlreadyRegistered ```solidity wrap theme={null} error LZ_AlreadyRegistered() ``` ### LZ\_SameValue ```solidity wrap theme={null} error LZ_SameValue() ``` ### LZ\_InvalidPayloadHash ```solidity wrap theme={null} error LZ_InvalidPayloadHash() ``` ### LZ\_PayloadHashNotFound ```solidity wrap theme={null} error LZ_PayloadHashNotFound(bytes32 expected, bytes32 actual) ``` ### LZ\_ComposeNotFound ```solidity wrap theme={null} error LZ_ComposeNotFound(bytes32 expected, bytes32 actual) ``` ### LZ\_ComposeExists ```solidity wrap theme={null} error LZ_ComposeExists() ``` ### LZ\_SendReentrancy ```solidity wrap theme={null} error LZ_SendReentrancy() ``` ### LZ\_NotImplemented ```solidity wrap theme={null} error LZ_NotImplemented() ``` ### LZ\_InsufficientFee ```solidity wrap theme={null} error LZ_InsufficientFee(uint256 requiredNative, uint256 suppliedNative, uint256 requiredLzToken, uint256 suppliedLzToken) ``` ### LZ\_ZeroLzTokenFee ```solidity wrap theme={null} error LZ_ZeroLzTokenFee() ``` ## GUID ### generate ```solidity wrap theme={null} function generate(uint64 _nonce, uint32 _srcEid, address _sender, uint32 _dstEid, bytes32 _receiver) internal pure returns (bytes32) ``` ## Transfer ### ADDRESS\_ZERO ```solidity wrap theme={null} address ADDRESS_ZERO ``` ### Transfer\_NativeFailed ```solidity wrap theme={null} error Transfer_NativeFailed(address _to, uint256 _value) ``` ### Transfer\_ToAddressIsZero ```solidity wrap theme={null} error Transfer_ToAddressIsZero() ``` ### native ```solidity wrap theme={null} function native(address _to, uint256 _value) internal ``` ### token ```solidity wrap theme={null} function token(address _token, address _to, uint256 _value) internal ``` ### nativeOrToken ```solidity wrap theme={null} function nativeOrToken(address _token, address _to, uint256 _value) internal ``` ## BlockedMessageLib ### supportsInterface ```solidity wrap theme={null} function supportsInterface(bytes4 interfaceId) public view returns (bool) ``` *See `IERC165` and `supportsInterface`.* ### version ```solidity wrap theme={null} function version() external pure returns (uint64 major, uint8 minor, uint8 endpointVersion) ``` ### messageLibType ```solidity wrap theme={null} function messageLibType() external pure returns (enum MessageLibType) ``` ### isSupportedEid ```solidity wrap theme={null} function isSupportedEid(uint32) external pure returns (bool) ``` ### fallback ```solidity wrap theme={null} fallback() external ``` ## BitMaps ### get ```solidity wrap theme={null} function get(BitMap256 bitmap, uint8 index) internal pure returns (bool) ``` *Returns whether the bit at `index` is set.* ### set ```solidity wrap theme={null} function set(BitMap256 bitmap, uint8 index) internal pure returns (BitMap256) ``` *Sets the bit at `index`.* ## ExecutorOptions ### WORKER\_ID ```solidity wrap theme={null} uint8 WORKER_ID ``` ### OPTION\_TYPE\_LZRECEIVE ```solidity wrap theme={null} uint8 OPTION_TYPE_LZRECEIVE ``` ### OPTION\_TYPE\_NATIVE\_DROP ```solidity wrap theme={null} uint8 OPTION_TYPE_NATIVE_DROP ``` ### OPTION\_TYPE\_LZCOMPOSE ```solidity wrap theme={null} uint8 OPTION_TYPE_LZCOMPOSE ``` ### OPTION\_TYPE\_ORDERED\_EXECUTION ```solidity wrap theme={null} uint8 OPTION_TYPE_ORDERED_EXECUTION ``` ### Executor\_InvalidLzReceiveOption ```solidity wrap theme={null} error Executor_InvalidLzReceiveOption() ``` ### Executor\_InvalidNativeDropOption ```solidity wrap theme={null} error Executor_InvalidNativeDropOption() ``` ### Executor\_InvalidLzComposeOption ```solidity wrap theme={null} error Executor_InvalidLzComposeOption() ``` ### nextExecutorOption ```solidity wrap theme={null} function nextExecutorOption(bytes _options, uint256 _cursor) internal pure returns (uint8 optionType, bytes option, uint256 cursor) ``` *decode the next executor option from the options starting from the specified cursor* #### Parameters | Name | Type | Description | | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | \_options | bytes | \[executor\_id]\[executor\_option]\[executor\_id]\[executor\_option]... executor\_option = \[option\_size]\[option\_type]\[option] option\_size = len(option\_type) + len(option) executor\_id: uint8, option\_size: uint16, option\_type: uint8, option: bytes | | \_cursor | uint256 | the cursor to start decoding from | #### Return Values | Name | Type | Description | | ---------- | ------- | ----------------------------------------------------- | | optionType | uint8 | the type of the option | | option | bytes | the option of the executor | | cursor | uint256 | the cursor to start decoding the next executor option | ### decodeLzReceiveOption ```solidity wrap theme={null} function decodeLzReceiveOption(bytes _option) internal pure returns (uint128 gas, uint128 value) ``` ### decodeNativeDropOption ```solidity wrap theme={null} function decodeNativeDropOption(bytes _option) internal pure returns (uint128 amount, bytes32 receiver) ``` ### decodeLzComposeOption ```solidity wrap theme={null} function decodeLzComposeOption(bytes _option) internal pure returns (uint16 index, uint128 gas, uint128 value) ``` ### encodeLzReceiveOption ```solidity wrap theme={null} function encodeLzReceiveOption(uint128 _gas, uint128 _value) internal pure returns (bytes) ``` ### encodeNativeDropOption ```solidity wrap theme={null} function encodeNativeDropOption(uint128 _amount, bytes32 _receiver) internal pure returns (bytes) ``` ### encodeLzComposeOption ```solidity wrap theme={null} function encodeLzComposeOption(uint16 _index, uint128 _gas, uint128 _value) internal pure returns (bytes) ``` ## PacketV1Codec ### PACKET\_VERSION ```solidity wrap theme={null} uint8 PACKET_VERSION ``` ### encode ```solidity wrap theme={null} function encode(struct Packet _packet) internal pure returns (bytes encodedPacket) ``` ### encodePacketHeader ```solidity wrap theme={null} function encodePacketHeader(struct Packet _packet) internal pure returns (bytes) ``` ### encodePayload ```solidity wrap theme={null} function encodePayload(struct Packet _packet) internal pure returns (bytes) ``` ### header ```solidity wrap theme={null} function header(bytes _packet) internal pure returns (bytes) ``` ### version ```solidity wrap theme={null} function version(bytes _packet) internal pure returns (uint8) ``` ### nonce ```solidity wrap theme={null} function nonce(bytes _packet) internal pure returns (uint64) ``` ### srcEid ```solidity wrap theme={null} function srcEid(bytes _packet) internal pure returns (uint32) ``` ### sender ```solidity wrap theme={null} function sender(bytes _packet) internal pure returns (bytes32) ``` ### senderAddressB20 ```solidity wrap theme={null} function senderAddressB20(bytes _packet) internal pure returns (address) ``` ### dstEid ```solidity wrap theme={null} function dstEid(bytes _packet) internal pure returns (uint32) ``` ### receiver ```solidity wrap theme={null} function receiver(bytes _packet) internal pure returns (bytes32) ``` ### receiverB20 ```solidity wrap theme={null} function receiverB20(bytes _packet) internal pure returns (address) ``` ### guid ```solidity wrap theme={null} function guid(bytes _packet) internal pure returns (bytes32) ``` ### message ```solidity wrap theme={null} function message(bytes _packet) internal pure returns (bytes) ``` ### payload ```solidity wrap theme={null} function payload(bytes _packet) internal pure returns (bytes) ``` ### payloadHash ```solidity wrap theme={null} function payloadHash(bytes _packet) internal pure returns (bytes32) ``` ## MessageLibBase This contract serves as a base for handling the communication between the LayerZero endpoint and a specific chain (referred to by its `localEid`). It simplifies the initialization and enforcement of endpoint-specific logic. *simply a container of endpoint address and local eid* ### endpoint ```solidity wrap theme={null} address endpoint ``` Holds the address of the LayerZero endpoint on this chain. ### localEid ```solidity wrap theme={null} uint32 localEid ``` A unique identifier (Eid) for the local chain. ### LZ\_MessageLib\_OnlyEndpoint ```solidity wrap theme={null} error LZ_MessageLib_OnlyEndpoint() ``` Error thrown when a function is accessed by a non-endpoint address. ### onlyEndpoint ```solidity wrap theme={null} modifier onlyEndpoint() ``` A modifier ensuring that only the LayerZero endpoint can call specific functions. ### constructor ```solidity wrap theme={null} constructor(address _endpoint, uint32 _localEid) internal ``` Initializes the contract with the LayerZero endpoint and `localEid`. ## ReceiveLibBaseE2 This is the base contract for handling the receive-side logic of messages in LayerZero V2. It simplifies the process compared to V1 by removing complexities like nonce management and executor whitelisting. *receive-side message library base contract on endpoint v2. it does not have the complication as the one of endpoint v1, such as nonce, executor whitelist, etc.* ### constructor ```solidity wrap theme={null} constructor(address _endpoint) internal ``` Initializes the contract with the LayerZero endpoint. ### supportsInterface ```solidity wrap theme={null} function supportsInterface(bytes4 _interfaceId) public view virtual returns (bool) ``` Determines whether the contract supports a specific interface. ### messageLibType ```solidity wrap theme={null} function messageLibType() external pure virtual returns (enum MessageLibType) ``` Specifies the type of the message library being used (e.g., for differentiation between send and receive libraries). ## WorkerOptions ```solidity wrap theme={null} struct WorkerOptions { uint8 workerId; bytes options; } ``` Defines options for specific worker configurations (e.g., a worker ID and additional options). ## SetDefaultExecutorConfigParam ```solidity wrap theme={null} struct SetDefaultExecutorConfigParam { uint32 eid; struct ExecutorConfig config; } ``` Used to configure the default settings for an executor, including the executor's address and max message size for a given chain (eid). ## ExecutorConfig ```solidity wrap theme={null} struct ExecutorConfig { uint32 maxMessageSize; address executor; } ``` ## SendLibBase *base contract for both SendLibBaseE1 and SendLibBaseE2* ### TREASURY\_MAX\_COPY ```solidity wrap theme={null} uint16 TREASURY_MAX_COPY ``` ### treasuryGasLimit ```solidity wrap theme={null} uint256 treasuryGasLimit ``` ### treasuryNativeFeeCap ```solidity wrap theme={null} uint256 treasuryNativeFeeCap ``` ### treasury ```solidity wrap theme={null} address treasury ``` ### executorConfigs ```solidity wrap theme={null} mapping(address => mapping(uint32 => struct ExecutorConfig)) executorConfigs ``` ### fees ```solidity wrap theme={null} mapping(address => uint256) fees ``` ### ExecutorFeePaid ```solidity wrap theme={null} event ExecutorFeePaid(address executor, uint256 fee) ``` ### TreasurySet ```solidity wrap theme={null} event TreasurySet(address treasury) ``` ### DefaultExecutorConfigsSet ```solidity wrap theme={null} event DefaultExecutorConfigsSet(struct SetDefaultExecutorConfigParam[] params) ``` ### ExecutorConfigSet ```solidity wrap theme={null} event ExecutorConfigSet(address oapp, uint32 eid, struct ExecutorConfig config) ``` ### TreasuryNativeFeeCapSet ```solidity wrap theme={null} event TreasuryNativeFeeCapSet(uint256 newTreasuryNativeFeeCap) ``` ### LZ\_MessageLib\_InvalidMessageSize ```solidity wrap theme={null} error LZ_MessageLib_InvalidMessageSize(uint256 actual, uint256 max) ``` ### LZ\_MessageLib\_InvalidAmount ```solidity wrap theme={null} error LZ_MessageLib_InvalidAmount(uint256 requested, uint256 available) ``` ### LZ\_MessageLib\_TransferFailed ```solidity wrap theme={null} error LZ_MessageLib_TransferFailed() ``` ### LZ\_MessageLib\_InvalidExecutor ```solidity wrap theme={null} error LZ_MessageLib_InvalidExecutor() ``` ### LZ\_MessageLib\_ZeroMessageSize ```solidity wrap theme={null} error LZ_MessageLib_ZeroMessageSize() ``` ### constructor ```solidity wrap theme={null} constructor(address _endpoint, uint32 _localEid, uint256 _treasuryGasLimit, uint256 _treasuryNativeFeeCap) internal ``` ### setDefaultExecutorConfigs ```solidity wrap theme={null} function setDefaultExecutorConfigs(struct SetDefaultExecutorConfigParam[] _params) external ``` ### setTreasuryNativeFeeCap ```solidity wrap theme={null} function setTreasuryNativeFeeCap(uint256 _newTreasuryNativeFeeCap) external ``` *the new value can not be greater than the old value, i.e. down only* ### getExecutorConfig ```solidity wrap theme={null} function getExecutorConfig(address _oapp, uint32 _remoteEid) public view returns (struct ExecutorConfig rtnConfig) ``` ### \_assertMessageSize ```solidity wrap theme={null} function _assertMessageSize(uint256 _actual, uint256 _max) internal pure ``` ### \_payExecutor ```solidity wrap theme={null} function _payExecutor(address _executor, uint32 _dstEid, address _sender, uint256 _msgSize, bytes _executorOptions) internal returns (uint256 executorFee) ``` ### \_payTreasury ```solidity wrap theme={null} function _payTreasury(address _sender, uint32 _dstEid, uint256 _totalNativeFee, bool _payInLzToken) internal returns (uint256 treasuryNativeFee, uint256 lzTokenFee) ``` ### \_quote ```solidity wrap theme={null} function _quote(address _sender, uint32 _dstEid, uint256 _msgSize, bool _payInLzToken, bytes _options) internal view returns (uint256, uint256) ``` *the abstract process for quote() is: 0/ split out the executor options and options of other workers 1/ quote workers 2/ quote executor 3/ quote treasury* #### Return Values | Name | Type | Description | | ---- | ------- | --------------------- | | \[0] | uint256 | nativeFee, lzTokenFee | | \[1] | uint256 | | ### \_quoteTreasury ```solidity wrap theme={null} function _quoteTreasury(address _sender, uint32 _dstEid, uint256 _totalNativeFee, bool _payInLzToken) internal view returns (uint256 nativeFee, uint256 lzTokenFee) ``` *this interface should be DoS-free if the user is paying with native. properties 1/ treasury can return an overly high lzToken fee 2/ if treasury returns an overly high native fee, it will be capped by maxNativeFee, which can be reasoned with the configurations 3/ the owner can not configure the treasury in a way that force this function to revert* ### \_parseTreasuryResult ```solidity wrap theme={null} function _parseTreasuryResult(uint256 _totalNativeFee, bool _payInLzToken, bool _success, bytes _result) internal view returns (uint256 nativeFee, uint256 lzTokenFee) ``` ### \_debitFee ```solidity wrap theme={null} function _debitFee(uint256 _amount) internal ``` *authenticated by msg.sender only* ### \_setTreasury ```solidity wrap theme={null} function _setTreasury(address _treasury) internal ``` ### \_setExecutorConfig ```solidity wrap theme={null} function _setExecutorConfig(uint32 _remoteEid, address _oapp, struct ExecutorConfig _config) internal ``` ### \_quoteVerifier ```solidity wrap theme={null} function _quoteVerifier(address _oapp, uint32 _eid, struct WorkerOptions[] _options) internal view virtual returns (uint256 nativeFee) ``` *these two functions will be overridden with specific logics of the library function* ### \_splitOptions ```solidity wrap theme={null} function _splitOptions(bytes _options) internal view virtual returns (bytes executorOptions, struct WorkerOptions[] validationOptions) ``` *this function will split the options into executorOptions and validationOptions* ## SendLibBaseE2 *send-side message library base contract on endpoint v2. design: the high level logic is the same as SendLibBaseE1 1/ with added interfaces 2/ adapt the functions to the new types, like uint32 for eid, address for sender.* ### NativeFeeWithdrawn ```solidity wrap theme={null} event NativeFeeWithdrawn(address worker, address receiver, uint256 amount) ``` ### LzTokenFeeWithdrawn ```solidity wrap theme={null} event LzTokenFeeWithdrawn(address lzToken, address receiver, uint256 amount) ``` ### LZ\_MessageLib\_NotTreasury ```solidity wrap theme={null} error LZ_MessageLib_NotTreasury() ``` ### LZ\_MessageLib\_CannotWithdrawAltToken ```solidity wrap theme={null} error LZ_MessageLib_CannotWithdrawAltToken() ``` ### constructor ```solidity wrap theme={null} constructor(address _endpoint, uint256 _treasuryGasLimit, uint256 _treasuryNativeFeeCap) internal ``` ### supportsInterface ```solidity wrap theme={null} function supportsInterface(bytes4 _interfaceId) public view virtual returns (bool) ``` ### send ```solidity wrap theme={null} function send(struct Packet _packet, bytes _options, bool _payInLzToken) public virtual returns (struct MessagingFee, bytes) ``` ### setTreasury ```solidity wrap theme={null} function setTreasury(address _treasury) external ``` ### withdrawFee ```solidity wrap theme={null} function withdrawFee(address _to, uint256 _amount) external ``` *E2 only* ### withdrawLzTokenFee ```solidity wrap theme={null} function withdrawLzTokenFee(address _lzToken, address _to, uint256 _amount) external ``` \_*lzToken is a user-supplied value because lzToken might change in the endpoint before all lzToken can be taken out E2 only treasury only function* ### quote ```solidity wrap theme={null} function quote(struct Packet _packet, bytes _options, bool _payInLzToken) external view returns (struct MessagingFee) ``` ### messageLibType ```solidity wrap theme={null} function messageLibType() external pure virtual returns (enum MessageLibType) ``` ### \_payWorkers ```solidity wrap theme={null} function _payWorkers(struct Packet _packet, bytes _options) internal returns (bytes encodedPacket, uint256 totalNativeFee) ``` 1/ handle executor 2/ handle other workers ### \_payVerifier ```solidity wrap theme={null} function _payVerifier(struct Packet _packet, struct WorkerOptions[] _options) internal virtual returns (uint256 otherWorkerFees, bytes encodedPacket) ``` ### receive ```solidity wrap theme={null} receive() external payable virtual ``` ## Treasury ### nativeBP ```solidity wrap theme={null} uint256 nativeBP ``` ### lzTokenFee ```solidity wrap theme={null} uint256 lzTokenFee ``` ### lzTokenEnabled ```solidity wrap theme={null} bool lzTokenEnabled ``` ### LZ\_Treasury\_LzTokenNotEnabled ```solidity wrap theme={null} error LZ_Treasury_LzTokenNotEnabled() ``` ### getFee ```solidity wrap theme={null} function getFee(address, uint32, uint256 _totalFee, bool _payInLzToken) external view returns (uint256) ``` ### payFee ```solidity wrap theme={null} function payFee(address, uint32, uint256 _totalFee, bool _payInLzToken) external payable returns (uint256) ``` ### setLzTokenEnabled ```solidity wrap theme={null} function setLzTokenEnabled(bool _lzTokenEnabled) external ``` ### setNativeFeeBP ```solidity wrap theme={null} function setNativeFeeBP(uint256 _nativeBP) external ``` ### setLzTokenFee ```solidity wrap theme={null} function setLzTokenFee(uint256 _lzTokenFee) external ``` ### withdrawLzToken ```solidity wrap theme={null} function withdrawLzToken(address _messageLib, address _lzToken, address _to, uint256 _amount) external ``` ### withdrawNativeFee ```solidity wrap theme={null} function withdrawNativeFee(address _messageLib, address payable _to, uint256 _amount) external ``` ### withdrawToken ```solidity wrap theme={null} function withdrawToken(address _token, address _to, uint256 _amount) external ``` ### \_getFee ```solidity wrap theme={null} function _getFee(uint256 _totalFee, bool _payInLzToken) internal view returns (uint256) ``` ## Worker ### MESSAGE\_LIB\_ROLE ```solidity wrap theme={null} bytes32 MESSAGE_LIB_ROLE ``` ### ALLOWLIST ```solidity wrap theme={null} bytes32 ALLOWLIST ``` ### DENYLIST ```solidity wrap theme={null} bytes32 DENYLIST ``` ### ADMIN\_ROLE ```solidity wrap theme={null} bytes32 ADMIN_ROLE ``` ### workerFeeLib ```solidity wrap theme={null} address workerFeeLib ``` ### allowlistSize ```solidity wrap theme={null} uint64 allowlistSize ``` ### defaultMultiplierBps ```solidity wrap theme={null} uint16 defaultMultiplierBps ``` ### priceFeed ```solidity wrap theme={null} address priceFeed ``` ### supportedOptionTypes ```solidity wrap theme={null} mapping(uint32 => uint8[]) supportedOptionTypes ``` ### constructor ```solidity wrap theme={null} constructor(address[] _messageLibs, address _priceFeed, uint16 _defaultMultiplierBps, address _roleAdmin, address[] _admins) internal ``` #### Parameters | Name | Type | Description | | ---------------------- | ---------- | --------------------------------------------------------------------------------- | | \_messageLibs | address\[] | array of message lib addresses that are granted the MESSAGE\_LIB\_ROLE | | \_priceFeed | address | price feed address | | \_defaultMultiplierBps | uint16 | default multiplier for worker fee | | \_roleAdmin | address | address that is granted the DEFAULT\_ADMIN\_ROLE (can grant and revoke all roles) | | \_admins | address\[] | array of admin addresses that are granted the ADMIN\_ROLE | ### onlyAcl ```solidity wrap theme={null} modifier onlyAcl(address _sender) ``` ### hasAcl ```solidity wrap theme={null} function hasAcl(address _sender) public view returns (bool) ``` \_Access control list using allowlist and denylist 1. if one address is in the denylist -> deny 2. else if address in the allowlist OR allowlist is empty (allows everyone)-> allow 3. else deny\_ #### Parameters | Name | Type | Description | | -------- | ------- | ---------------- | | \_sender | address | address to check | ### setPaused ```solidity wrap theme={null} function setPaused(bool _paused) external ``` *flag to pause execution of workers (if used with whenNotPaused modifier)* #### Parameters | Name | Type | Description | | -------- | ---- | ------------------------------- | | \_paused | bool | true to pause, false to unpause | ### setPriceFeed ```solidity wrap theme={null} function setPriceFeed(address _priceFeed) external ``` #### Parameters | Name | Type | Description | | ----------- | ------- | ------------------ | | \_priceFeed | address | price feed address | ### setWorkerFeeLib ```solidity wrap theme={null} function setWorkerFeeLib(address _workerFeeLib) external ``` #### Parameters | Name | Type | Description | | -------------- | ------- | ---------------------- | | \_workerFeeLib | address | worker fee lib address | ### setDefaultMultiplierBps ```solidity wrap theme={null} function setDefaultMultiplierBps(uint16 _multiplierBps) external ``` #### Parameters | Name | Type | Description | | --------------- | ------ | --------------------------------- | | \_multiplierBps | uint16 | default multiplier for worker fee | ### withdrawFee ```solidity wrap theme={null} function withdrawFee(address _lib, address _to, uint256 _amount) external ``` *supports withdrawing fee from ULN301, ULN302 and more* #### Parameters | Name | Type | Description | | -------- | ------- | -------------------------- | | \_lib | address | message lib address | | \_to | address | address to withdraw fee to | | \_amount | uint256 | amount to withdraw | ### withdrawToken ```solidity wrap theme={null} function withdrawToken(address _token, address _to, uint256 _amount) external ``` *supports withdrawing token from the contract* #### Parameters | Name | Type | Description | | -------- | ------- | ---------------------------- | | \_token | address | token address | | \_to | address | address to withdraw token to | | \_amount | uint256 | amount to withdraw | ### setSupportedOptionTypes ```solidity wrap theme={null} function setSupportedOptionTypes(uint32 _eid, uint8[] _optionTypes) external ``` ### getSupportedOptionTypes ```solidity wrap theme={null} function getSupportedOptionTypes(uint32 _eid) external view returns (uint8[]) ``` ### \_grantRole ```solidity wrap theme={null} function _grantRole(bytes32 _role, address _account) internal ``` *overrides AccessControl to allow for counting of allowlistSize* #### Parameters | Name | Type | Description | | --------- | ------- | ------------------------ | | \_role | bytes32 | role to grant | | \_account | address | address to grant role to | ### \_revokeRole ```solidity wrap theme={null} function _revokeRole(bytes32 _role, address _account) internal ``` *overrides AccessControl to allow for counting of allowlistSize* #### Parameters | Name | Type | Description | | --------- | ------- | --------------------------- | | \_role | bytes32 | role to revoke | | \_account | address | address to revoke role from | ### renounceRole ```solidity wrap theme={null} function renounceRole(bytes32, address) public pure ``` *overrides AccessControl to disable renouncing of roles* ## TargetParam ```solidity wrap theme={null} struct TargetParam { uint8 idx; address addr; } ``` ## DVNParam ```solidity wrap theme={null} struct DVNParam { uint16 idx; address addr; } ``` ## IExecutor ### DstConfigParam ```solidity wrap theme={null} struct DstConfigParam { uint32 dstEid; uint64 lzReceiveBaseGas; uint64 lzComposeBaseGas; uint16 multiplierBps; uint128 floorMarginUSD; uint128 nativeCap; } ``` ### DstConfig ```solidity wrap theme={null} struct DstConfig { uint64 lzReceiveBaseGas; uint16 multiplierBps; uint128 floorMarginUSD; uint128 nativeCap; uint64 lzComposeBaseGas; } ``` ### ExecutionParams ```solidity wrap theme={null} struct ExecutionParams { address receiver; struct Origin origin; bytes32 guid; bytes message; bytes extraData; uint256 gasLimit; } ``` ### NativeDropParams ```solidity wrap theme={null} struct NativeDropParams { address receiver; uint256 amount; } ``` ### DstConfigSet ```solidity wrap theme={null} event DstConfigSet(struct IExecutor.DstConfigParam[] params) ``` ### NativeDropApplied ```solidity wrap theme={null} event NativeDropApplied(struct Origin origin, uint32 dstEid, address oapp, struct IExecutor.NativeDropParams[] params, bool[] success) ``` ### dstConfig ```solidity wrap theme={null} function dstConfig(uint32 _dstEid) external view returns (uint64, uint16, uint128, uint128, uint64) ``` ## IExecutorFeeLib ### FeeParams ```solidity wrap theme={null} struct FeeParams { address priceFeed; uint32 dstEid; address sender; uint256 calldataSize; uint16 defaultMultiplierBps; } ``` ### Executor\_NoOptions ```solidity wrap theme={null} error Executor_NoOptions() ``` ### Executor\_NativeAmountExceedsCap ```solidity wrap theme={null} error Executor_NativeAmountExceedsCap(uint256 amount, uint256 cap) ``` ### Executor\_UnsupportedOptionType ```solidity wrap theme={null} error Executor_UnsupportedOptionType(uint8 optionType) ``` ### Executor\_InvalidExecutorOptions ```solidity wrap theme={null} error Executor_InvalidExecutorOptions(uint256 cursor) ``` ### Executor\_ZeroLzReceiveGasProvided ```solidity wrap theme={null} error Executor_ZeroLzReceiveGasProvided() ``` ### Executor\_ZeroLzComposeGasProvided ```solidity wrap theme={null} error Executor_ZeroLzComposeGasProvided() ``` ### Executor\_EidNotSupported ```solidity wrap theme={null} error Executor_EidNotSupported(uint32 eid) ``` ### getFeeOnSend ```solidity wrap theme={null} function getFeeOnSend(struct IExecutorFeeLib.FeeParams _params, struct IExecutor.DstConfig _dstConfig, bytes _options) external returns (uint256 fee) ``` ### getFee ```solidity wrap theme={null} function getFee(struct IExecutorFeeLib.FeeParams _params, struct IExecutor.DstConfig _dstConfig, bytes _options) external view returns (uint256 fee) ``` ## ILayerZeroExecutor ### assignJob ```solidity wrap theme={null} function assignJob(uint32 _dstEid, address _sender, uint256 _calldataSize, bytes _options) external returns (uint256 price) ``` ### getFee ```solidity wrap theme={null} function getFee(uint32 _dstEid, address _sender, uint256 _calldataSize, bytes _options) external view returns (uint256 price) ``` ## ILayerZeroTreasury ### getFee ```solidity wrap theme={null} function getFee(address _sender, uint32 _dstEid, uint256 _totalNativeFee, bool _payInLzToken) external view returns (uint256 fee) ``` ### payFee ```solidity wrap theme={null} function payFee(address _sender, uint32 _dstEid, uint256 _totalNativeFee, bool _payInLzToken) external payable returns (uint256 fee) ``` ## IWorker ### SetWorkerLib ```solidity wrap theme={null} event SetWorkerLib(address workerLib) ``` ### SetPriceFeed ```solidity wrap theme={null} event SetPriceFeed(address priceFeed) ``` ### SetDefaultMultiplierBps ```solidity wrap theme={null} event SetDefaultMultiplierBps(uint16 multiplierBps) ``` ### SetSupportedOptionTypes ```solidity wrap theme={null} event SetSupportedOptionTypes(uint32 dstEid, uint8[] optionTypes) ``` ### Withdraw ```solidity wrap theme={null} event Withdraw(address lib, address to, uint256 amount) ``` ### Worker\_NotAllowed ```solidity wrap theme={null} error Worker_NotAllowed() ``` ### Worker\_OnlyMessageLib ```solidity wrap theme={null} error Worker_OnlyMessageLib() ``` ### Worker\_RoleRenouncingDisabled ```solidity wrap theme={null} error Worker_RoleRenouncingDisabled() ``` ### setPriceFeed ```solidity wrap theme={null} function setPriceFeed(address _priceFeed) external ``` ### priceFeed ```solidity wrap theme={null} function priceFeed() external view returns (address) ``` ### setDefaultMultiplierBps ```solidity wrap theme={null} function setDefaultMultiplierBps(uint16 _multiplierBps) external ``` ### defaultMultiplierBps ```solidity wrap theme={null} function defaultMultiplierBps() external view returns (uint16) ``` ### withdrawFee ```solidity wrap theme={null} function withdrawFee(address _lib, address _to, uint256 _amount) external ``` ### setSupportedOptionTypes ```solidity wrap theme={null} function setSupportedOptionTypes(uint32 _eid, uint8[] _optionTypes) external ``` ### getSupportedOptionTypes ```solidity wrap theme={null} function getSupportedOptionTypes(uint32 _eid) external view returns (uint8[]) ``` ## SafeCall *copied from [https://github.com/nomad-xyz/ExcessivelySafeCall/blob/main/src/ExcessivelySafeCall.sol](https://github.com/nomad-xyz/ExcessivelySafeCall/blob/main/src/ExcessivelySafeCall.sol).* ### safeCall ```solidity wrap theme={null} function safeCall(address _target, uint256 _gas, uint256 _value, uint16 _maxCopy, bytes _calldata) internal returns (bool, bytes) ``` calls a contract with a specified gas limit and value and captures the return data #### Parameters | Name | Type | Description | | ---------- | ------- | ------------------------------------------------------------ | | \_target | address | The address to call | | \_gas | uint256 | The amount of gas to forward to the remote contract | | \_value | uint256 | The value in wei to send to the remote contract to memory. | | \_maxCopy | uint16 | The maximum number of bytes of returndata to copy to memory. | | \_calldata | bytes | The data to send to the remote contract | #### Return Values | Name | Type | Description | | ---- | ----- | ------------------------------------------------------------------------------- | | \[0] | bool | success and returndata, as `.call()`. Returndata is capped to `_maxCopy` bytes. | | \[1] | bytes | | ### safeStaticCall ```solidity wrap theme={null} function safeStaticCall(address _target, uint256 _gas, uint16 _maxCopy, bytes _calldata) internal view returns (bool, bytes) ``` Use when you *really* really *really* don't trust the called contract. This prevents the called contract from causing reversion of the caller in as many ways as we can. *The main difference between this and a solidity low-level call is that we limit the number of bytes that the callee can cause to be copied to caller memory. This prevents stupid things like malicious contracts returning 10,000,000 bytes causing a local OOG when copying to memory.* #### Parameters | Name | Type | Description | | ---------- | ------- | ------------------------------------------------------------ | | \_target | address | The address to call | | \_gas | uint256 | The amount of gas to forward to the remote contract | | \_maxCopy | uint16 | The maximum number of bytes of returndata to copy to memory. | | \_calldata | bytes | The data to send to the remote contract | #### Return Values | Name | Type | Description | | ---- | ----- | ------------------------------------------------------------------------------- | | \[0] | bool | success and returndata, as `.call()`. Returndata is capped to `_maxCopy` bytes. | | \[1] | bytes | | ## DVNMock ### Executed ```solidity wrap theme={null} event Executed(uint32 vid, address target, bytes callData, uint256 expiration, bytes signatures) ``` ### vid ```solidity wrap theme={null} uint32 vid ``` ### constructor ```solidity wrap theme={null} constructor(uint32 _vid) public ``` ### execute ```solidity wrap theme={null} function execute(struct ExecuteParam[] _params) external ``` ### verify ```solidity wrap theme={null} function verify(bytes _packetHeader, bytes32 _payloadHash, uint64 _confirmations) external ``` ## ExecutorMock ### NativeDropMeta ```solidity wrap theme={null} event NativeDropMeta(uint32 srcEid, bytes32 sender, uint64 nonce, uint32 dstEid, address oapp, uint256 nativeDropGasLimit) ``` ### NativeDropped ```solidity wrap theme={null} event NativeDropped(address receiver, uint256 amount) ``` ### Executed301 ```solidity wrap theme={null} event Executed301(bytes packet, uint256 gasLimit) ``` ### Executed302 ```solidity wrap theme={null} event Executed302(uint32 srcEid, bytes32 sender, uint64 nonce, address receiver, bytes32 guid, bytes message, bytes extraData, uint256 gasLimit) ``` ### dstEid ```solidity wrap theme={null} uint32 dstEid ``` ### constructor ```solidity wrap theme={null} constructor(uint32 _dstEid) public ``` ### nativeDrop ```solidity wrap theme={null} function nativeDrop(struct Origin _origin, uint32 _dstEid, address _oapp, struct IExecutor.NativeDropParams[] _nativeDropParams, uint256 _nativeDropGasLimit) external payable ``` ### nativeDropAndExecute301 ```solidity wrap theme={null} function nativeDropAndExecute301(struct Origin _origin, struct IExecutor.NativeDropParams[] _nativeDropParams, uint256 _nativeDropGasLimit, bytes _packet, uint256 _gasLimit) external payable ``` ### execute301 ```solidity wrap theme={null} function execute301(bytes _packet, uint256 _gasLimit) external ``` ### nativeDropAndExecute302 ```solidity wrap theme={null} function nativeDropAndExecute302(struct IExecutor.NativeDropParams[] _nativeDropParams, uint256 _nativeDropGasLimit, struct IExecutor.ExecutionParams _executionParams) external payable ``` ### \_nativeDrop ```solidity wrap theme={null} function _nativeDrop(struct Origin _origin, uint32 _dstEid, address _oapp, struct IExecutor.NativeDropParams[] _nativeDropParams, uint256 _nativeDropGasLimit) internal ``` ## LzReceiveParam ```solidity wrap theme={null} struct LzReceiveParam { struct Origin origin; address receiver; bytes32 guid; bytes message; bytes extraData; uint256 gas; uint256 value; } ``` ## NativeDropParam ```solidity wrap theme={null} struct NativeDropParam { address _receiver; uint256 _amount; } ``` ## IReceiveUlnView ### verifiable ```solidity wrap theme={null} function verifiable(bytes _packetHeader, bytes32 _payloadHash) external view returns (enum VerificationState) ``` ## Verification ```solidity wrap theme={null} struct Verification { bool submitted; uint64 confirmations; } ``` ## ReceiveUlnBase *includes the utility functions for checking ULN states and logics* ### hashLookup ```solidity wrap theme={null} mapping(bytes32 => mapping(bytes32 => mapping(address => struct Verification))) hashLookup ``` ### PayloadVerified ```solidity wrap theme={null} event PayloadVerified(address dvn, bytes header, uint256 confirmations, bytes32 proofHash) ``` ### LZ\_ULN\_InvalidPacketHeader ```solidity wrap theme={null} error LZ_ULN_InvalidPacketHeader() ``` ### LZ\_ULN\_InvalidPacketVersion ```solidity wrap theme={null} error LZ_ULN_InvalidPacketVersion() ``` ### LZ\_ULN\_InvalidEid ```solidity wrap theme={null} error LZ_ULN_InvalidEid() ``` ### LZ\_ULN\_Verifying ```solidity wrap theme={null} error LZ_ULN_Verifying() ``` ### verifiable ```solidity wrap theme={null} function verifiable(struct UlnConfig _config, bytes32 _headerHash, bytes32 _payloadHash) external view returns (bool) ``` ### assertHeader ```solidity wrap theme={null} function assertHeader(bytes _packetHeader, uint32 _localEid) external pure ``` ### \_verify ```solidity wrap theme={null} function _verify(bytes _packetHeader, bytes32 _payloadHash, uint64 _confirmations) internal ``` *per DVN signing function* ### \_verified ```solidity wrap theme={null} function _verified(address _dvn, bytes32 _headerHash, bytes32 _payloadHash, uint64 _requiredConfirmation) internal view returns (bool verified) ``` ### \_verifyAndReclaimStorage ```solidity wrap theme={null} function _verifyAndReclaimStorage(struct UlnConfig _config, bytes32 _headerHash, bytes32 _payloadHash) internal ``` ### \_assertHeader ```solidity wrap theme={null} function _assertHeader(bytes _packetHeader, uint32 _localEid) internal pure ``` ### \_checkVerifiable ```solidity wrap theme={null} function _checkVerifiable(struct UlnConfig _config, bytes32 _headerHash, bytes32 _payloadHash) internal view returns (bool) ``` *for verifiable view function checks if this verification is ready to be committed to the endpoint* ## SendUlnBase *includes the utility functions for checking ULN states and logics* ### DVNFeePaid ```solidity wrap theme={null} event DVNFeePaid(address[] requiredDVNs, address[] optionalDVNs, uint256[] fees) ``` ### \_splitUlnOptions ```solidity wrap theme={null} function _splitUlnOptions(bytes _options) internal pure returns (bytes, struct WorkerOptions[]) ``` ### \_payDVNs ```solidity wrap theme={null} function _payDVNs(mapping(address => uint256) _fees, struct Packet _packet, struct WorkerOptions[] _options) internal returns (uint256 totalFee, bytes encodedPacket) ``` \---------- pay and assign jobs ---------- ### \_assignJobs ```solidity wrap theme={null} function _assignJobs(mapping(address => uint256) _fees, struct UlnConfig _ulnConfig, struct ILayerZeroDVN.AssignJobParam _param, bytes dvnOptions) internal returns (uint256 totalFee, uint256[] dvnFees) ``` ### \_quoteDVNs ```solidity wrap theme={null} function _quoteDVNs(address _sender, uint32 _dstEid, struct WorkerOptions[] _options) internal view returns (uint256 totalFee) ``` \---------- quote ---------- ### \_getFees ```solidity wrap theme={null} function _getFees(struct UlnConfig _config, uint32 _dstEid, address _sender, bytes[] _optionsArray, uint8[] _dvnIds) internal view returns (uint256 totalFee) ``` ## UlnConfig ```solidity wrap theme={null} struct UlnConfig { uint64 confirmations; uint8 requiredDVNCount; uint8 optionalDVNCount; uint8 optionalDVNThreshold; address[] requiredDVNs; address[] optionalDVNs; } ``` ## SetDefaultUlnConfigParam ```solidity wrap theme={null} struct SetDefaultUlnConfigParam { uint32 eid; struct UlnConfig config; } ``` ## UlnBase *includes the utility functions for checking ULN states and logics* ### DEFAULT ```solidity wrap theme={null} uint8 DEFAULT ``` ### NIL\_DVN\_COUNT ```solidity wrap theme={null} uint8 NIL_DVN_COUNT ``` ### NIL\_CONFIRMATIONS ```solidity wrap theme={null} uint64 NIL_CONFIRMATIONS ``` ### ulnConfigs ```solidity wrap theme={null} mapping(address => mapping(uint32 => struct UlnConfig)) ulnConfigs ``` ### LZ\_ULN\_Unsorted ```solidity wrap theme={null} error LZ_ULN_Unsorted() ``` ### LZ\_ULN\_InvalidRequiredDVNCount ```solidity wrap theme={null} error LZ_ULN_InvalidRequiredDVNCount() ``` ### LZ\_ULN\_InvalidOptionalDVNCount ```solidity wrap theme={null} error LZ_ULN_InvalidOptionalDVNCount() ``` ### LZ\_ULN\_AtLeastOneDVN ```solidity wrap theme={null} error LZ_ULN_AtLeastOneDVN() ``` ### LZ\_ULN\_InvalidOptionalDVNThreshold ```solidity wrap theme={null} error LZ_ULN_InvalidOptionalDVNThreshold() ``` ### LZ\_ULN\_InvalidConfirmations ```solidity wrap theme={null} error LZ_ULN_InvalidConfirmations() ``` ### LZ\_ULN\_UnsupportedEid ```solidity wrap theme={null} error LZ_ULN_UnsupportedEid(uint32 eid) ``` ### DefaultUlnConfigsSet ```solidity wrap theme={null} event DefaultUlnConfigsSet(struct SetDefaultUlnConfigParam[] params) ``` ### UlnConfigSet ```solidity wrap theme={null} event UlnConfigSet(address oapp, uint32 eid, struct UlnConfig config) ``` ### setDefaultUlnConfigs ```solidity wrap theme={null} function setDefaultUlnConfigs(struct SetDefaultUlnConfigParam[] _params) external ``` \_about the DEFAULT ULN config 1. its values are all LITERAL (e.g. 0 is 0). whereas in the oapp ULN config, 0 (default value) points to the default ULN config this design enables the oapp to point to DEFAULT config without explicitly setting the config 2. its configuration is more restrictive than the oapp ULN config that a) it must not use NIL value, where NIL is used only by oapps to indicate the LITERAL 0 b) it must have at least one DVN\_ ### getUlnConfig ```solidity wrap theme={null} function getUlnConfig(address _oapp, uint32 _remoteEid) public view returns (struct UlnConfig rtnConfig) ``` ### getAppUlnConfig ```solidity wrap theme={null} function getAppUlnConfig(address _oapp, uint32 _remoteEid) external view returns (struct UlnConfig) ``` *Get the uln config without the default config for the given remoteEid.* ### \_setUlnConfig ```solidity wrap theme={null} function _setUlnConfig(uint32 _remoteEid, address _oapp, struct UlnConfig _param) internal ``` ### \_isSupportedEid ```solidity wrap theme={null} function _isSupportedEid(uint32 _remoteEid) internal view returns (bool) ``` *a supported Eid must have a valid default uln config, which has at least one dvn* ### \_assertSupportedEid ```solidity wrap theme={null} function _assertSupportedEid(uint32 _remoteEid) internal view ``` ## ExecuteParam ```solidity wrap theme={null} struct ExecuteParam { uint32 vid; address target; bytes callData; uint256 expiration; bytes signatures; } ``` ## ISendLibBase ### fees ```solidity wrap theme={null} function fees(address _worker) external view returns (uint256) ``` ## IReceiveUln ### verify ```solidity wrap theme={null} function verify(bytes _packetHeader, bytes32 _payloadHash, uint64 _confirmations) external ``` ## ReceiveLibParam ```solidity wrap theme={null} struct ReceiveLibParam { address sendLib; uint32 dstEid; bytes32 receiveLib; } ``` ## DVNAdapterBase base contract for DVN adapters \_limitations: * doesn't accept alt token * doesn't respect block confirmations\_ ### DVNAdapter\_InsufficientBalance ```solidity wrap theme={null} error DVNAdapter_InsufficientBalance(uint256 actual, uint256 requested) ``` ### DVNAdapter\_NotImplemented ```solidity wrap theme={null} error DVNAdapter_NotImplemented() ``` ### DVNAdapter\_MissingRecieveLib ```solidity wrap theme={null} error DVNAdapter_MissingRecieveLib(address sendLib, uint32 dstEid) ``` ### ReceiveLibsSet ```solidity wrap theme={null} event ReceiveLibsSet(struct ReceiveLibParam[] params) ``` ### MAX\_CONFIRMATIONS ```solidity wrap theme={null} uint64 MAX_CONFIRMATIONS ``` *on change of application config, dvn adapters will not perform any additional verification to avoid messages from being stuck, all verifications from adapters will be done with the maximum possible confirmations* ### receiveLibs ```solidity wrap theme={null} mapping(address => mapping(uint32 => bytes32)) receiveLibs ``` *receive lib to call verify() on at destination* ### constructor ```solidity wrap theme={null} constructor(address _roleAdmin, address[] _admins, uint16 _defaultMultiplierBps) internal ``` ### setReceiveLibs ```solidity wrap theme={null} function setReceiveLibs(struct ReceiveLibParam[] _params) external ``` sets receive lib for destination chains *DEFAULT\_ADMIN\_ROLE can set MESSAGE\_LIB\_ROLE for sendLibs and use below function to set receiveLibs* ### \_getAndAssertReceiveLib ```solidity wrap theme={null} function _getAndAssertReceiveLib(address _sendLib, uint32 _dstEid) internal view returns (bytes32 lib) ``` ### \_encode ```solidity wrap theme={null} function _encode(bytes32 _receiveLib, bytes _packetHeader, bytes32 _payloadHash) internal pure returns (bytes) ``` ### \_encodeEmpty ```solidity wrap theme={null} function _encodeEmpty() internal pure returns (bytes) ``` ### \_decodeAndVerify ```solidity wrap theme={null} function _decodeAndVerify(uint32 _srcEid, bytes _payload) internal ``` ### \_withdrawFeeFromSendLib ```solidity wrap theme={null} function _withdrawFeeFromSendLib(address _sendLib, address _to) internal ``` ### \_assertBalanceAndWithdrawFee ```solidity wrap theme={null} function _assertBalanceAndWithdrawFee(address _sendLib, uint256 _messageFee) internal ``` ### receive ```solidity wrap theme={null} receive() external payable ``` *to receive refund* ## DVNAdapterMessageCodec ### DVNAdapter\_InvalidMessageSize ```solidity wrap theme={null} error DVNAdapter_InvalidMessageSize() ``` ### PACKET\_HEADER\_SIZE ```solidity wrap theme={null} uint256 PACKET_HEADER_SIZE ``` ### MESSAGE\_SIZE ```solidity wrap theme={null} uint256 MESSAGE_SIZE ``` ### encode ```solidity wrap theme={null} function encode(bytes32 _receiveLib, bytes _packetHeader, bytes32 _payloadHash) internal pure returns (bytes payload) ``` ### decode ```solidity wrap theme={null} function decode(bytes _message) internal pure returns (address receiveLib, bytes packetHeader, bytes32 payloadHash) ``` ### srcEid ```solidity wrap theme={null} function srcEid(bytes _message) internal pure returns (uint32) ``` ## IDVN ### DstConfigParam ```solidity wrap theme={null} struct DstConfigParam { uint32 dstEid; uint64 gas; uint16 multiplierBps; uint128 floorMarginUSD; } ``` ### DstConfig ```solidity wrap theme={null} struct DstConfig { uint64 gas; uint16 multiplierBps; uint128 floorMarginUSD; } ``` ### SetDstConfig ```solidity wrap theme={null} event SetDstConfig(struct IDVN.DstConfigParam[] params) ``` ### dstConfig ```solidity wrap theme={null} function dstConfig(uint32 _dstEid) external view returns (uint64, uint16, uint128) ``` ## IDVNFeeLib ### FeeParams ```solidity wrap theme={null} struct FeeParams { address priceFeed; uint32 dstEid; uint64 confirmations; address sender; uint64 quorum; uint16 defaultMultiplierBps; } ``` ### DVN\_UnsupportedOptionType ```solidity wrap theme={null} error DVN_UnsupportedOptionType(uint8 optionType) ``` ### DVN\_EidNotSupported ```solidity wrap theme={null} error DVN_EidNotSupported(uint32 eid) ``` ### getFeeOnSend ```solidity wrap theme={null} function getFeeOnSend(struct IDVNFeeLib.FeeParams _params, struct IDVN.DstConfig _dstConfig, bytes _options) external payable returns (uint256 fee) ``` ### getFee ```solidity wrap theme={null} function getFee(struct IDVNFeeLib.FeeParams _params, struct IDVN.DstConfig _dstConfig, bytes _options) external view returns (uint256 fee) ``` ## ILayerZeroDVN ### AssignJobParam ```solidity wrap theme={null} struct AssignJobParam { uint32 dstEid; bytes packetHeader; bytes32 payloadHash; uint64 confirmations; address sender; } ``` ### assignJob ```solidity wrap theme={null} function assignJob(struct ILayerZeroDVN.AssignJobParam _param, bytes _options) external payable returns (uint256 fee) ``` ### getFee ```solidity wrap theme={null} function getFee(uint32 _dstEid, uint64 _confirmations, address _sender, bytes _options) external view returns (uint256 fee) ``` ## IReceiveUlnE2 *should be implemented by the ReceiveUln302 contract and future ReceiveUln contracts on EndpointV2* ### verify ```solidity wrap theme={null} function verify(bytes _packetHeader, bytes32 _payloadHash, uint64 _confirmations) external ``` for each dvn to verify the payload *this function signature 0x0223536e* ### commitVerification ```solidity wrap theme={null} function commitVerification(bytes _packetHeader, bytes32 _payloadHash) external ``` verify the payload at endpoint, will check if all DVNs verified ## DVNOptions ### WORKER\_ID ```solidity wrap theme={null} uint8 WORKER_ID ``` ### OPTION\_TYPE\_PRECRIME ```solidity wrap theme={null} uint8 OPTION_TYPE_PRECRIME ``` ### DVN\_InvalidDVNIdx ```solidity wrap theme={null} error DVN_InvalidDVNIdx() ``` ### DVN\_InvalidDVNOptions ```solidity wrap theme={null} error DVN_InvalidDVNOptions(uint256 cursor) ``` ### groupDVNOptionsByIdx ```solidity wrap theme={null} function groupDVNOptionsByIdx(bytes _options) internal pure returns (bytes[] dvnOptions, uint8[] dvnIndices) ``` *group dvn options by its idx* #### Parameters | Name | Type | Description | | --------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | \_options | bytes | \[dvn\_id]\[dvn\_option]\[dvn\_id]\[dvn\_option]... dvn\_option = \[option\_size]\[dvn\_idx]\[option\_type]\[option] option\_size = len(dvn\_idx) + len(option\_type) + len(option) dvn\_id: uint8, dvn\_idx: uint8, option\_size: uint16, option\_type: uint8, option: bytes | #### Return Values | Name | Type | Description | | ---------- | -------- | ------------------------------------------------------------- | | dvnOptions | bytes\[] | the grouped options, still share the same format of \_options | | dvnIndices | uint8\[] | the dvn indices | ### \_insertDVNOptions ```solidity wrap theme={null} function _insertDVNOptions(bytes[] _dvnOptions, uint8[] _dvnIndices, uint8 _dvnIdx, bytes _newOptions) internal pure ``` ### getNumDVNs ```solidity wrap theme={null} function getNumDVNs(bytes _options) internal pure returns (uint8 numDVNs) ``` *get the number of unique dvns* #### Parameters | Name | Type | Description | | --------- | ----- | ---------------------------------------------- | | \_options | bytes | the format is the same as groupDVNOptionsByIdx | ### nextDVNOption ```solidity wrap theme={null} function nextDVNOption(bytes _options, uint256 _cursor) internal pure returns (uint8 optionType, bytes option, uint256 cursor) ``` *decode the next dvn option from \_options starting from the specified cursor* #### Parameters | Name | Type | Description | | --------- | ------- | ---------------------------------------------- | | \_options | bytes | the format is the same as groupDVNOptionsByIdx | | \_cursor | uint256 | the cursor to start decoding | #### Return Values | Name | Type | Description | | ---------- | ------- | -------------------------------------------- | | optionType | uint8 | the type of the option | | option | bytes | the option | | cursor | uint256 | the cursor to start decoding the next option | ## UlnOptions ### TYPE\_1 ```solidity wrap theme={null} uint16 TYPE_1 ``` ### TYPE\_2 ```solidity wrap theme={null} uint16 TYPE_2 ``` ### TYPE\_3 ```solidity wrap theme={null} uint16 TYPE_3 ``` ### LZ\_ULN\_InvalidWorkerOptions ```solidity wrap theme={null} error LZ_ULN_InvalidWorkerOptions(uint256 cursor) ``` ### LZ\_ULN\_InvalidWorkerId ```solidity wrap theme={null} error LZ_ULN_InvalidWorkerId(uint8 workerId) ``` ### LZ\_ULN\_InvalidLegacyType1Option ```solidity wrap theme={null} error LZ_ULN_InvalidLegacyType1Option() ``` ### LZ\_ULN\_InvalidLegacyType2Option ```solidity wrap theme={null} error LZ_ULN_InvalidLegacyType2Option() ``` ### LZ\_ULN\_UnsupportedOptionType ```solidity wrap theme={null} error LZ_ULN_UnsupportedOptionType(uint16 optionType) ``` ### decode ```solidity wrap theme={null} function decode(bytes _options) internal pure returns (bytes executorOptions, bytes dvnOptions) ``` *decode the options into executorOptions and dvnOptions* #### Parameters | Name | Type | Description | | --------- | ----- | ------------------------------------------------------------------------ | | \_options | bytes | the options can be either legacy options (type 1 or 2) or type 3 options | #### Return Values | Name | Type | Description | | --------------- | ----- | ------------------------------------------------------------- | | executorOptions | bytes | the executor options, share the same format of type 3 options | | dvnOptions | bytes | the dvn options, share the same format of type 3 options | ### decodeLegacyOptions ```solidity wrap theme={null} function decodeLegacyOptions(uint16 _optionType, bytes _options) internal pure returns (bytes executorOptions) ``` *decode the legacy options (type 1 or 2) into executorOptions* #### Parameters | Name | Type | Description | | ------------ | ------ | ------------------------------------------------------------------------ | | \_optionType | uint16 | the legacy option type | | \_options | bytes | the legacy options, which still has the option type in the first 2 bytes | #### Return Values | Name | Type | Description | | --------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | executorOptions | bytes | the executor options, share the same format of type 3 options Data format: legacy type 1: \[extraGas] legacy type 2: \[extraGas]\[dstNativeAmt]\[dstNativeAddress] extraGas: uint256, dstNativeAmt: uint256, dstNativeAddress: bytes | ## AddressSizeConfig ### addressSizes ```solidity wrap theme={null} mapping(uint32 => uint256) addressSizes ``` ### AddressSizeSet ```solidity wrap theme={null} event AddressSizeSet(uint16 eid, uint256 size) ``` ### AddressSizeConfig\_InvalidAddressSize ```solidity wrap theme={null} error AddressSizeConfig_InvalidAddressSize() ``` ### AddressSizeConfig\_AddressSizeAlreadySet ```solidity wrap theme={null} error AddressSizeConfig_AddressSizeAlreadySet() ``` ### setAddressSize ```solidity wrap theme={null} function setAddressSize(uint16 _eid, uint256 _size) external ``` ## ILayerZeroReceiveLibrary ### setConfig ```solidity wrap theme={null} function setConfig(uint16 _chainId, address _userApplication, uint256 _configType, bytes _config) external ``` ### getConfig ```solidity wrap theme={null} function getConfig(uint16 _chainId, address _userApplication, uint256 _configType) external view returns (bytes) ``` ## SetDefaultExecutorParam ```solidity wrap theme={null} struct SetDefaultExecutorParam { uint32 eid; address executor; } ``` ## ReceiveLibBaseE1 *receive-side message library base contract on endpoint v1. design: 1/ it provides an internal execute function that calls the endpoint. It enforces the path definition on V1. 2/ it provides interfaces to configure executors that is whitelisted to execute the msg to prevent grieving* ### executors ```solidity wrap theme={null} mapping(address => mapping(uint32 => address)) executors ``` ### defaultExecutors ```solidity wrap theme={null} mapping(uint32 => address) defaultExecutors ``` ### PacketDelivered ```solidity wrap theme={null} event PacketDelivered(struct Origin origin, address receiver) ``` ### InvalidDst ```solidity wrap theme={null} event InvalidDst(uint16 srcChainId, bytes32 srcAddress, address dstAddress, uint64 nonce, bytes32 payloadHash) ``` ### DefaultExecutorsSet ```solidity wrap theme={null} event DefaultExecutorsSet(struct SetDefaultExecutorParam[] params) ``` ### ExecutorSet ```solidity wrap theme={null} event ExecutorSet(address oapp, uint32 eid, address executor) ``` ### LZ\_MessageLib\_InvalidExecutor ```solidity wrap theme={null} error LZ_MessageLib_InvalidExecutor() ``` ### LZ\_MessageLib\_OnlyExecutor ```solidity wrap theme={null} error LZ_MessageLib_OnlyExecutor() ``` ### constructor ```solidity wrap theme={null} constructor(address _endpoint, uint32 _localEid) internal ``` ### setDefaultExecutors ```solidity wrap theme={null} function setDefaultExecutors(struct SetDefaultExecutorParam[] _params) external ``` ### getExecutor ```solidity wrap theme={null} function getExecutor(address _oapp, uint32 _remoteEid) public view returns (address) ``` ### \_setExecutor ```solidity wrap theme={null} function _setExecutor(uint32 _remoteEid, address _oapp, address _executor) internal ``` ### \_execute ```solidity wrap theme={null} function _execute(uint16 _srcEid, bytes32 _sender, address _receiver, uint64 _nonce, bytes _message, uint256 _gasLimit) internal ``` *this function change pack the path as required for EndpointV1* ## ReceiveUln301 *ULN301 will be deployed on EndpointV1 and is for backward compatibility with ULN302 on EndpointV2. 301 can talk to both 301 and 302 This is a gluing contract. It simply parses the requests and forward to the super.impl() accordingly. In this case, it combines the logic of ReceiveUlnBase and ReceiveLibBaseE1* ### CONFIG\_TYPE\_EXECUTOR ```solidity wrap theme={null} uint256 CONFIG_TYPE_EXECUTOR ``` ### CONFIG\_TYPE\_ULN ```solidity wrap theme={null} uint256 CONFIG_TYPE_ULN ``` ### LZ\_ULN\_InvalidConfigType ```solidity wrap theme={null} error LZ_ULN_InvalidConfigType(uint256 configType) ``` ### constructor ```solidity wrap theme={null} constructor(address _endpoint, uint32 _localEid) public ``` ### setConfig ```solidity wrap theme={null} function setConfig(uint16 _eid, address _oapp, uint256 _configType, bytes _config) external ``` ### commitVerification ```solidity wrap theme={null} function commitVerification(bytes _packet, uint256 _gasLimit) external ``` *in 301, this is equivalent to execution as in Endpoint V2 dont need to check endpoint verifiable here to save gas, as it will reverts if not verifiable.* ### verify ```solidity wrap theme={null} function verify(bytes _packetHeader, bytes32 _payloadHash, uint64 _confirmations) external ``` ### getConfig ```solidity wrap theme={null} function getConfig(uint16 _eid, address _oapp, uint256 _configType) external view returns (bytes) ``` ### version ```solidity wrap theme={null} function version() external pure returns (uint64 major, uint8 minor, uint8 endpointVersion) ``` ## VerificationState ```solidity wrap theme={null} enum VerificationState { Verifying, Verifiable, Verified } ``` ## IReceiveUln301 ### assertHeader ```solidity wrap theme={null} function assertHeader(bytes _packetHeader, uint32 _localEid) external pure ``` ### addressSizes ```solidity wrap theme={null} function addressSizes(uint32 _dstEid) external view returns (uint256) ``` ### endpoint ```solidity wrap theme={null} function endpoint() external view returns (address) ``` ### verifiable ```solidity wrap theme={null} function verifiable(struct UlnConfig _config, bytes32 _headerHash, bytes32 _payloadHash) external view returns (bool) ``` ### getUlnConfig ```solidity wrap theme={null} function getUlnConfig(address _oapp, uint32 _remoteEid) external view returns (struct UlnConfig rtnConfig) ``` ## ReceiveUln301View ### endpoint ```solidity wrap theme={null} contract ILayerZeroEndpoint endpoint ``` ### receiveUln301 ```solidity wrap theme={null} contract IReceiveUln301 receiveUln301 ``` ### localEid ```solidity wrap theme={null} uint32 localEid ``` ### initialize ```solidity wrap theme={null} function initialize(address _endpoint, uint32 _localEid, address _receiveUln301) external ``` ### executable ```solidity wrap theme={null} function executable(bytes _packetHeader, bytes32 _payloadHash) public view returns (enum ExecutionState) ``` ### verifiable ```solidity wrap theme={null} function verifiable(bytes _packetHeader, bytes32 _payloadHash) external view returns (enum VerificationState) ``` *keeping the same interface as 302 a verifiable message requires it to be ULN verifiable only, excluding the endpoint verifiable check* ## SendLibBaseE1 *send-side message library base contract on endpoint v1. design: 1/ it enforces the path definition on V1 and interacts with the nonce contract 2/ quote: first executor, then verifier (e.g. DVNs), then treasury 3/ send: first executor, then verifier (e.g. DVNs), then treasury. the treasury pay much be DoS-proof* ### nonceContract ```solidity wrap theme={null} contract INonceContract nonceContract ``` ### treasuryFeeHandler ```solidity wrap theme={null} contract ITreasuryFeeHandler treasuryFeeHandler ``` ### lzToken ```solidity wrap theme={null} address lzToken ``` ### PacketSent ```solidity wrap theme={null} event PacketSent(bytes encodedPayload, bytes options, uint256 nativeFee, uint256 lzTokenFee) ``` ### NativeFeeWithdrawn ```solidity wrap theme={null} event NativeFeeWithdrawn(address user, address receiver, uint256 amount) ``` ### LzTokenSet ```solidity wrap theme={null} event LzTokenSet(address token) ``` ### constructor ```solidity wrap theme={null} constructor(address _endpoint, uint256 _treasuryGasLimit, uint256 _treasuryNativeFeeCap, address _nonceContract, uint32 _localEid, address _treasuryFeeHandler) internal ``` ### send ```solidity wrap theme={null} function send(address _sender, uint64, uint16 _dstEid, bytes _path, bytes _message, address payable _refundAddress, address _lzTokenPaymentAddress, bytes _options) external payable ``` *the abstract process for send() is: 1/ pay workers, which includes the executor and the validation workers 2/ pay treasury 3/ in EndpointV1, here we handle the fees and refunds* ### setLzToken ```solidity wrap theme={null} function setLzToken(address _lzToken) external ``` ### setTreasury ```solidity wrap theme={null} function setTreasury(address _treasury) external ``` ### withdrawFee ```solidity wrap theme={null} function withdrawFee(address _to, uint256 _amount) external ``` ### estimateFees ```solidity wrap theme={null} function estimateFees(uint16 _dstEid, address _sender, bytes _message, bool _payInLzToken, bytes _options) external view returns (uint256 nativeFee, uint256 lzTokenFee) ``` ### \_assertPath ```solidity wrap theme={null} function _assertPath(address _sender, bytes _path, uint256 remoteAddressSize) internal pure ``` *path = remoteAddress + localAddress.* ### \_payLzTokenFee ```solidity wrap theme={null} function _payLzTokenFee(address _sender, uint256 _lzTokenFee) internal ``` ### \_outbound ```solidity wrap theme={null} function _outbound(address _sender, uint16 _dstEid, bytes _path, bytes _message) internal returns (struct Packet packet) ``` \_outbound does three things 1. asserts path 2. increments the nonce 3. assemble packet\_ #### Return Values | Name | Type | Description | | ------ | ------------- | --------------------- | | packet | struct Packet | to be sent to workers | ### \_payWorkers ```solidity wrap theme={null} function _payWorkers(address _sender, uint16 _dstEid, bytes _path, bytes _message, bytes _options) internal returns (bytes encodedPacket, uint256 totalNativeFee) ``` 1/ handle executor 2/ handle other workers ### \_payVerifier ```solidity wrap theme={null} function _payVerifier(struct Packet _packet, struct WorkerOptions[] _options) internal virtual returns (uint256 otherWorkerFees, bytes encodedPacket) ``` ## SendUln301 *ULN301 will be deployed on EndpointV1 and is for backward compatibility with ULN302 on EndpointV2. 301 can talk to both 301 and 302 This is a gluing contract. It simply parses the requests and forward to the super.impl() accordingly. In this case, it combines the logic of SendUlnBase and SendLibBaseE1* ### CONFIG\_TYPE\_EXECUTOR ```solidity wrap theme={null} uint256 CONFIG_TYPE_EXECUTOR ``` ### CONFIG\_TYPE\_ULN ```solidity wrap theme={null} uint256 CONFIG_TYPE_ULN ``` ### LZ\_ULN\_InvalidConfigType ```solidity wrap theme={null} error LZ_ULN_InvalidConfigType(uint256 configType) ``` ### constructor ```solidity wrap theme={null} constructor(address _endpoint, uint256 _treasuryGasLimit, uint256 _treasuryGasForFeeCap, address _nonceContract, uint32 _localEid, address _treasuryFeeHandler) public ``` ### setConfig ```solidity wrap theme={null} function setConfig(uint16 _eid, address _oapp, uint256 _configType, bytes _config) external ``` ### getConfig ```solidity wrap theme={null} function getConfig(uint16 _eid, address _oapp, uint256 _configType) external view returns (bytes) ``` ### version ```solidity wrap theme={null} function version() external pure returns (uint64 major, uint8 minor, uint8 endpointVersion) ``` ### isSupportedEid ```solidity wrap theme={null} function isSupportedEid(uint32 _eid) external view returns (bool) ``` ### \_quoteVerifier ```solidity wrap theme={null} function _quoteVerifier(address _sender, uint32 _dstEid, struct WorkerOptions[] _options) internal view returns (uint256) ``` ### \_payVerifier ```solidity wrap theme={null} function _payVerifier(struct Packet _packet, struct WorkerOptions[] _options) internal virtual returns (uint256 otherWorkerFees, bytes encodedPacket) ``` ### \_splitOptions ```solidity wrap theme={null} function _splitOptions(bytes _options) internal pure returns (bytes, struct WorkerOptions[]) ``` *this function will split the options into executorOptions and validationOptions* ## TreasuryFeeHandler ### endpoint ```solidity wrap theme={null} contract ILayerZeroEndpoint endpoint ``` ### LZ\_TreasuryFeeHandler\_OnlySendLibrary ```solidity wrap theme={null} error LZ_TreasuryFeeHandler_OnlySendLibrary() ``` ### LZ\_TreasuryFeeHandler\_OnlyOnSending ```solidity wrap theme={null} error LZ_TreasuryFeeHandler_OnlyOnSending() ``` ### LZ\_TreasuryFeeHandler\_InvalidAmount ```solidity wrap theme={null} error LZ_TreasuryFeeHandler_InvalidAmount(uint256 required, uint256 supplied) ``` ### constructor ```solidity wrap theme={null} constructor(address _endpoint) public ``` ### payFee ```solidity wrap theme={null} function payFee(address _lzToken, address _sender, uint256 _required, uint256 _supplied, address _treasury) external ``` ## IMessageLibE1 extends ILayerZeroMessagingLibrary instead of ILayerZeroMessagingLibraryV2 for reducing the contract size ### LZ\_MessageLib\_InvalidPath ```solidity wrap theme={null} error LZ_MessageLib_InvalidPath() ``` ### LZ\_MessageLib\_InvalidSender ```solidity wrap theme={null} error LZ_MessageLib_InvalidSender() ``` ### LZ\_MessageLib\_InsufficientMsgValue ```solidity wrap theme={null} error LZ_MessageLib_InsufficientMsgValue() ``` ### LZ\_MessageLib\_LzTokenPaymentAddressMustBeSender ```solidity wrap theme={null} error LZ_MessageLib_LzTokenPaymentAddressMustBeSender() ``` ### setLzToken ```solidity wrap theme={null} function setLzToken(address _lzToken) external ``` ### setTreasury ```solidity wrap theme={null} function setTreasury(address _treasury) external ``` ### withdrawFee ```solidity wrap theme={null} function withdrawFee(address _to, uint256 _amount) external ``` ### version ```solidity wrap theme={null} function version() external view returns (uint64 major, uint8 minor, uint8 endpointVersion) ``` ## INonceContract ### increment ```solidity wrap theme={null} function increment(uint16 _chainId, address _ua, bytes _path) external returns (uint64) ``` ## ITreasuryFeeHandler ### payFee ```solidity wrap theme={null} function payFee(address _lzToken, address _sender, uint256 _required, uint256 _supplied, address _treasury) external ``` ## IUltraLightNode301 ### commitVerification ```solidity wrap theme={null} function commitVerification(bytes _packet, uint256 _gasLimit) external ``` ## NonceContractMock ### OnlySendLibrary ```solidity wrap theme={null} error OnlySendLibrary() ``` ### endpoint ```solidity wrap theme={null} contract ILayerZeroEndpoint endpoint ``` ### outboundNonce ```solidity wrap theme={null} mapping(uint16 => mapping(bytes => uint64)) outboundNonce ``` ### constructor ```solidity wrap theme={null} constructor(address _endpoint) public ``` ### increment ```solidity wrap theme={null} function increment(uint16 _chainId, address _ua, bytes _path) external returns (uint64) ``` ## ReceiveUln302 *This is a gluing contract. It simply parses the requests and forward to the super.impl() accordingly. In this case, it combines the logic of ReceiveUlnBase and ReceiveLibBaseE2* ### CONFIG\_TYPE\_ULN ```solidity wrap theme={null} uint32 CONFIG_TYPE_ULN ``` *CONFIG\_TYPE\_ULN=2 here to align with SendUln302/ReceiveUln302/ReceiveUln301* ### LZ\_ULN\_InvalidConfigType ```solidity wrap theme={null} error LZ_ULN_InvalidConfigType(uint32 configType) ``` ### constructor ```solidity wrap theme={null} constructor(address _endpoint) public ``` ### supportsInterface ```solidity wrap theme={null} function supportsInterface(bytes4 _interfaceId) public view returns (bool) ``` ### setConfig ```solidity wrap theme={null} function setConfig(address _oapp, struct SetConfigParam[] _params) external ``` ### commitVerification ```solidity wrap theme={null} function commitVerification(bytes _packetHeader, bytes32 _payloadHash) external ``` *dont need to check endpoint verifiable here to save gas, as it will reverts if not verifiable.* ### verify ```solidity wrap theme={null} function verify(bytes _packetHeader, bytes32 _payloadHash, uint64 _confirmations) external ``` *for dvn to verify the payload* ### getConfig ```solidity wrap theme={null} function getConfig(uint32 _eid, address _oapp, uint32 _configType) external view returns (bytes) ``` ### isSupportedEid ```solidity wrap theme={null} function isSupportedEid(uint32 _eid) external view returns (bool) ``` ### version ```solidity wrap theme={null} function version() external pure returns (uint64 major, uint8 minor, uint8 endpointVersion) ``` ## VerificationState ```solidity wrap theme={null} enum VerificationState { Verifying, Verifiable, Verified, NotInitializable } ``` ## IReceiveUln302 ### assertHeader ```solidity wrap theme={null} function assertHeader(bytes _packetHeader, uint32 _localEid) external pure ``` ### verifiable ```solidity wrap theme={null} function verifiable(struct UlnConfig _config, bytes32 _headerHash, bytes32 _payloadHash) external view returns (bool) ``` ### getUlnConfig ```solidity wrap theme={null} function getUlnConfig(address _oapp, uint32 _remoteEid) external view returns (struct UlnConfig rtnConfig) ``` ## ReceiveUln302View ### receiveUln302 ```solidity wrap theme={null} contract IReceiveUln302 receiveUln302 ``` ### localEid ```solidity wrap theme={null} uint32 localEid ``` ### initialize ```solidity wrap theme={null} function initialize(address _endpoint, address _receiveUln302) external ``` ### verifiable ```solidity wrap theme={null} function verifiable(bytes _packetHeader, bytes32 _payloadHash) external view returns (enum VerificationState) ``` *a ULN verifiable requires it to be endpoint verifiable and committable* ### \_endpointVerifiable ```solidity wrap theme={null} function _endpointVerifiable(struct Origin origin, address _receiver, bytes32 _payloadHash) internal view returns (bool) ``` *checks for endpoint verifiable and endpoint has payload hash* ## SendUln302 *This is a gluing contract. It simply parses the requests and forward to the super.impl() accordingly. In this case, it combines the logic of SendUlnBase and SendLibBaseE2* ### CONFIG\_TYPE\_EXECUTOR ```solidity wrap theme={null} uint32 CONFIG_TYPE_EXECUTOR ``` ### CONFIG\_TYPE\_ULN ```solidity wrap theme={null} uint32 CONFIG_TYPE_ULN ``` ### LZ\_ULN\_InvalidConfigType ```solidity wrap theme={null} error LZ_ULN_InvalidConfigType(uint32 configType) ``` ### constructor ```solidity wrap theme={null} constructor(address _endpoint, uint256 _treasuryGasLimit, uint256 _treasuryGasForFeeCap) public ``` ### setConfig ```solidity wrap theme={null} function setConfig(address _oapp, struct SetConfigParam[] _params) external ``` ### getConfig ```solidity wrap theme={null} function getConfig(uint32 _eid, address _oapp, uint32 _configType) external view returns (bytes) ``` ### version ```solidity wrap theme={null} function version() external pure returns (uint64 major, uint8 minor, uint8 endpointVersion) ``` ### isSupportedEid ```solidity wrap theme={null} function isSupportedEid(uint32 _eid) external view returns (bool) ``` ### \_quoteVerifier ```solidity wrap theme={null} function _quoteVerifier(address _sender, uint32 _dstEid, struct WorkerOptions[] _options) internal view returns (uint256) ``` ### \_payVerifier ```solidity wrap theme={null} function _payVerifier(struct Packet _packet, struct WorkerOptions[] _options) internal returns (uint256 otherWorkerFees, bytes encodedPacket) ``` ### \_splitOptions ```solidity wrap theme={null} function _splitOptions(bytes _options) internal pure returns (bytes, struct WorkerOptions[]) ``` *this function will split the options into executorOptions and validationOptions* ## WorkerUpgradeable ### MESSAGE\_LIB\_ROLE ```solidity wrap theme={null} bytes32 MESSAGE_LIB_ROLE ``` ### ALLOWLIST ```solidity wrap theme={null} bytes32 ALLOWLIST ``` ### DENYLIST ```solidity wrap theme={null} bytes32 DENYLIST ``` ### ADMIN\_ROLE ```solidity wrap theme={null} bytes32 ADMIN_ROLE ``` ### workerFeeLib ```solidity wrap theme={null} address workerFeeLib ``` ### allowlistSize ```solidity wrap theme={null} uint64 allowlistSize ``` ### defaultMultiplierBps ```solidity wrap theme={null} uint16 defaultMultiplierBps ``` ### priceFeed ```solidity wrap theme={null} address priceFeed ``` ### supportedOptionTypes ```solidity wrap theme={null} mapping(uint32 => uint8[]) supportedOptionTypes ``` ### \_\_Worker\_init ```solidity wrap theme={null} function __Worker_init(address[] _messageLibs, address _priceFeed, uint16 _defaultMultiplierBps, address _roleAdmin, address[] _admins) internal ``` #### Parameters | Name | Type | Description | | ---------------------- | ---------- | --------------------------------------------------------------------------------- | | \_messageLibs | address\[] | array of message lib addresses that are granted the MESSAGE\_LIB\_ROLE | | \_priceFeed | address | price feed address | | \_defaultMultiplierBps | uint16 | default multiplier for worker fee | | \_roleAdmin | address | address that is granted the DEFAULT\_ADMIN\_ROLE (can grant and revoke all roles) | | \_admins | address\[] | array of admin addresses that are granted the ADMIN\_ROLE | ### \_\_Worker\_init\_unchained ```solidity wrap theme={null} function __Worker_init_unchained(address[] _messageLibs, address _priceFeed, uint16 _defaultMultiplierBps, address _roleAdmin, address[] _admins) internal ``` ### onlyAcl ```solidity wrap theme={null} modifier onlyAcl(address _sender) ``` ### hasAcl ```solidity wrap theme={null} function hasAcl(address _sender) public view returns (bool) ``` \_Access control list using allowlist and denylist 1. if one address is in the denylist -> deny 2. else if address in the allowlist OR allowlist is empty (allows everyone)-> allow 3. else deny\_ #### Parameters | Name | Type | Description | | -------- | ------- | ---------------- | | \_sender | address | address to check | ### setPaused ```solidity wrap theme={null} function setPaused(bool _paused) external ``` *flag to pause execution of workers (if used with whenNotPaused modifier)* #### Parameters | Name | Type | Description | | -------- | ---- | ------------------------------- | | \_paused | bool | true to pause, false to unpause | ### setPriceFeed ```solidity wrap theme={null} function setPriceFeed(address _priceFeed) external ``` #### Parameters | Name | Type | Description | | ----------- | ------- | ------------------ | | \_priceFeed | address | price feed address | ### setWorkerFeeLib ```solidity wrap theme={null} function setWorkerFeeLib(address _workerFeeLib) external ``` #### Parameters | Name | Type | Description | | -------------- | ------- | ---------------------- | | \_workerFeeLib | address | worker fee lib address | ### setDefaultMultiplierBps ```solidity wrap theme={null} function setDefaultMultiplierBps(uint16 _multiplierBps) external ``` #### Parameters | Name | Type | Description | | --------------- | ------ | --------------------------------- | | \_multiplierBps | uint16 | default multiplier for worker fee | ### withdrawFee ```solidity wrap theme={null} function withdrawFee(address _lib, address _to, uint256 _amount) external ``` *supports withdrawing fee from ULN301, ULN302 and more* #### Parameters | Name | Type | Description | | -------- | ------- | -------------------------- | | \_lib | address | message lib address | | \_to | address | address to withdraw fee to | | \_amount | uint256 | amount to withdraw | ### withdrawToken ```solidity wrap theme={null} function withdrawToken(address _token, address _to, uint256 _amount) external ``` *supports withdrawing token from the contract* #### Parameters | Name | Type | Description | | -------- | ------- | ---------------------------- | | \_token | address | token address | | \_to | address | address to withdraw token to | | \_amount | uint256 | amount to withdraw | ### setSupportedOptionTypes ```solidity wrap theme={null} function setSupportedOptionTypes(uint32 _eid, uint8[] _optionTypes) external ``` ### getSupportedOptionTypes ```solidity wrap theme={null} function getSupportedOptionTypes(uint32 _eid) external view returns (uint8[]) ``` ### \_grantRole ```solidity wrap theme={null} function _grantRole(bytes32 _role, address _account) internal ``` *overrides AccessControl to allow for counting of allowlistSize* #### Parameters | Name | Type | Description | | --------- | ------- | ------------------------ | | \_role | bytes32 | role to grant | | \_account | address | address to grant role to | ### \_revokeRole ```solidity wrap theme={null} function _revokeRole(bytes32 _role, address _account) internal ``` *overrides AccessControl to allow for counting of allowlistSize* #### Parameters | Name | Type | Description | | --------- | ------- | --------------------------- | | \_role | bytes32 | role to revoke | | \_account | address | address to revoke role from | ### renounceRole ```solidity wrap theme={null} function renounceRole(bytes32, address) public pure ``` *overrides AccessControl to disable renouncing of roles* # Best Practices for Contract Ownership Source: https://docs.layerzero.network/v2/developers/evm/technical-reference/transfer-ownership Learn the Owner and Delegate roles on LayerZero OApp and OFT contracts, why they usually share one address, and how to transfer both to a multisig safely. Our OApp and OFT contract standards inherit the [OpenZeppelin `Ownable` standard](https://docs.openzeppelin.com/contracts/5.x/access-control) by default, which gives deployed contracts flexible, secure administration. But these contracts expose **two** distinct control roles, the **Owner** and the **Delegate**, and decisions about transferring or renouncing them must be made carefully. This page explains what each role controls, why they usually share one address, and how to hand both to a multisig without bricking your configuration. ## Contract Ownership Pattern When you deploy a LayerZero contract, the trailing constructor argument seeds **both** roles at once: ```solidity theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol"; contract MyOFT is OFT { constructor( string memory _name, string memory _symbol, address _lzEndpoint, address _delegate ) OFT(_name, _symbol, _lzEndpoint, _delegate) // registers _delegate as the Endpoint delegate Ownable(_delegate) // sets _delegate as the contract owner {} } ``` The single `_delegate` value is passed to two base constructors: * `OFT(..., _delegate)` registers `_delegate` as the contract's **Delegate** inside the LayerZero Endpoint. * `Ownable(_delegate)` sets `_delegate` as the contract **Owner**. So at deploy time the Owner and the Delegate are the **same address**. This matches the constructor in our canonical OFT example, [`examples/oft/contracts/MyOFT.sol`](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft/contracts/MyOFT.sol) (`OFT` already inherits `Ownable`, so `MyOFT` only needs to list `OFT` and pass `Ownable(_delegate)` in its constructor). Some quickstarts name this argument `_owner` instead of `_delegate` (see the [OFT quickstart](/v2/developers/evm/oft/quickstart) and [OApp overview](/v2/developers/evm/oapp/overview)). It is the same mechanic with a different label. The Delegate is stored and enforced by the Endpoint, in its per-OApp `delegates` mapping, not on your contract. Calling `setDelegate()` on your OApp writes that value on the Endpoint. See [`setDelegate` in the Endpoint V2 API](/v2/developers/evm/technical-reference/api#setdelegate). ## Understanding Owner vs Delegate The two roles control different layers of your application: | Role | Lives in | Controls | Key functions | | ------------ | ------------------------------------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Owner** | Your OApp contract (OpenZeppelin `Ownable`) | Application-level policy | `setPeer()`, `setEnforcedOptions()`, `setDelegate()` | | **Delegate** | The LayerZero Endpoint (set via `setDelegate()`) | Protocol and security config | `setConfig()` (DVNs, Executor, confirmations), `setSendLibrary()`, `setReceiveLibrary()`, plus message recovery `skip()` / `nilify()` / `burn()` / `clear()` | The Owner is the only role that can change the Delegate; the Delegate cannot change the Owner. For the full per-role permission table, see [Security and roles](/v2/concepts/technical-reference/oapp-reference#security-and-roles) and the [Delegate glossary entry](/v2/concepts/glossary#delegate). **RBAC variant:** Some OApps replace `Ownable` with role-based access control, for example the [Stablecoin OFT](/v2/developers/evm/stablecoin-oft/rbac-reference), where `DEFAULT_ADMIN_ROLE` replaces the single owner and `setDelegate()` is disabled. The guidance on this page applies to standard `Ownable` OApp and OFT contracts; see the [RBAC reference](/v2/developers/evm/stablecoin-oft/rbac-reference) for that model. ### Why They Should Match Keep the Owner and the Delegate set to the **same address** unless you have a specific reason to split them: * Our wiring tooling (`lz:oapp:wire`) calls both owner-gated functions (`setPeer`, `setEnforcedOptions`) and delegate-gated Endpoint functions (`setConfig`) in a single run. If the two roles are different addresses, the run reverts on whichever calls the signer is not authorized for: owner-gated calls revert through OpenZeppelin `Ownable` (`OwnableUnauthorizedAccount`), and delegate-gated Endpoint calls revert with `LZ_Unauthorized` from the Endpoint. * Fewer privileged addresses means a smaller attack surface and simpler multisig management. If you do split them, understand that the Delegate independently controls all Endpoint and security configuration, with no signature required from the Owner. ## Transferring Control Safely One ordering rule matters a great deal when you move control to a new address: **Set the Delegate before transferring ownership.** Only the current Owner can call `setDelegate()`. Once you call `transferOwnership()`, the old key can no longer call owner-gated setters such as `setDelegate()` or `setEnforcedOptions()`. If you transfer ownership to your multisig first but leave the old EOA as the Delegate, later Endpoint calls (for example `setSendLibrary()` during wiring) revert, because the multisig is not the Delegate yet. For that reason our own tooling moves the Delegate first, then the Owner. A correct manual sequence: ```typescript theme={null} const safeMultisig = "0xYourSafeAddress"; // 1. Point the Delegate at your multisig FIRST, while the deployer is still Owner. await (await oft.setDelegate(safeMultisig)).wait(); // 2. Then transfer ownership. After this, the old key loses owner-gated access. await (await oft.transferOwnership(safeMultisig)).wait(); ``` With devtools you do not script this by hand: set the `delegate` and `owner` fields in `layerzero.config.ts`, then use the wiring flow and `npx hardhat lz:ownable:transfer-ownership`. See [Adding a delegate](/v2/get-started/create-lz-oapp/configuring-pathways#adding-delegate) and [Adding an owner](/v2/get-started/create-lz-oapp/configuring-pathways#adding-owner). **Set an explicit Delegate and pin your config.** An OApp with no Delegate set (or one whose contract never exposed `setDelegate()`) cannot configure its own Endpoint settings and is stuck on mutable protocol defaults, which means relying on us to configure DVNs and libraries on its behalf. Set a Delegate and pin your DVN, library, and confirmation config explicitly. ## Use a Multisig for Both Roles Whatever address holds these roles can reconfigure your application, so our recommendation is direct: Use a multisig for **both** the contract Owner and the Endpoint Delegate (they can be the same multisig). An EOA owner is low-hanging fruit for attackers. Transfer both roles, not just the Owner. * **Retain control with a secure multisig.** Do not renounce ownership of critical contracts. Transfer both roles to a multisig and choose a quorum that no single party can satisfy alone. * **Owner and Delegate are equally sensitive.** The split is functional (application vs protocol), not a difference in blast radius: each role can ultimately reconfigure peers or security and disrupt your messaging. Secure both equally. * **Stay flexible.** Keeping control lets you adjust peers, delegates, DVN configuration, and enforced options as your crosschain deployment evolves. * **Document and audit.** Record who holds each role, and review your multisig signers and quorum regularly. **Renouncing is a one-way door.** Do not renounce ownership unless you intend permanent immutability. Renouncing is irreversible: you can never call owner-gated functions again, so your peers and enforced options are frozen and you can no longer change the Delegate. It is a legitimate way to make an OApp's peers immutable, but note that it does not touch the Delegate itself: the Delegate keeps its `setConfig` and message-recovery powers (`clear`, `skip`, `nilify`, `burn`) until you neutralize it separately. ## Transfer to a Safe Multisig [Safe](https://safe.global/) (formerly Gnosis Safe) is the common choice for the Owner and Delegate multisig. Rather than transferring by hand, add a `safeConfig` block (with `safeUrl` and `safeAddress`) to the relevant network in your `hardhat.config.ts`, then push the ownership and wiring transactions through the Safe for approval with the `lz:oapp:wire --safe` flag. The full setup is documented in [Wiring via Safe multisig](/v2/get-started/create-lz-oapp/configuring-pathways#wiring-via-safe-multisig). To call `transferOwnership()`, `setDelegate()`, or any owner function interactively against your own deployment, use the [contracts playground](/v2/developers/evm/contracts-playground). ## Non-EVM Caveats Owner and Delegate are distinct roles on Solana too, but the **address you register is VM-specific and easy to get wrong**. With a [Squads](https://squads.so/) multisig, the owner and delegate must be the Squads **Vault** address, not the Multisig Account address; our tooling rejects the Multisig Account address for these roles. You pass the Multisig Account address only through the `--multisig-key` helper flag, and the tooling derives the Vault (at index 0) from it. Setting the wrong account here is hard to undo, so follow the exact steps in [Transferring OFT ownership on Solana](/v2/developers/solana/technical-reference/solana-guidance#transferring-oft-ownership-on-solana). ## Summary * The Owner controls application policy (`setPeer`, `setEnforcedOptions`); the Delegate controls Endpoint and security config (`setConfig`, libraries, message recovery). * They start as the same address at deploy and should usually stay that way; a mismatch makes wiring revert on whichever role's calls the signer is not authorized for. * Set the Delegate before you transfer ownership, because only the Owner can call `setDelegate()`. * Use a multisig for both roles. Do not use an EOA for production ownership. * Do not renounce ownership unless you intend permanent, unrecoverable immutability. # Deploy Deterministic Addresses Source: https://docs.layerzero.network/v2/developers/evm/tooling/uniform-address Deploying the same OApp contract address on multiple chains can be useful for testing purposes and helpful to users interacting with your contracts across... Deploying the same OApp contract address on multiple chains can be useful for testing purposes and helpful to users interacting with your contracts across various networks. Several methods exist to deploy the same contract address on multiple chains: ### Traditional Method Typically, deploying a contract on different chains involves ensuring the deployer’s nonce is synchronized across these chains. However, the deployment process, often involving multiple transactions, can lead to nonce discrepancies which break the desired deployment. ### CREATE2 Factory While `CREATE2` allows for deterministic deployment of contracts, the resulting address depends on the hash of the contract's creation code. This implies that using different constructor parameters on various chains will result in different contract addresses. ### CREATE3 Factory The `CREATE3` factory improves on `CREATE2` by determining the contract’s address solely based on the deployer's address and a salt value. This method significantly simplifies the deployment of contracts with the same address across multiple chains. Read the [CREATE3 Factory Docs](https://github.com/zeframlou/create3-factory). ### CREATEX Factory CREATEX uses an advanced method for creating and deploying smart contracts with the same address. It's designed to streamline and secure the use of the `CREATE` and `CREATE2` EVM opcodes for contract creation. Read the [CREATEX Factory Docs](https://github.com/pcaversaccio/createx). # Debugging Messages Source: https://docs.layerzero.network/v2/developers/evm/troubleshooting/debugging-messages Frequently asked questions about Debugging Messages. Troubleshooting tips, common issues, and solutions for LayerZero V2 development. Essential information f... The V2 protocol now splits the verification and contract logic execution of messages into two separate, distinct phases: **`Verified`**: the destination chain has received verification from all configured [DVNs](../../../concepts/modular-security/security-stack-dvns) and the message nonce has been committed to the [Endpoint](../../../concepts/protocol/layerzero-endpoint)'s messaging channel. **`Delivered`**: the message has been successfully executed by the [Executor](../../../concepts/permissionless-execution/executors). Because verification and execution are separate, LayerZero can provide specific error handling for each message state. General debugging steps can be found [here](../../../concepts/troubleshooting/debugging-messages). ## Message Execution When your message is successfully delivered to the destination chain, the protocol attempts to execute the message with the execution parameters defined by the sender. Message execution can result in two possible states: * **Success**: If the execution is successful, an event (`PacketReceived`) is emitted. * **Failure**: If the execution fails, the contract reverses the clearing of the payload (re-inserts the payload) and emits an event (`LzReceiveAlert`) to signal the failure. * **Out of Gas**: The message fails because the transaction that contains the message doesn't provide enough gas for execution. The [Message Execution Options](../../../tools/sdks/options) applied to a message can be viewed on LayerZero Scan. There are several ways to determine the optimal gas values for these options. See [Determining Gas Costs](../../../tools/sdks/options#determining-gas-costs) for more details. * **Logic Error**: There's an error in either the contract code or the message parameters passed that prevents the message from being executed correctly. ### Retry Message Because LayerZero separates the verification of a message from its execution, if a message fails to execute due to either of the reasons above, the message can be retried without having to resend it from the origin chain. This is possible because the message has already been confirmed by the DVNs as a valid message packet, meaning execution can be retried at anytime, by anyone. Here's how an OApp contract owner or user can retry a message: * **Using LayerZero Scan**: For users that want a simple frontend interface to interact with, LayerZero Scan provides both message failure detection and in-browser message retrying. * **Calling `lzReceive` Directly**: If message execution fails, any user can retry the call on the Endpoint's `lzReceive` function via the block explorer or any popular library for interacting with the blockchain like [ethers](https://docs.ethers.org/v5/), [viem](https://viem.sh/docs/getting-started.html), etc. #### lzReceive() - Receive Messages Note: In the event of an `lzCompose` failure, the resolution process is similar. Any user can simply retry the call by invoking the Endpoint’s `lzCompose` function. #### lzCompose() - Execute Compose Messages ### Skipping Nonce Occasionally, an [OApp delegate](../oapp/overview#setting-delegates) may want to cancel the verification of an in-flight message. This might be due to a variety of reasons, such as: * **Race Conditions**: conditions where multiple transactions are being processed in parallel, and some might become invalid or redundant before they are processed. * **Error Handling**: In scenarios where a message cannot be delivered (for example, due to being invalid or because prerequisites are not met), the skip function provides a way to bypass it and continue with subsequent messages. By allowing the OApp to skip the problematic message, the OApp can maintain efficiency and avoid getting stuck by a single bottleneck. The `skip` function should be used only in instances where either message **verification** fails or must be stopped, not message **execution**. LayerZero provides separate handling for retrying or removing messages that have successfully been verified, but fail to execute. It is crucial to use this function with caution because once a payload is skipped, it cannot be recovered.
An OApp's delegate can call the `skip` method via the Endpoint to stop message delivery: #### skip() **Example for calling `skip`** 1. **Set up Dependencies and Define the ABI** ```js wrap theme={null} // using ethers v5 const {ethers} = require('ethers'); const skipFunctionABI = [ 'function skip(address _oapp,uint32 _srcEid, bytes32 _sender, uint64 _nonce)', ]; ``` 2. **Configure the Contract Instance** ```js wrap theme={null} // Example Endpoint Address const ENDPOINT_CONTRACT_ADDRESS = '0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7'; const provider = new ethers.providers.JsonRpcProvider(YOUR_RPC_URL); const signer = new ethers.Wallet(YOUR_PRIVATE_KEY, provider); const endpointContract = new ethers.Contract(ENDPOINT_CONTRACT_ADDRESS, skipFunctionABI, signer); ``` 3. **Prepare Function Parameters** ```js wrap theme={null} // Example Oapp Address const oAppAddress = '0x123123123678afecb367f032d93F642f64180aa3'; // Parameters for the skip function const srcEid = 50121; // srcEid example // padding an example address to bytes32 const sender = ethers.zeroPadValue(`0x5FbDB2315678afecb367f032d93F642f64180aa3`, 32); const nonce = 3; // uint64 nonce example const tx = await endpointContract.skip(oAppAddress, srcEid, sender, nonce); ``` 4. **Send the Transaction** ```js wrap theme={null} const tx = await endpointContract.skip(oAppAddress, srcEid, sender, nonce); await tx.wait(); ``` ### Clearing Message As a last resort, an OApp contract owner may want to force eject a message packet, either due to an unrecoverable error or to prevent a malicious packet from being executed: * When logic errors exist and the message can't be retried successfully. * When a malicious message needs to be avoided. **Using the `clear` Function**: This function exists on the Endpoint and allows an OApp contract delegate to burn the message payload so it can never be retried again. It is crucial to use this function with caution because once a payload is cleared, it cannot be recovered. #### clear() - Clear Stored Message **Example for calling `clear`** 1. **Set up Dependencies and Define the ABI** ```js wrap theme={null} // using ethers v5 const {ethers} = require('ethers'); const clearFunctionABI = [ { inputs: [ { components: [ {internalType: 'uint32', name: 'srcEid', type: 'uint32'}, {internalType: 'bytes32', name: 'sender', type: 'bytes32'}, {internalType: 'uint64', name: 'nonce', type: 'uint64'}, ], internalType: 'struct Origin', name: '_origin', type: 'tuple', }, {internalType: 'bytes32', name: '_guid', type: 'bytes32'}, {internalType: 'bytes', name: '_message', type: 'bytes'}, ], name: 'clear', outputs: [], stateMutability: 'nonpayable', type: 'function', }, ]; ``` 2. **Configure the Contract Instance** ```js wrap theme={null} // Example Endpoint Address const ENDPOINT_CONTRACT_ADDRESS = '0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7'; const provider = new ethers.providers.JsonRpcProvider(YOUR_RPC_URL); const signer = new ethers.Wallet(YOUR_PRIVATE_KEY, provider); const endpointContract = new ethers.Contract(ENDPOINT_CONTRACT_ADDRESS, clearFunctionABI, signer); ``` 3. **Prepare Function Parameters** ```js wrap theme={null} // Example Oapp Address const oAppAddress = '0x123123123678afecb367f032d93F642f64180aa3'; // Parameters for the skip function const origin = { srcEid: 10111, // example source chain endpoint Id sender: ethers.zeroPadValue(`0x5FbDB2315678afecb367f032d93F642f64180aa3`, 32), // bytes32 representation of an address nonce: 3, // example nonce }; const _guid = '0x0af522cbed56c0e67988a3eab0e83fc576d501659ffe7743ffa4a0a76b40419d'; // example _guid const _message = '0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000064849484948490000000000000000000000000000000000000000000000000000'; //example _message ``` 4. **Send the Transaction** ```js wrap theme={null} const tx = await endpointContract.clear(oAppAddress, origin, _guid, _message); await tx.wait(); ``` ### Nilify and Burn These two functions exist in the Endpoint contract and are used in very specific cases to avoid malicious acts by DVNs. These two functions are infrequently utilized and serve as precautionary design measures. `nilify` and `burn` are called similarly to `clear` and `skip`, refer to those examples if needed. #### nilify() - Mark Message as Nil The `nilify` function is designed to transform a non-executed payload hash into NIL value (0xFFFFFF...). This transformation enables the resubmission of these NIL packets via the MessageLib back into the endpoint, providing a recovery mechanism from disruptions caused by malicious DVNs. #### burn() - Permanently Block Message The `burn` function operates similarly to the `clear` function with two key distinctions: 1. The OApp is not required to be aware of the original payload 2. The nonce designated for burning must be less than the `lazyInboundNonce` This function exists to avoid malicious DVNs from hiding the original payload to avoid the message from being cleared. # Error Codes & Handling Source: https://docs.layerzero.network/v2/developers/evm/troubleshooting/error-messages Common issues and solutions for Error Codes & Handling. Troubleshoot problems and find answers to frequently asked questions. LayerZero enables secure... This section shows the error that typically occurs when a function is called with parameters that do not match the expected type, range, or format. You can decode LayerZero error codes that are not in human readable format using [**create-lz-oapp**](./error-messages). ### Invalid Argument * `InvalidArgument()` A general error code that implies the parameter passed is invalid. * `InvalidAmount()` An invalid amount has been passed as input. For example, when setting Treasury Native Fee Cap, if the new value is larger than the old valid, this error would occur. * `InvalidNonce()` The error occurs if the nonce value is not the expected nonce. Returned either by the Endpoint if the inbound nonce is not verifiable, or if the provided nonce value is not the next expected nonce (i.e., current nonce + 1). This ensures that nonces are processed in order and no nonce is missed or processed out of order. * `InvalidSizeForAddress()` The error occurs when the input parameter is of the incorrect size. * `InvalidAddress()` The error occurs when the input parameter is of the incorrect length. * `InvalidMessageSize()` The error occurs when the actual message size exceeds the message size cap. * `InvalidPath()` The error occurs when the path length doesn't match `20` + remoteAddressSize. * `InvalidSender()` The error occurs when the sender doesn't match the source address in the path. * `InvalidConfirmations()` The error occurs when a call is made before the required OApp block confirmations. * `InvalidExpiry(uint256 expiry, uint256 minExpiry)` When setting expiry time for the default library, thrown if the expiry time is set before or equal to the current block timestamp. * `InvalidReceiveLibrary()` The error occurs when the receive library is not a valid library when verifying a message. * `InvalidPacketVersion()` The error occurs when the version number of the packet header does not match the expected packet version defined in the ULN. * `InvalidRequiredDVNCount()` The error occurs if the verifier list is not empty while the DVNCount is configured to NONE or DEFAULT. * `InvalidPacketHeader()` The error occurs if the length of packetHeader is not 81. * `InvalidDVNIdx()` The error occurs when the `_DVNIdx` is 255 or greater. The max number of DVN is 255. * `InvalidLegacyType1Option()` The error occurs if there's invalid `type1` option settings (invalid adapterParams from v1). * `InvalidLegacyType2Option()` The error occurs if there's invalid `type2` option settings (invalid adapterParams from v1). * `InvalidDVNOptions()` The error occurs if an invalid or unsupported DVN was set in the DVN config params. * `InvalidRequiredDVNCount()` The error occurs if the actual amount of required DVN violates the config. * `InvalidOptionalDVNCount()` The error occurs if the actual amount of optional DVN violates the config. * `InvalidOptionalDVNThreshold()` The error occurs if the actual threshold of optional DVN violates the config. * `InvalidExecutorOptions()` The error occurs if cursor is not equal to the length of options. * `InvalidExecutor()` The error occurs if the executor address is zero in config params. * `InvalidConfigType()` The error occurs if the config type is invalid. * `InvalidPayloadHash()` The error occurs if the payloadHash passed in the \_inbound argument is empty. * `OnlySendLib()` The error occurs when the message library is ReceiveLibrary while it is supposed to be `SendLibrary`. * `OnlyReceiveLib()` The error occurs when the message library is SendLibrary while it is supposed to be `ReceiveLibrary`. * `OnlyRegisteredLib()` Only registered libraries can be passed as a parameter. Unregistered libraries can't be included. * `OnlyRegisteredOrDefaultLib()` Only non-default libraries can be passed as a parameter. Unregistered or non-default libraries can't be included. * `OnlyNonDefaultLib()` The error occurs when the `_newLib` is either the same as the `defaultLib` or the `oldLib`. Pass a new library address (`_newLib`) that's not the default or current library. * `PathNotInitializable()` The error occurs if the path can not be initialized. * `PathNotVerifiable()` The error occurs if the path is not verifiable. * `ZeroMessageSize()` The error occurs if max message size is zero in config params. * `ZeroLzTokenFee()` If `payInLzToken` is true, the supplied fee must be greater than 0 to prevent a race condition in which an oapp sending a message with lz token and the lz token is set to a new token between the tx being sent and the tx being mined. If the required lz token fee is 0 and the old lz token would be locked in the contract instead of being refunded. * `AtLeastOneDVN()` The error occurs if zero (0) is set for both requiredDVNCount and optionalDVNThreshold. * `InsufficientFee()` The error occurs if `required.nativeFee` is larger than `suppliedNativeFee` or `required.lzTokenFee` is larger than `suppliedLzTokenFee`, or when the msg.value is less than the returned fee amount. * `InsufficientMsgValue()` The error occurs if msg.value is less than the total NativeFee. * `UnknownL2Eid()` This error occurs if the L2 Eid is unknown when looking up the L1 EID for the particular L2 networks. * `Unsorted()` The error occurs when there are duplicate addresses in the `_dvns` array. * `UnsupportedEid()` The error occurs when the endpoint id of the packet header does not match the expected packet endpoint defined in the ULN. * `CannotWithdrawAltToken()` The error occurs if native token is the same as lzToken. * `LzTokenPaymentAddressMustBeSender()` The error occurs if lzToken payment address is not the sender. * `SameValue()` The error occurs if the provided `_newLib` address is the same as the currently set `defaultSendLibrary` for the given `_eid`, or if a user attempts to set the `defaultReceiveLibrary` for a specific `_eid` to the same address it's currently set to. * `NoOptions()` The error occurs if the length of options is zero. * `InvalidWorkerOptions()` The error occurs if the worker options are invalid (less than 2 bytes). * `InvalidWorkerId()` The error occurs if the worker ID is 0. * `NativeAmountExceedsCap()` The error occurs if the native amount to be received on destination exceeds native airdrop cap. * `Verifying()` The error occurs if the state of a packet with the passed arguments (`_config`, `_headerHash` and `_payloadHash`) is not verfiable yet. ### Invalid State This section shows the error that typically occurs if it does not meet certain expected conditions when a function is called or a transaction is executed. * `TransferNativeFailed` The error occurs when sending less than the `_required` amount of native token to the receiver. * `SendReentrancy()` The error occurs when the `_sendContext` has already been entered. The `MessagingContext` requires that \_sendContext has not been entered, and acts as a non-reentrancy guard. ### Permission Denied This section shows the errors that typically occur when a function or operation is attempted by an address that doesn't have the necessary permissions. * `OnlyAltToken()` Only `altFeeToken` can be used for fees. * `OnlyEndpoint()` * `SimpleMessageLib.sol`: requires endpoint == msg.sender * `OnlyExecutor()` The error occurs when the msg.sender is not the executor when executing the message. * `OnlyPriceUpdater()` The error occurs if an unauthorized address (not the contract owner and not in the priceUpdater list) tries to call the function. * `OnlyWhitelistCaller()` `SimpleMessageLib.sol`: requires `msg.sender == whitelistCaller` to call `validatePacket` * `ToIsAddressZero()` The error occurs if the \_to address is zero when calling withdrawFee. * `LzTokenIsAddressZero()` The error occurs if the lzToken address is zero to call withdrawLzTokenFee. * `Unauthorized()` When the msg.send is not the OApp or the delegates of the OApp. * `NotTreasury()` The error occurs if msg.sender is not Treasury when calling treasury only function. ### Not Found This section shows the errors that typically occur when a requested resource does not exist. * `PayloadHashNotFound` In MessagingChannel.sol, the error occurs when the actual payload hash doesn't match the expected payload hash. * `ComposedMessageNotFound` In MessagingComposer.sol, the error occurs when the actual hash doesn't match the expected hash of a composed message. ### Already Exists This section shows the errors that typically error when adding something that conflicts with an existing resource in the contract. * `AddressSizeAlreadySet()` The error occurs when an endpoint's address size has already been set. * `AlreadyRegistered()` The error occurs when the `_lib` has already been registered. * `ComposeExists()` The error occurs when message hash doesn't pass the identity check in the composeQueue. The message must have not been sent before. ### Not Implemented This section shows the error that typically occur when a certain function, method or feature that is not yet defined in the contract. * `NotImplemented()` A general error code that implies undefined function. * `UnsupportedInterface()` The error occurs if the library does not implement ERC165 interface. * `UnsupportedOptionType()` The error occurs when the option type is not supported. For example, Endpoint V1 does not support type 3 options. ### Unavailable This section shows the error that typically occur when a requested resouce is not currently available. * `LzTokenUnavailable()` The error occurs if LzToken is not available for payments but users passed LzTokens in for payments. Simply set payInLzToken to false in this case. * `LzTokenNotEnabled()` The error occurs if the lzToken is not enabled when calling \_getFee . * `DefaultSendLibUnavailable()` The error occurs if the send message library doesn't support the specific endpoint ID. * `DefaultReceiveLibUnavailable()` The error occurs if the receive message library doesn't support the specific endpoint ID. # Hyperliquid & LayerZero Composer - Core Concepts Source: https://docs.layerzero.network/v2/developers/hyperliquid/hyperliquid-concepts This document covers the essential concepts of Hyperliquid and the LayerZero Hyperliquid Composer. Understanding these is key before proceeding with the... This document covers the essential concepts of Hyperliquid and the LayerZero Hyperliquid Composer. Understanding these is key before proceeding with the deployment. ### 1. Introduction to Hyperliquid Hyperliquid consists of an `EVM` named `HyperEVM` and a `L1` network called `HyperCore`. These networks function together under the same `HyperBFT` consensus to act as a singular network. `HyperCore` includes fully onchain perpetual futures and spot order books. Every order, cancel, trade, and liquidation happens transparently with one-block finality inherited from HyperBFT. `HyperCore` currently supports 200k orders / second The `HyperEVM` brings the familiar general-purpose smart contract platform pioneered by Ethereum to the Hyperliquid blockchain. With the `HyperEVM`, the performant liquidity and financial primitives of `HyperCore` are available as permissionless building blocks for all users and builders. 3D architectural diagram of the Hyperliquid Stack showing HyperBFT as the foundation layer, HyperCore and HyperEVM as middle layers, and application towers on top including Oracles, Spot, Perps, Borrowing & Lending, Auctions, Vaults, Governance, Native Stablecoins, Bridges, and more #### HyperCore **HyperCore**, or Core, is a high-performance Layer 1 that manages the exchange’s onchain order books with one-block finality. Communication with `HyperCore` is done via `L1 actions` or `actions`, as opposed to the usual RPC calls which are used for EVM chains. Full list of `L1 actions` here: [Exchange endpoint](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint). #### HyperEVM **HyperEVM**, or EVM, is an Ethereum Virtual Machine (EVM)-compatible environment that allows developers to build decentralized applications (dApps). You can interact with HyperEVM via traditional `eth_` RPC calls (full list here: [HyperEVM JSON-RPC](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm/json-rpc)). `HyperEVM` has precompiles that let you interact with `HyperCore`, where spot and perpetual trading happens (and is probably why you are interested in going to Hyperliquid). If you are not listing on `HyperCore`, then HyperEVM is your almost standard EVM network - you just need to switch block sizes. #### **Block Explorers:** `HyperEVM` and `HyperCore` have their own block explorers. You can find ([a list of explorers here](https://hyperliquid-co.gitbook.io/community-docs/ecosystem/projects/tools#blockchain-explorers)). ### 2. Hyperliquid API Hyperliquid supports several API functions that users can use on HyperCore to query information, following is an example. ```bash wrap theme={null} curl -X POST https://api.hyperliquid-testnet.xyz/info \ -H "Content-Type: application/json" \ -d '{"type": "spotMeta"}' ``` This will give you the spot meta data for HyperCore. A sample response is below. ```json wrap theme={null} { "universe": [ { "name": "ALICE", "szDecimals": 0, "weiDecimals": 6, "index": 1231, "tokenId": "0x503e1e612424896ec6e7a02c7350c963", "isCanonical": false, "evmContract": null, "fullName": null, "deployerTradingFeeShare": "1.0" } ] } ``` * The `tokenId` is the address of the token on `HyperCore`. * The `evmContract` is the address of the `ERC20` token on `HyperEVM`. * The `deployerTradingFeeShare` is the fee share for the deployer of the token. ### 3. HyperCore Actions An action as defined by Hyperliquid is a transaction that is sent to the `HyperCore` - as it updates state on the `HyperCore` it needs to be a signed transaction from the wallet of the action sender. You need to use `ethers-v6` to sign actions - [https://docs.ethers.org/v6/api/providers/#Signer-signTypedData](https://docs.ethers.org/v6/api/providers/#Signer-signTypedData) ```bash wrap theme={null} # add ethers-v6 to your project as an alias for ethers@^6.13.5 pnpm add ethers-v6@npm:ethers@^6.13.5 ``` ```ts wrap theme={null} import {Wallet} from 'ethers'; // ethers-v5 wallet import {Wallet as ethersV6Wallet} from 'ethers-v6'; // ethers-v6 wallet const signerv6 = new ethersV6Wallet(wallet.privateKey); // where wallet is an ethers.Wallet from ethers-v5 const signature = await signerv6.signTypedData(domain, types, message); ``` This is because in `ethers-v5` EIP-712 signing is not stable: [https://docs.ethers.org/v5/api/signer/#Signer-signTypedData](https://docs.ethers.org/v5/api/signer/#Signer-signTypedData) > Experimental feature (this method name will change) > This is still an experimental feature. If using it, please specify the exact version of ethers you are using (e.g. spcify "5.0.18", not "^5.0.18") as the method name will be renamed from \_signTypedData to signTypedData once it has been used in the field a bit. You can use the official [Hyperliquid Python SDK](https://github.com/hyperliquid-dex/hyperliquid-python-sdk) to interact with `HyperCore`. LayerZero also built an in-house minimal [TypeScript SDK](./hyperliquid-sdk) that focuses on switching blocks, deploying the `HyperCore` token, and connecting the `HyperCore` token to a `HyperEVM` ERC20 (OFT). ### 4. Accounts You can use the same account (private key) on both `HyperEVM` and `HyperCore`. `HyperCore` uses signed Ethereum transactions to validate data. ### 5. Multi-Block Architecture `HyperEVM` and `HyperCore` are separate entities, so they have separate blocks, interleaved by their creation order. #### HyperEVM Blocks `HyperEVM` has two kinds of blocks: * **Small Blocks**: Default, 1-second block time, 2M gas limit. For high throughput transactions. OFT deployments are typically larger than 2M gas. * **Big Blocks**: 1 transaction per block, 1 block per minute, 30M gas limit. For deploying large contracts. You can toggle between block types for your account using an `L1 action` of type `evmUserModify`: ```json wrap theme={null} {"type": "evmUserModify", "usingBigBlocks": true} ``` You can also switch to big blocks using [LayerZero Hyperliquid SDK](./hyperliquid-sdk#3-switching-blocks-evmusermodify) with a simple command: ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer set-block --size big --network mainnet --private-key $PRIVATE_KEY ``` Flagging a user for big blocks means all subsequent HyperEVM transactions from that user will be big block transactions until toggled off. To toggle back to small blocks, set `usingBigBlocks` to `false`. Alternatively, use `bigBlockGasPrice` instead of `gasPrice` in transactions. #### HyperCore Blocks `HyperCore` has its own blocks, which means there are 3 block types in total. As `HyperCore` and `HyperEVM` blocks are produced at different speeds, with `HyperCore` creating more than `HyperEVM`, the blocks are created in not a strictly alternating manner. For example, the block sequence might look like this: ``` [Core] → [Core] → [EVM-small] → [Core] → [Core] → [EVM-small] → [Core] → [EVM-large] → [Core] → [EVM-small] ``` ### 6. Precompiles and System Contracts Hyperliquid uses precompiles in two ways: System Contracts and L1ActionPrecompiles. **System Contracts**: * `0x2222222222222222222222222222222222222222`: System contract address for the `HYPE` token. * `0x200000000000000000000000000000000000abcd`: System contract address for a created Core Spot token (asset bridge). * `0x3333333333333333333333333333333333333333`: The `CoreWriter` for sending transactions to HyperCore. **L1ActionPrecompiles**: * `0x0000000000000000000000000000000000000801`: `SpotBalance` precompile for reading token balances * `0x0000000000000000000000000000000000000810`: `CoreUserExists` precompile for checking account activation **Key Precompile Functions:** * `spotBalance(address user, uint64 token)`: Returns balance information for a user's token holdings on HyperCore * `coreUserExists(address user)`: Checks if a user account is activated on HyperCore * `CoreWriter`: Writes to the first produced `HyperCore` block after the production of the EVM block The Composer implementation uses these precompiles directly for reliable HyperCore interaction. ### 7. Token Standards * **Token standard on HyperEVM**: `ERC20` (EVM Spot) * **Token standard on HyperCore**: `HIP-1` (Core Spot) Deploying a Core Spot token involves a 31-hour Dutch auction for a core spot index, followed by configuration. ### Critical Note on Hyperliquidity Using the Hyperliquid UI for spot deployment ([https://app.hyperliquid.xyz/deploySpot](https://app.hyperliquid.xyz/deploySpot)) forces the use of "Hyperliquidity". **This is NOT supported by LayerZero** as it can lead to an uncollateralized asset bridge. Deploy via API/SDK to avoid this. The [LayerZero SDK](./hyperliquid-sdk) facilitates this. ### 8. The Asset Bridge: Linking EVM Spot (ERC20) and Core Spot (HIP-1) For tokens to be transferable between `HyperEVM` and `HyperCore`, the EVM Spot (**ERC20**) and Core Spot (**HIP-1**) must be linked. This creates an **asset bridge precompile** at an address like `0x2000...abcd` (where `abcd` is the `coreIndexId` of the **HIP-1** in hexadecimal). **Linking Process:** 1. **`requestEvmContract`**: Initiated by the HyperCore deployer, signaling intent to link HIP-1 to an ERC20. 2. **`finalizeEvmContract`**: Initiated by the HyperEVM deployer (EOA) to confirm the link. **Asset Bridge Mechanics:** The asset bridge (`0x2000...abcd`) acts like a lockbox. * To send tokens from HyperEVM to HyperCore: Transfer ERC20 tokens to its asset bridge address on HyperEVM. * To send tokens from HyperCore to HyperEVM: Use the `spotSend` L1 action, targeting the asset bridge address on HyperCore. **Funding:** For tokens to move into `HyperCore`, the deployer must mint the maximum supply (e.g., `u64.max` via API) of HIP-1 tokens to the token's asset bridge address on `HyperCore` (or to their deployer account and then transfer). `u64.max` is the maximum value for a `u64` integer, which is `2^64 - 1`. It's a 20-digit number: `18,446,744,073,709,551,615` (18.4 quintillion, or `18` + 18 zeros) Example of transition in bridge balances: 1. Initial state: `[AssetBridgeEVM: 0 | AssetBridgeCore: 0]` 2. Fund HyperCore bridge: `[AssetBridgeEVM: 0 | AssetBridgeCore: X]` 3. User bridges `X*scale` tokens from EVM to Core: User sends `X*scale` ERC20 to EVM bridge. 4. New state: `[AssetBridgeEVM: X*scale | AssetBridgeCore: 0]` ### Critical Warning on Bridge Capacity Hyperliquid has **no checks** for asset bridge capacity. If you try to bridge more tokens than available on the destination side of the bridge, all tokens will be locked in the asset bridge address **forever**. The Hyperliquid Composer contract includes checks to refund users on `HyperEVM` if such a scenario is detected. ### Partial Funding Issue "Partially funding" the HyperCore asset bridge is problematic. If initial funds are consumed (`[X.EVM | 0]`) and you add more `Y.Core` tokens to the HyperCore bridge, it might trigger a withdrawal of `X.EVM` tokens, leading to `[0 | Y.Core]` but with `X.Core` tokens (previously converted from `X.EVM`) still in circulation on HyperCore that cannot be withdrawn back to EVM. **Always fully fund the HyperCore side of the asset bridge with the total intended circulatable supply via the bridge.** ### 9. Communication between HyperEVM and HyperCore * **HyperEVM reads state from HyperCore**: Via `precompiles` (e.g., perp positions). * **HyperEVM writes to HyperCore**: Via `events` at specific `precompile` addresses AND by transferring tokens through the asset bridge. ### 10. Transfers between HyperEVM and HyperCore Spot assets can be sent from HyperEVM to HyperCore and vice versa. They are called `Core Spot` and `EVM Spot`. These are done by sending an `ERC20::transfer` with asset bridge address as the recipient. To move tokens across: 1. Send tokens to the **asset bridge address** (`0x2000...abcd`) on the source network (HyperEVM or HyperCore). * On HyperEVM, this is an `ERC20::transfer(assetBridgeAddress, value)` * The event emitted is `Transfer(address from, address to, uint256 value)` → `Transfer(_from, assetBridgeAddress, value);` * The `Transfer` event is picked up by Hyperliquid's backend. 2. The tokens are credited to your account on the destination network. 3. Then, on the destination network, send tokens from your address to the final receiver's address. The [HyperliquidComposer](https://github.com/LayerZero-Labs/devtools/blob/main/packages/hyperliquid-composer/contracts/HyperLiquidComposer.sol) contract from [LayerZero Hyperliquid SDK](./hyperliquid-sdk) automates these actions. ### 11. Hyperliquid Composer The Composer facilitates `X-network` → `HyperCore` OFT transfers. **Why a Composer?** Users might want to hold tokens on `HyperEVM` and only move to `HyperCore` for trading. Auto-conversion in `lzReceive` isn't ideal. An `lzCompose` function allows this flexibility. **Mechanism:** 1. A LayerZero message sends tokens to Hyperliquid. `lzReceive` on the OFT on HyperEVM mints tokens to the `HyperLiquidComposer` contract address. 2. The `composeMsg` in `SendParam` (from the source chain call) contains the **actual receiver's address** on Hyperliquid. 3. The `HyperLiquidComposer`'s `lzCompose` function is triggered. 4. The Composer: * Transfers the received EVM Spot tokens (ERC20) from itself to the token's **asset bridge address** (`0x2000...abcd`). This `Transfer` event signals Hyperliquid's backend to credit the tokens on HyperCore. * Performs an `CoreWriter` transaction (to `0x33...33`) instructing HyperCore to execute a `spot transfer` of the corresponding HIP-1 tokens from the Composer's implied Core address (derived from its EVM address) to the **actual receiver's address** (from `composeMsg`) on HyperCore. That particular `Transfer` event is what Hyperliquid nodes/relayers listen to in order to credit the `receiver` address on Core. ```solidity wrap theme={null} struct SendParam { uint32 dstEid; bytes32 to; // OFT address (so that the OFT can execute the `compose` call) uint256 amountLD; uint256 minAmountLD; bytes extraOptions; bytes composeMsg; // token receiver address (msg.sender if you want your address to receive the token) bytes oftCmd; } ``` ### Token Decimals `HyperCore::HIP1` decimals can differ from `HyperEVM::ERC20` decimals. The Composer handles scaling. Amounts on HyperCore will reflect HIP-1 decimals. Converting back restores ERC20 decimals. **Composer Contract:** The composer is a separate contract deployed on HyperEVM that uses Hyperliquid precompiles directly. ```solidity wrap theme={null} contract HyperLiquidComposer is HyperLiquidCore, ReentrancyGuard, IHyperLiquidComposer, IOAppComposer { constructor( address _oft, // The OFT contract address uint64 _coreIndexId, // Core Spot token's index ID int8 _assetDecimalDiff // Decimal difference: EVM decimals - Core decimals ) { // Automatically configures both ERC20 and native HYPE support // Uses precompiles for direct HyperCore interaction } function lzCompose(address _oft, bytes32 _guid, bytes calldata _message, address _executor, bytes calldata _extraData) external payable override { // Error handling with failedMessages mapping // Direct precompile calls for HyperCore interaction // Built-in activation checks and refund mechanisms } } ``` ### 12. LayerZero Transaction on HyperEVM LayerZero OFT transfers to Hyperliquid use the compose pattern to enable automatic bridging from HyperEVM to HyperCore. **How Compose Enables HyperCore Transfers:** 1. **OFT Delivery**: The LayerZero message delivers tokens to the `HyperLiquidComposer` contract address on HyperEVM 2. **Compose Message**: The `SendParam.composeMsg` contains the final recipient's address and any native HYPE amount needed 3. **Automatic Bridging**: The Composer's `lzCompose` function automatically bridges tokens from HyperEVM to HyperCore and transfers them to the final recipient **ComposeMsg Encoding:** The `composeMsg` uses a standardized 64-byte format that tells the Composer where to send tokens on HyperCore: ```solidity wrap theme={null} // Encode the final recipient and optional HYPE amount bytes memory composeMsg = abi.encode( uint256 minMsgValue, // Minimum HYPE amount to send to recipient (0 if none) address receiver // Final recipient address on HyperCore ); ``` This encoding allows the Composer to handle both ERC20 token bridging and optional native HYPE transfers in a single compose operation. **Composer Logic:** 1. **Activation Check**: Uses `coreUserExists()` precompile to verify receiver is activated on HyperCore 2. **Bridge Capacity Check**: Uses `spotBalance()` precompile to check asset bridge capacity 3. **Dual Asset Support**: Handles both ERC20 tokens and native HYPE transfers 4. **Error Recovery**: Failed messages are stored in `failedMessages` mapping for later refund **Implementation Details:** ```solidity wrap theme={null} // Precompile-based approach function lzCompose(address _oft, bytes32 _guid, bytes calldata _message, ...) external payable { // 1. Decode message with enhanced validation (uint256 minMsgValue, address receiver) = abi.decode(composeMsg, (uint256, address)); // 2. Check activation status via precompile if (!coreUserExists(receiver).exists) revert CoreUserNotActivated(); // 3. Check bridge capacity via precompile uint64 bridgeBalance = spotBalance(assetBridge, coreIndex).total; // 4. Transfer to asset bridge IERC20(token).safeTransfer(assetBridge, amounts.evm); // 5. Use CoreWriter precompile for HyperCore transfer _submitCoreWriterTransfer(receiver, coreIndex, amounts.core); } // Precompile-based CoreWriter call function _submitCoreWriterTransfer(address _to, uint64 _coreIndex, uint64 _coreAmount) internal { bytes memory action = abi.encode(_to, _coreIndex, _coreAmount); bytes memory payload = abi.encodePacked(SPOT_SEND_HEADER, action); // Pre-computed header ICoreWriter(HLP_CORE_WRITER).sendRawAction(payload); } ``` **Error Handling Features:** * **Failed Message Storage**: Messages that fail decoding are stored for crosschain refund * **Automatic Refunds**: Failed transfers refund tokens to receiver on HyperEVM * **Gas Protection**: Minimum gas requirements prevent out-of-gas failures * **Bridge Capacity Protection**: Prevents token locking when bridge capacity is insufficient # Deployment Guide - OFT on Hyperliquid with LayerZero Composer Source: https://docs.layerzero.network/v2/developers/hyperliquid/hyperliquid-oft-deployment This guide provides a step-by-step process for deploying your Omnichain Fungible Token (OFT) on Hyperliquid (both HyperEVM and HyperCore) using the... This guide provides a step-by-step process for deploying your Omnichain Fungible Token (OFT) on Hyperliquid (both HyperEVM and HyperCore) using the LayerZero Hyperliquid Composer and SDK. ## Prerequisites * **Understanding Core Concepts**: Ensure you've reviewed [Hyperliquid - Core Concepts](./hyperliquid-concepts). * **Software**: * Node.js, pnpm/npm/yarn. * `@layerzerolabs/hyperliquid-composer` SDK installed (`npx @layerzerolabs/hyperliquid-composer -h` to check). * Hardhat or Forge for contract deployment and scripting (examples use Hardhat and Forge). * **Accounts & Funding**: * An EVM-compatible wallet with a private key for deployments. * **Crucially**: Your deployer address must be activated on **HyperCore**. This typically means it needs to have received at least \$1 in `USDC` or `HYPE` on HyperCore. This is required for operations like block switching or deploying Core Spot assets, as these involve L1 actions. If not funded, you might see errors like `L1 error: User or API Wallet does not exist.` * **LayerZero Configuration**: A `layerzero.config.ts` file for your OApp. ## Hyperliquid Composer Deployment Checklist This checklist is a kind of cheat sheet and "table of contents" for anyone deploying to HyperEVM and HyperCore. The full guide is below and the checklist is just a quick reference, with links to sections in the full guide. ### Step 0: Deploy your OFT | Action | Performed by | Actionable with | Recommended for | | ------ | ------------ | ------------------------------------------------------------------------ | ------------------------- | | Path 1 | OFT Deployer | `LZ_ENABLE_EXPERIMENTAL_HYPERLIQUID_EXAMPLE=1 npx create-lz-oapp@latest` | HyperCore deployments | | Path 2 | OFT Deployer | Vanilla OFT repo + `npx @layerzerolabs/hyperliquid-composer` | Only HyperEVM deployments | * [ ] Activate your deployer account on HyperCore without burning a nonce on HyperEVM. Get someone to send at least **\$1** in `USDC` or `HYPE` to your deployer account on HyperCore, or get funds on a burner wallet on HyperEVM, transfer it across, and then transfer it to the deployer account. #### Path 1 - With a new repo * [ ] Create a new Hyperliquid example repo ```bash wrap theme={null} LZ_ENABLE_EXPERIMENTAL_HYPERLIQUID_EXAMPLE=1 npx create-lz-oapp@latest ``` * This comes with the composer and composer deploy script. * Deploy scripts perform block switching operations. * Composer can be deployed after the core spot is deployed ([Step 4](#step-4-deploy-the-hyperliquidcomposer-contract)). It will not work until the two are linked. * Composer has default error handling mentioned in [Modifying OFT/Composer Behavior & Error Handling](#modifying-oftcomposer-behavior--error-handling). #### Path 2 - Existing repo with OFT Block switching is not present in the default OFT deploy script. * [ ] Switch to big block before deploying the OFT ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer set-block \ --size big \ --network \ --log-level verbose \ --private-key $PRIVATE_KEY ``` * [ ] Deploy the OFT * [ ] Switch to small block after deploying the OFT ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer set-block \ --size small \ --network \ --log-level verbose \ --private-key $PRIVATE_KEY ``` If you are only doing `HyperEVM` deployment, you are done. The rest of the steps are only for `HyperCore` deployments. ### Step 1 (Optional): Purchase your HyperCore Spot | Action | Performed by | Actionable with | Required for | | ------------- | ----------------- | -------------------------------------------------------------------------------- | ------------ | | Purchase Spot | CoreSpot Deployer | [https://app.hyperliquid.xyz/deploySpot](https://app.hyperliquid.xyz/deploySpot) | `HyperCore` | | Blocked by | None | None | Step 2 | * [ ] Purchase your HyperCore Spot [engaging in the auction](https://hyperliquid.gitbook.io/hyperliquid-docs/hyperliquid-improvement-proposals-hips/hip-1-native-token-standard#gas-cost-for-deployment) ### Step 2: Deploy the Core Spot | Action | Performed by | Actionable with | Required for | | --------------- | ----------------- | ----------------------------------------- | ------------ | | Deploy CoreSpot | CoreSpot Deployer | `npx @layerzerolabs/hyperliquid-composer` | `HyperCore` | | Blocked by | OFT Deployer | Step 0 | Step 3 | | Blocked by | CoreSpot Deployer | Step 1 | Step 3 | * [ ] Deploy the CoreSpot following [Step 2: Deploy the Core Spot (HIP-1 Token)](#step-2-deploy-the-core-spot-hip-1-token) #### Step 2.1: Create a HyperCore deployment file | Action | Performed by | Actionable with | Required for | | -------------------------------- | ----------------- | ----------------------------------------- | ------------ | | Create HyperCore Deployment File | CoreSpot Deployer | `npx @layerzerolabs/hyperliquid-composer` | `HyperCore` | | Blocked by | OFT Deployer | Step 0 | Step 3 | | Blocked by | CoreSpot Deployer | Step 1 | Step 2.2 | * [ ] Follow the instuctions in [Step 2.1: Create a HyperCore Deployment File](#step-21-create-a-hypercore-deployment-file-core-spot-create) * [ ] Core spot deployer needs OFT address and deployed transaction hash #### Step 2.2: Set the user genesis | Action | Performed by | Actionable with | Required for | | ---------------- | ----------------- | ----------------------------------------- | ------------ | | Set User Genesis | CoreSpot Deployer | `npx @layerzerolabs/hyperliquid-composer` | `HyperCore` | | Blocked by | CoreSpot Deployer | Step 2.1 | Step 2.4 | * [ ] Follow the instructions in [Step 2.2: Set User Genesis](#step-22-set-user-genesis-usergenesis) * [ ] HyperCore balances are u64 - the max balance is `2^64 - 1 = 18446744073709551615` * [ ] Make sure the total balances in the json does not exceed this value. * [ ] Re-runnable until the next step is executed. * [ ] UserGenesis transactions stack : If you set the balance of address X to `18446744073709551615` and then set the balance of address Y to `18446744073709551615` after removing X from the json, the net effect is that both X and Y will have `18446744073709551615` tokens. * You can either mint the entire amount to the asset bridge address (default) or the deployer address. * If you want to read more about the asset bridge address, see [Modifying OFT/Composer Behavior & Error Handling](#modifying-oftcomposer-behavior--error-handling) #### Step 2.3: Confirm the user genesis | Action | Performed by | Actionable with | Required for | | -------------------- | ----------------- | ----------------------------------------- | ------------ | | Confirm User Genesis | CoreSpot Deployer | `npx @layerzerolabs/hyperliquid-composer` | `HyperCore` | | Blocked by | CoreSpot Deployer | Step 2.2 | Step 2.5 | * [ ] Follow the instructions in [Step 2.3: Confirm User Genesis](#step-23-confirm-user-genesis-setgenesis) * [ ] Locks in the user genesis step and is now immutable. #### Step 2.4: Register the spot | Action | Performed by | Actionable with | Required for | | ------------- | ----------------- | ----------------------------------------- | ------------ | | Register Spot | CoreSpot Deployer | `npx @layerzerolabs/hyperliquid-composer` | `HyperCore` | | Blocked by | CoreSpot Deployer | Step 2.3 | Step 3 | * [ ] Follow the instructions in [Step 2.4: Register the Spot](#step-24-register-the-spot-registerspot) * [ ] Only USDC is supported on HyperCore at the moment - the SDK defaults to USDC. * [ ] Make sure the asset bridge address on HyperCore has all the tokens minted in Step 2.2. Partial funding is not supported. #### Step 2.5: Register Hyperliquidity | Action | Performed by | Actionable with | Required for | | ----------------------- | ----------------- | ----------------------------------------- | ------------ | | Register Hyperliquidity | CoreSpot Deployer | `npx @layerzerolabs/hyperliquid-composer` | `HyperCore` | | Blocked by | OFT Deployer | Step 0 | Step 6 | | Blocked by | CoreSpot Deployer | Step 2.1 | None | * [ ] Follow the instructions in [Step 2.5: Register Hyperliquidity](#step-25-register-hyperliquidity-createspotdeployment) * [ ] `nOrders` MUST be set to 0 as we are not engaging with hyperliquidity * [ ] The other values are token owner choice (is usually non 0) * Step MUST be run even though we set `noHyperliquidity=true` in genesis * This can be run even after deployment and linking * The final step to be executed after which the token will be listed on the spot order book. #### Step 2.6: Set deployer fee share | Action | Performed by | Actionable with | Required for | | ---------------------- | ----------------- | ----------------------------------------- | ------------ | | Set Deployer Fee Share | CoreSpot Deployer | `npx @layerzerolabs/hyperliquid-composer` | `HyperCore` | | Blocked by | OFT Deployer | Step 0 | Step 6 | | Blocked by | CoreSpot Deployer | Step 2.1 | None | * [ ] Follow the instructions in [Step 2.6: Set Deployer Trading Fee Share](#step-26-set-deployer-trading-fee-share-setdeployertradingfeeshare) * [ ] Trading fee share is usually 100% (default value) - this allocates the trading fees to the token deployer instead of burning it. * [ ] Do not lose or burn your deployer address as it collects tokens. * [ ] Step can be re-run as long as the new fee% is lower than the current one. * Even though the default value is 100%, it is recommended that you set it * This can be run even after deployment and linking ### Step 3: Connect the HyperCoreSpot to HyperEVM OFT #### Step 3.1: Create a request to connect the HyperCoreSpot to HyperEVM OFT | Action | Performed by | Actionable with | Required for | | -------------- | ----------------- | ----------------------------------------- | ------------ | | Create Request | CoreSpot Deployer | `npx @layerzerolabs/hyperliquid-composer` | `HyperCore` | | Blocked by | CoreSpot Deployer | Step 0, Step 2 | Step 3.2 | * [ ] Follow the instructions in [Step 3.1: Request EVM Contract Link](#step-31-request-evm-contract-link-core--evm-intention) * [ ] Make sure the core spot deployer has the OFT address. #### Step 3.2: Accept the request to connect the HyperCoreSpot to HyperEVM OFT | Action | Performed by | Actionable with | Required for | | -------------- | ----------------- | ----------------------------------------- | ------------ | | Accept Request | OFT Deployer | `npx @layerzerolabs/hyperliquid-composer` | `HyperCore` | | Blocked by | CoreSpot Deployer | Step 3.1 | Step 4 | * [ ] Follow the instructions in [Step 3.2: Finalize EVM Contract Link](#step-32-finalize-evm-contract-link-evm--core-confirmation) * [ ] Create a deployment file for the core spot before linking. ### Step 4: Deploy the Composer | Action | Performed by | Actionable with | Required for | | --------------- | ----------------- | ----------------------------------------- | ------------ | | Deploy Composer | OFT Deployer | `npx @layerzerolabs/hyperliquid-composer` | `HyperCore` | | Blocked by | CoreSpot Deployer | Step 3 | None | * [ ] Follow the instructions in [Step 4: Deploy the HyperLiquidComposer Contract](#step-4-deploy-the-hyperliquidcomposer-contract) * Deployer script in the OFT repo will deploy the composer - it also handles block switching. * [ ] Make sure the Composer's address is activated on HyperCore (sending it at least \$1 worth of `HYPE` or `USDC`). * Composer is re-deployable and independent of the OFT and does not need to be linked with anything. ### Step 5: Listing on spot order books | Action | Performed by | Actionable with | Required for | | ----------------- | ----------------- | ----------------------------------------- | ------------ | | Spot Book Listing | Automatic | `npx @layerzerolabs/hyperliquid-composer` | HyperCore | | Blocked by | CoreSpot Deployer | Step 2 | none | This is automatically completed when all steps in Step 2 are completed. ### Step 6: Listing on perp order books | Action | Performed by | Actionable with | Required for | | ----------------- | ----------------- | ----------------------------------------- | ------------ | | Perp Book Listing | Automatic | `npx @layerzerolabs/hyperliquid-composer` | HyperCore | | Blocked by | CoreSpot Deployer | Step 2 | none | This is controlled by the Hyperliquid community [(source)](https://hyperliquid.gitbook.io/hyperliquid-docs/trading/perpetual-assets): > Hyperliquid currently supports trading of 100+ assets. Assets are added according to community input. ## Full Hyperliquid OFT Deployment Guide ### Step 0: Deploy Your OFT on HyperEVM You have two main paths depending on your project setup: starting fresh or using an existing OFT project. #### **Path 1: New Project using LayerZero Hyperliquid Example** This path is recommended if you are starting fresh and intend to deploy to HyperCore. 1. **Create a new Hyperliquid example repository:** ```bash wrap theme={null} LZ_ENABLE_EXPERIMENTAL_HYPERLIQUID_EXAMPLE=1 npx create-lz-oapp@latest ``` * This template includes the `HyperLiquidComposer` contract and its deployment script. * The deploy scripts automatically handle HyperEVM block switching (to "big blocks" for deployment and back to "small blocks" after deployment is complete). 2. **Activate Deployer Account on HyperCore:** Ensure your OFT deployer address has a balance (e.g., \$1 USDC or HYPE) on HyperCore *before* deploying. This is needed for the deploy script to perform L1 actions like block switching. 3. **Deploy your OFT and (optionally) the Composer:** The example repository will have `hardhat-deploy` scripts. ```bash wrap theme={null} npx hardhat lz:deploy --tags MyHyperLiquidOFT # Or your OFT's tag # The Composer can be deployed later (Step 4), after the Core Spot is set up. # npx hardhat lz:deploy --tags MyHyperLiquidComposer ``` * The Composer comes with default error handling mechanisms (detailed in the "Modifying OFT/Composer Behavior" section below). #### **Path 2: Existing OFT Project** If you have an existing OFT project and want to add Hyperliquid support: 1. **Activate Deployer Account on HyperCore:** As above, ensure your deployer address is funded on HyperCore for L1 actions. 2. **Manually Switch to Big Blocks on HyperEVM:** Contract deployments on HyperEVM typically require "big blocks" due to gas limits. ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer set-block \ --size big \ --network {testnet | mainnet} \ --private-key $PRIVATE_KEY \ [--log-level verbose] ``` *Replace `{testnet | mainnet}` and `$PRIVATE_KEY` accordingly.* 3. **Deploy your OFT:** Use your existing deployment scripts (e.g., `npx hardhat deploy --network hyperliquid_testnet --tags YourOFTTag`). 4. **Manually Switch back to Small Blocks on HyperEVM:** ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer set-block \ --size small \ --network {testnet | mainnet} \ --private-key $PRIVATE_KEY \ [--log-level verbose] ``` **Post-Deployment (Both Paths):** * **Wire your OFTs:** Connect your newly deployed OFT on Hyperliquid with its counterparts on other chains. ```bash wrap theme={null} npx hardhat lz:oapp:wire --oapp-config path/to/your/layerzero.config.ts ``` * **Test Basic OFT Transfers:** Verify that standard OFT sends (without composition) work to and from Hyperliquid (HyperEVM). > ⚠️ **If you are only deploying to HyperEVM (i.e., your token will only exist as an ERC20 on HyperEVM and not be bridged to HyperCore), you are done with deployment steps related to Hyperliquid specifics beyond standard OFT deployment.** The following steps are for HyperCore integration. ### Step 1: (Optional) Purchase Your HyperCore Spot Index This step is required if you want your token to exist natively on HyperCore (as a HIP-1 token) and be bridgeable with your HyperEVM OFT. * **Action:** Purchase a Core Spot Index. * **Performed by:** CoreSpot Deployer (can be the same as OFT Deployer). * **Tool:** Hyperliquid UI: * [https://app.hyperliquid.xyz/deploySpot](https://app.hyperliquid.xyz/deploySpot) (for mainnet) * [https://app.hyperliquid-testnet.xyz/deploySpot](https://app.hyperliquid-testnet.xyz/deploySpot) (for testnet) * **Details:** This involves participating in a [Dutch auction for the deployment gas cost](https://hyperliquid.gitbook.io/hyperliquid-docs/hyperliquid-improvement-proposals-hips/hip-1-native-token-standard#gas-cost-for-deployment). The auction duration is 31 hours. ### Step 2: Deploy the Core Spot (HIP-1 Token) This process registers your token natively on HyperCore. **Tool:** `@layerzerolabs/hyperliquid-composer` SDK. **General Notes for CoreSpot Deployment:** ### REMINDER: HYPERLIQUIDITY IS NOT SUPPORTED BY LAYERZERO When deploying a Core Spot, avoid using the "Hyperliquidity" feature often defaulted by the Hyperliquid UI. It is incompatible with the LayerZero asset bridge mechanism as it can lead to uncollateralized states. The SDK commands help you deploy *without* Hyperliquidity. You can monitor the deployment progress using the [Hyperliquid UI](https://app.hyperliquid.xyz/deploySpot) or by querying the API: ```bash wrap theme={null} curl -X POST "https://api.hyperliquid.xyz/info" \ -H "Content-Type: application/json" \ -d '{ "type": "spotDeployState", "user": "" }' ``` This will return a json object with the current state of the spot deployment. #### **Step 2.1: Create a HyperCore Deployment File (`core-spot create`)** This will create a new file under `./deployments/hypercore-{testnet | mainnet}` with the name of the Core Spot token index. This is not a Hyperliquid step but rather something to make the deployment process easier. This file stores configuration for your Core Spot token and is used by subsequent SDK commands. It is crucial to the functioning of the token deployment after which it really is not needed. * **Action:** Create HyperCore Deployment File. * **Command:** ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer core-spot \ --action create \ [--oapp-config \ --token-index \ --network {testnet | mainnet} \ [--log-level { info | verbose }] ``` * ``: The index you obtained in Step 1 (or intend to use if the auction allows direct index specification). * If `--oapp-config` is provided and your OFT is defined, it can pre-fill some details. Otherwise, the SDK might prompt for OFT address and deployment transaction hash later, especially during the linking phase. * **Output:** Creates a JSON file at `./deployments/hypercore-{testnet | mainnet}/.json`. #### **Step 2.2: Set User Genesis (`userGenesis`)** Define the initial supply and distribution of your HIP-1 token on HyperCore. * **Action:** Set the genesis balances for the deployer and the users. * **Preparation:** 1. Edit the JSON file created in Step 2.1 (`./deployments/hypercore-{testnet | mainnet}/.json`). 2. Populate the `userAndWei` or `existingTokenAndWei` sections. The file should initially contain entries for the `deployer` and the `asset bridge address` (e.g., `0x2000...`), typically with `0 wei`. 3. **Crucially for the asset bridge**: To enable bridging the *entire* supply, mint the total supply (e.g., `18446744073709551615` for `u64.max`) to the **asset bridge address** corresponding to your token. You can find how to compute this address using `npx @layerzerolabs/hyperliquid-composer to-bridge --token-index `. Example snippet for the JSON: ```json wrap theme={null} "userAndWei": [ { "user": "0xAssetBridgeAddressForYourToken", // Replace with actual bridge address "wei": "18446744073709551615" // Max u64 or your total supply } ], "existingTokenAndWei": [], // Ensure this is empty if not used "blacklistUsers": [] ``` 4. If not using `existingTokenAndWei` or `userAndWei` for other users, ensure their arrays are empty (`[]`) to avoid errors like `Error deploying spot: missing token max_supply`. ```json wrap theme={null} // Change this: "existingTokenAndWei": [ { "token": 0, "wei": "" } ] // To this: "existingTokenAndWei": [] ``` * **Command:** ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer user-genesis \ --token-index \ [--action {* | userAndWei | existingTokenAndWei | blacklistUsers}] \ # Default is * (all) --network {testnet | mainnet} \ --private-key $PRIVATE_KEY_HYPERLIQUID \ [--log-level { info | verbose }] ``` * **Details:** * HyperCore HIP-1 tokens use `u64` for balances (max: `18,446,744,073,709,551,615`). Ensure total balances don't exceed this. * This step is re-runnable until Step 2.3 (Confirm User Genesis) is executed. There is no limit to the number of times you can re-run this command. * For in-depth understanding of why full funding of the asset bridge is critical, refer to [The Asset Bridge Mechanics](./hyperliquid-concepts#8-the-asset-bridge-linking-evm-spot-erc20-and-core-spot-hip-1) in Core Concepts. #### **Step 2.3: Confirm User Genesis (`setGenesis`)** This step finalizes the genesis balances set in Step 2.2, making them immutable on HyperCore. ### Warning: This action is irreversible. * **Action:** Confirm User Genesis. * **Command:** ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer set-genesis \ --token-index \ --network {testnet | mainnet} \ --private-key $PRIVATE_KEY_HYPERLIQUID \ [--log-level {info | verbose }] ``` #### **Step 2.4: Register the Spot (`registerSpot`)** This registers your Core Spot token on HyperCore and typically creates a trading pair against USDC, which is the only supported quote token as of now. * **Action:** Register Spot. * **Command:** ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer register-spot \ --token-index \ --network {testnet | mainnet} \ --private-key $PRIVATE_KEY_HYPERLIQUID \ [--log-level { info | verbose }] ``` * **Details:** * Currently, USDC is the primary quote token on HyperCore; the SDK defaults to this. * Ensure the asset bridge address on HyperCore holds the full token supply intended for bridging (as minted in Step 2.2). **Partial funding of the bridge is not supported and can lead to permanently locked tokens.** * **Verification:** You can check your deployed Core Spot token details: ```bash wrap theme={null} curl -X POST "https://api.hyperliquid-testnet.xyz/info" \ # or mainnet URL -H "Content-Type: application/json" \ -d '{"type": "tokenDetails", "tokenId": ""}' ``` * `` is the onchain identifier for your HIP-1 token (can be found via explorers or API responses). #### **Step 2.5: Register Hyperliquidity (`createSpotDeployment`)** This step creates a spot deployment without hyperliquidity, which is required for LayerZero integration. * **Action:** Create Spot Deployment. * **Command:** ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer create-spot-deployment \ --token-index \ --network {testnet | mainnet} \ --private-key $PRIVATE_KEY_HYPERLIQUID \ [--log-level {info | verbose}] ``` * **Prompts:** You will be prompted for the following values: * `startPx`: The starting price for the token. * `orderSz`: The size of each order (as a float, not wei). * `nSeededLevels`: The number of levels the deployer wishes to seed with USDC instead of tokens. You will NOT be prompted for `nOrders` as it is automatically set to 0 because LayerZero does not support Hyperliquidity. See [Hyperliquid Python SDK example](https://github.com/hyperliquid-dex/hyperliquid-python-sdk/blob/master/examples/spot_deploy.py#L97-L104) for reference. * **Details:** * There are tight range bounds on the input values that can be viewed at Hyperliquid's [frontend checks](https://hyperliquid.gitbook.io/hyperliquid-docs/hyperliquid-improvement-proposals-hips/frontend-checks#hyperliquidity). * This step can be executed after the Core Spot is fully deployed and even after linking with the EVM contract. * After completing this step, `spot-deploy-state` queries will fail, which is expected behavior. The SDK does not currently enforce the frontend checks for input validation. Ensure your values comply with Hyperliquid's requirements to avoid deployment issues. #### **Step 2.6: Set Deployer Trading Fee Share (`setDeployerTradingFeeShare`)** Configure the trading fee share for the deployer of the Core Spot token. * **Action:** Set Deployer Fee Share. * **Command:** ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer trading-fee \ --token-index \ --share \ # e.g., "100%" or "0%" --network {testnet | mainnet} \ --private-key $PRIVATE_KEY_HYPERLIQUID \ [--log-level { info | verbose }] ``` * **Details:** * A [deployer fee share](https://hyperliquid.gitbook.io/hyperliquid-docs/trading/fees) is claimed per transaction on HyperCore * Share can be `[0%, 100%]`. A `100%` share allocates the deployer's portion of trading fees to the token deployer. `0%` burns it. * The deployer address collects these fees; ensure it's secure. This step can be re-run to lower the fee share but NOT to increase it. It can also be run after the Core Spot is fully deployed, so it might be a good idea to set the fee to 100% and be able to lower it later. ### Step 3: Connect the HyperCoreSpot (HIP-1) to HyperEVM OFT (ERC20) This two-step process establishes the link that allows tokens to be bridged between HyperCore and HyperEVM via the asset bridge precompile. * **Preparation:** If you haven't used `--oapp-config` in previous steps, the SDK might prompt for your OFT contract address (on HyperEVM) and its deployment transaction hash (to get the nonce). Ensure the CoreSpot deployer has access to the OFT address. #### Step 3.1: Request EVM Contract Link (Core → EVM Intention) The Core Spot deployer initiates a request on HyperCore to link the HIP-1 token to a specific ERC20 contract on HyperEVM. * **Action:** Create Link Request. * **Performed by:** CoreSpot Deployer. * **Command:** ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer request-evm-contract \ [--oapp-config path/to/your/layerzero.config.ts] \ --token-index \ --network {testnet | mainnet} \ --private-key $PRIVATE_KEY_HYPERLIQUID \ [--log-level verbose] ``` * **Note:** This step can be re-issued multiple times (e.g., if the ERC20 address was initially incorrect) until `finalizeEvmContract` (Step 3.2) is completed. #### Step 3.2: Finalize EVM Contract Link (EVM → Core Confirmation) The OFT (ERC20) deployer on HyperEVM confirms and finalizes the link. * **Action:** Accept/Finalize Link Request. * **Performed by:** OFT Deployer (the EOA that deployed the ERC20 contract on HyperEVM). * **Command:** ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer finalize-evm-contract \ [--oapp-config path/to/your/layerzero.config.ts] \ --token-index \ --network {testnet | mainnet} \ --private-key $PRIVATE_KEY_HYPERLIQUID \ # This should be the private key of the OFT deployer on HyperEVM [--log-level verbose] ``` ### This step is final and irreversible for the given token pair. ### Step 4: Deploy the HyperLiquidComposer Contract The Composer contract facilitates the actual bridging of tokens from HyperEVM to HyperCore when receiving LayerZero messages. * **Action:** Deploy Composer. * **Performed by:** OFT Deployer. * **Command (if using the Hyperliquid example repo):** ```bash wrap theme={null} npx hardhat lz:deploy --tags MyHyperLiquidComposer --network hyperliquid_testnet # or your target network ``` * The deployment script in the example repository handles block switching (to "big blocks" and back) automatically. If deploying manually, ensure you are on a "big block". * **Funding Requirement:** * **Crucial**: The deployed `HyperLiquidComposer` contract address **must be activated on HyperCore** by sending it at least \$1 worth of `USDC` or `HYPE` on HyperCore. This is because the Composer uses `CoreWriter` precompiles to transfer tokens directly on HyperCore. * **Features:** * **Activation Checks**: The Composer verifies receiver activation using `coreUserExists()` precompile * **Bridge Capacity Validation**: Uses `spotBalance()` precompile to prevent bridge overflow scenarios * **Failed Message Recovery**: Failed compose messages can be refunded to source chains via `refundToSrc()` * **Dual Asset Support**: Handles both ERC20 tokens and native HYPE transfers in a single contract * **Notes:** * The Composer is stateless regarding individual user balances (it doesn't hold tokens long-term). * It can be deployed at any point, but it's functionally useful only after the OFT and Core Spot are deployed and linked. * It's re-deployable. If re-deployed, ensure any systems pointing to it are updated. ### Step 5: Sending Tokens (from other chains to HyperEVM/Core) After all deployments and linking are complete, you can send tokens from another network through LayerZero to a recipient on Hyperliquid. The Composer will handle the final hop to HyperCore if specified. * **Forge Script Example (from LayerZero devtools):** Ensure your `.env` is populated with `PRIVATE_KEY`, `RPC_URL_BSC_TESTNET` (or your source chain RPC). ```bash wrap theme={null} forge script script/SendScript.s.sol \ --private-key $PRIVATE_KEY \ --rpc-url $RPC_URL_SOURCE_CHAIN \ --sig "exec(uint256,uint128,uint128)" \ \ # Amount of OFT to send in local decimals \ # Gas to forward for HyperCore L1 action (e.g., 100000). If > 0, attempts to send to HyperCore. \ # Value (in HYPE) to send to fund user on HyperCore (e.g., 0). --broadcast ``` * The `SendScript.s.sol` (or your custom sending logic) would prepare a `SendParam` where: * `SendParam.dstEid` points to Hyperliquid. * `SendParam.to` is the OFT address on Hyperliquid. * `SendParam.composeMsg` is `abi.encode(uint256 minMsgValue, address actualReceiverAddressOnHyperliquid)`. * `SendParam.extraOptions` might be used to specify gas for the `lzCompose` call and the subsequent L1 action. ### Error Handling & Recovery The `HyperLiquidComposer` implementation includes comprehensive error handling and recovery mechanisms: #### **Precompile-Based Validation** * **Activation Check**: Uses `coreUserExists()` precompile to verify receiver is activated before attempting transfers * **Bridge Capacity Check**: Uses `spotBalance()` precompile to validate sufficient bridge capacity * **Gas Protection**: Enforces minimum gas requirements (`MIN_GAS()` / `MIN_GAS_WITH_VALUE()`) to prevent execution failures #### **Error Recovery Mechanisms** * **Failed Message Storage**: Messages that fail decoding are stored in `failedMessages[guid]` mapping * **Crosschain Refunds**: Failed messages can be refunded to source chain using `refundToSrc(bytes32 guid)` * **HyperEVM Refunds**: Failed transfers automatically refund tokens to receiver on HyperEVM * **Fallback Protection**: Multiple fallback mechanisms prevent token locking scenarios #### **Dual Asset Support** * **ERC20 Tokens**: Handles OFT token transfers with decimal scaling * **Native HYPE**: Supports native token transfers alongside ERC20 transfers * **Combined Operations**: Can transfer both ERC20 and HYPE in a single compose operation #### **Error Scenarios & Handling** 1. **Malformed Message**: Stored in `failedMessages` for crosschain refund 2. **Inactive Receiver**: Transaction reverts with `CoreUserNotActivated()` error 3. **Insufficient Bridge Capacity**: Transaction reverts to prevent token locking 4. **Gas Exhaustion**: Protected by minimum gas requirements 5. **Precompile Failures**: Automatic refund to HyperEVM receiver address The implementation provides strong safety guarantees while maintaining compatibility with LayerZero OFT workflows. # LayerZero Hyperliquid SDK - Command Reference Source: https://docs.layerzero.network/v2/developers/hyperliquid/hyperliquid-sdk This section provides a reference for the CLI commands available through the `@layerzerolabs/hyperliquid-composer`. LayerZero enables secure crosschain... This section provides a reference for the CLI commands available through the `@layerzerolabs/hyperliquid-composer` SDK. Explanations and examples can be found in the [Hyperliquid OFT Deployment Guide](./hyperliquid-oft-deployment). To view all commands and their options, run: ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer -h ``` ### 1. Type Conversions #### Compute the asset bridge address for a Core Spot token ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer to-bridge --token-index ``` ### 2. Reading Core Spot State #### List Core Spot metadata ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer core-spot \ --action get \ --token-index \ --network {testnet | mainnet} \ [--log-level {info | verbose}] ``` #### Create a deployment file for Core Spot deployment ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer core-spot \ --action create \ [--oapp-config ] \ --token-index \ --network {testnet | mainnet} \ [--log-level {info | verbose}] ``` #### Get a HIP-1 Token's information ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer hip-token \ --token-index \ --network {testnet | mainnet} \ [--log-level {info | verbose}] ``` #### View a deployment state ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer spot-deploy-state \ --token-index \ --network {testnet | mainnet} \ --deployer-address <0x> \ [--log-level {info | verbose}] ``` ### 3. Switching Blocks (`evmUserModify`) ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer set-block \ --size {small | big} \ --network {testnet | mainnet} \ --private-key $PRIVATE_KEY \ [--log-level {info | verbose}] ``` ### 4. Deploying a CoreSpot (`spotDeploy`) #### 4.1 `userGenesis` ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer user-genesis \ --token-index \ [--action {* | userAndWei | existingTokenAndWei | blacklistUsers}] \ --network {testnet | mainnet} \ --private-key $PRIVATE_KEY_HYPERLIQUID \ [--log-level {info | verbose}] ``` #### 4.2 `genesis` ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer set-genesis \ --token-index \ --network {testnet | mainnet} \ --private-key $PRIVATE_KEY_HYPERLIQUID \ [--log-level {info | verbose}] ``` #### 4.3 `registerSpot` ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer register-spot \ --token-index \ --network {testnet | mainnet} \ --private-key $PRIVATE_KEY_HYPERLIQUID \ [--log-level {info | verbose}] ``` #### 4.4 `createSpotDeployment` ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer create-spot-deployment \ --token-index \ --network {testnet | mainnet} \ --private-key $PRIVATE_KEY_HYPERLIQUID \ [--log-level {info | verbose}] ``` #### 4.5 `setDeployerTradingFeeShare` ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer trading-fee \ --token-index \ --share <[0%,100%]> \ --network {testnet | mainnet} \ --private-key $PRIVATE_KEY_HYPERLIQUID \ [--log-level {info | verbose}] ``` ### 5. Linking HyperEVM (OFT) and HyperCore (HIP-1) #### 5.1 `requestEvmContract` ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer request-evm-contract \ [--oapp-config ] \ --token-index \ --network {testnet | mainnet} \ --private-key $PRIVATE_KEY_HYPERCORE_DEPLOYER \ # CoreSpot Deployer's key [--log-level verbose] ``` #### 5.2 `finalizeEvmContract` ```bash wrap theme={null} npx @layerzerolabs/hyperliquid-composer finalize-evm-contract \ [--oapp-config ] \ --token-index \ --network {testnet | mainnet} \ --private-key $PRIVATE_KEY_HYPEREVM_DEPLOYER \ # OFT Deployer's key (on HyperEVM) [--log-level verbose] ``` # DVN and Executor Configuration Source: https://docs.layerzero.network/v2/developers/iota/configuration/dvn-executor-config Step-by-step guide to dvn and executor configuration using LayerZero V2. Build and deploy omnichain applications with crosschain messaging. Follow step-by-s... This guide explains how to configure Decentralized Verifier Networks (DVNs), Executors, and message libraries for your IOTA OApp or OFT using the SDK. **Production deployments should use multiple required DVNs from independent operators.** A single-DVN configuration means a compromise of that one verifier results in unrestricted forged messages on the pathway. The examples on this page show the LayerZero Labs DVN with a `` placeholder so the snippet does not silently model a single-DVN production setup. Replace `` with a real non-LayerZero-Labs provider — see [DVN Addresses](/v2/deployments/dvn-addresses) for available providers per chain. See the [Integration Checklist](/v2/tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway) for production DVN guidance. ## Overview Configuration is done through the OApp SDK instance. All examples use `sdk.getOApp(packageId)` to get the OApp instance, then call SDK methods for configuration. **Configuration flow**: 1. Set message libraries (optional) 2. Configure DVNs for send/receive (optional but recommended) 3. Set enforced options (optional) 4. Set peer addresses (required - opens pathway, call last!) ## SDK Setup ```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); // Always use SDK factory ``` ## Configuration Methods ### Set Peer ```typescript wrap theme={null} // Configure peer for destination chain await oapp.setPeerMoveCall(tx, dstEid, peerBytes32); ``` **Address format**: Use package ID for IOTA peers, 32-byte address for other chains. ### Set Message Libraries ```typescript wrap theme={null} // Set custom send library await oapp.setSendLibraryMoveCall(tx, dstEid, libraryAddress); // Set custom receive library await oapp.setReceiveLibraryMoveCall(tx, srcEid, libraryAddress, gracePeriod); ``` **Default**: Uses Endpoint defaults if not configured. ## DVN Configuration ### Configure Receive DVN (Inbound) ```typescript wrap theme={null} import { SDK, OAppUlnConfigBcs, PACKAGE_ULN_302_ADDRESS, OBJECT_ULN_302_ADDRESS, PACKAGE_DVN_LAYERZERO_ADDRESS, } 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); // Encode configuration const config = OAppUlnConfigBcs.serialize({ use_default_confirmations: false, use_default_required_dvns: false, use_default_optional_dvns: true, uln_config: { confirmations: 15, // Replace with a non-LayerZero-Labs DVN; see /v2/deployments/dvn-addresses required_dvns: [ PACKAGE_DVN_LAYERZERO_ADDRESS[Stage.MAINNET], PACKAGE_DVN__ADDRESS[Stage.MAINNET], ], optional_dvns: [], optional_dvn_threshold: 0, }, }).toBytes(); // Two-step Call pattern const tx = new Transaction(); const configCall = await oapp.setConfigMoveCall( tx, PACKAGE_ULN_302_ADDRESS[Stage.MAINNET], 30184, // Remote EID 3, // CONFIG_TYPE_RECEIVE_ULN config, ); tx.moveCall({ target: `${PACKAGE_ULN_302_ADDRESS[Stage.MAINNET]}::uln_302::set_config`, arguments: [tx.object(OBJECT_ULN_302_ADDRESS[Stage.MAINNET]), configCall], }); await client.signAndExecuteTransaction({transaction: tx, signer: keypair}); ``` ### Configure Send DVN (Outbound) ```typescript wrap theme={null} // Same pattern, use CONFIG_TYPE_SEND_ULN = 2 const configCall = await oapp.setConfigMoveCall( tx, PACKAGE_ULN_302_ADDRESS[Stage.MAINNET], 30184, 2, // CONFIG_TYPE_SEND_ULN config, ); ``` **Config types**: `1` = Executor, `2` = Send ULN, `3` = Receive ULN **DVN addresses**: Use `PACKAGE_DVN_LAYERZERO_ADDRESS[Stage.MAINNET]` or see [Deployed Contracts](/v2/deployments/chains/iota). ### Set Enforced Options ```typescript wrap theme={null} import {Options} from '@layerzerolabs/lz-v2-utilities'; const options = Options.newOptions() .addExecutorLzReceiveOption(60000, 0) // Gas for destination .toBytes(); await oapp.setEnforcedOptionsMoveCall(tx, dstEid, msgType, options); ``` ## Gas Limit Recommendations Based on gas profiling: ### OApp/OFT Operations | Operation | Gas Used (IOTA) | Recommended Budget | Notes | | ------------ | --------------- | ------------------ | --------------------------------------------------------------------- | | `lz_receive` | 2,000-4,172 | 3,500-5,000 | For OApps and custom business logic, this needs independent profiling | | `oft_send` | 4,728,620 | 6,700,000 | Includes endpoint + ULN | | `dvn_verify` | 5,684,108 | 7,700,000 | Verification submission | | `dvn_commit` | 517,248 | 2,500,000 | Commit verification | ### Enforced Options Examples For EVM destinations: ```typescript wrap theme={null} import {Options} from '@layerzerolabs/lz-v2-utilities'; // Standard OApp message const options = Options.newOptions() .addExecutorLzReceiveOption(60000, 0) // 60k gas, no msg.value .toBytes(); // OFT with compose const optionsCompose = Options.newOptions() .addExecutorLzReceiveOption(200000, 0) // Higher for compose .toBytes(); ``` For IOTA destinations: ```typescript wrap theme={null} const optionsForIOTA = Options.newOptions() .addExecutorLzReceiveOption(5000, 0) // 5k gas units, no msg.value .toBytes(); ``` **Note**: Based on gas profiling, IOTA `lz_receive` uses 2,000-5,000 gas units. No msg.value needed - IOTA handles storage internally. ## Common Issues **Errors**: * `InvalidBCSBytes` → Use `OAppUlnConfigBcs.serialize()` for DVN config * `oapp_registry::get_messaging_channel abort code: 1` → Used object ID instead of package ID as peer * Channel not initialized → Registration creates MessagingChannel automatically (no manual init needed) ## Next Steps * [OApp Overview](/v2/developers/iota/oapp/overview) - Base messaging standard * [OFT Overview](/v2/developers/iota/oft/overview) - Token standard and deployment * [OFT SDK](/v2/developers/iota/oft/sdk) - Complete SDK methods and examples * [Technical Overview](/v2/developers/iota/technical-overview) - IOTA fundamentals and architecture * [Protocol Overview](/v2/developers/iota/protocol-overview) - Complete message workflows * [Troubleshooting](/v2/developers/iota/troubleshooting/common-errors) - Common configuration issues # Getting Started with LayerZero V2 on IOTA L1 Source: https://docs.layerzero.network/v2/developers/iota/getting-started Get started with Getting Started with on IOTA L1. Step-by-step tutorial for building omnichain applications on LayerZero V2. LayerZero enables secure... Any data, whether it's a fungible token transfer, an NFT, or some other smart contract input can be encoded onchain as bytes and delivered to a destination chain to trigger some action using LayerZero. Because of this, any blockchain that broadly supports state propagation and events can be connected to LayerZero, including **IOTA L1**. If you're new to LayerZero, we recommend reviewing [**"What is LayerZero?"**](/v2/concepts/getting-started/what-is-layerzero) before continuing.
LayerZero provides [**IOTA L1 Move Packages**](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/iota/contracts) that can communicate with the equivalent [Solidity Contract Libraries](/v2/developers/evm/overview) and [Solana Programs](/v2/developers/solana/overview) deployed on other chains. These packages, like their Solidity and Rust counterparts, simplify calling the [LayerZero Endpoint](../../concepts/protocol/layerzero-endpoint), provide message handling, interfaces for protocol configurations, and other utilities for interoperability: * **Omnichain Fungible Token (OFT)**: extends OApp with functionality for handling omnichain token transfers using IOTA L1's coin framework. * **Omnichain Application (OApp)**: the base package utilities for omnichain messaging and configuration. Each of these package standards implements common functions for **sending** and **receiving** omnichain messages. ## Differences from the Ethereum Virtual Machine The full differences between Solidity/EVM and IOTA L1/Move are significant. For comprehensive guides, see: * [IOTA Documentation](https://docs.iota.org/) * [Move Book](https://move-book.com/) * [IOTA Developer Basics](https://docs.iota.org/developer/iota-101) Skip this section if you already feel comfortable working with the IOTA L1 blockchain and its object model. ### Object Model vs Account Model The most fundamental difference is how state is organized: **EVM (Account Model)**: ```rust wrap theme={null} Account { address: 0x123... balance: 100 ETH storage: { slot_0: value_0, slot_1: value_1, ... } code: bytecode } ``` All state lives in storage slots within the account. Functions modify these slots. **IOTA (Object Model)**: ```rust wrap theme={null} Object { id: UID (globally unique) owner: Address | Shared | Immutable type: Module::StructName fields: { field_1: value_1, field_2: value_2, ... } } ``` State lives in individual objects. Functions take objects as parameters and modify them. ### Writing Smart Contracts on IOTA To create a new ERC20 token on an EVM-compatible blockchain, a developer inherits and redeploys the ERC20 contract: ```solidity wrap theme={null} // EVM: Inherit and deploy contract MyToken is ERC20 { constructor() ERC20("MyToken", "MTK") {} } ``` **IOTA is different.** Instead of inheritance, IOTA uses: 1. **[Packages](https://docs.iota.org/developer/iota-101/move-overview/package-upgrades/introduction)**: Published Move code (immutable) 2. **[Objects](https://docs.iota.org/developer/iota-101/objects/object-model)**: State containers with unique IDs 3. **[Capabilities](https://docs.iota.org/developer/iota-101/move-overview/patterns/capabilities)**: Authorization objects Rather than deploying a new contract, you publish a package once, then create object instances. **One-Time Witness Pattern**: IOTA uses the [one-time witness (OTW)](https://docs.iota.org/developer/iota-101/move-overview/one-time-witness) pattern to prove code runs exactly once during package initialization: ```rust wrap theme={null} /// IOTA: One-time witness pattern /// Struct name must match module name in ALL_CAPS public struct MY_TOKEN has drop {} // Only `drop` ability /// Called once when package is published fun init(otw: MY_TOKEN, ctx: &mut TxContext) { // Create coin with metadata // The `otw` parameter can only be created once by the runtime let (treasury_cap, coin_metadata) = coin::create_currency( otw, // Proves this is the first/only call 9, // Decimals b"MTK", // Symbol b"MyToken", // Name b"My token", // Description option::none(), ctx ); // CoinMetadata automatically frozen (immutable) // TreasuryCap transferred to deployer (can mint/burn) transfer::public_transfer(treasury_cap, ctx.sender()); } ``` **Key Differences**: * **No redeploy**: Package is published once, objects created many times * **No inheritance**: Use composition and capabilities instead * **Object ownership**: State has explicit ownership (address, shared, immutable) * **Type safety**: Move's type system prevents many runtime errors ### Object Ownership Types IOTA's ownership model determines who can access and modify objects. Understanding these types is essential for building LayerZero applications: | Ownership | Access | Example | LayerZero Usage | | -------------------------------------------------------------------------------------------- | -------------------- | --------------------- | ---------------------- | | [**Owned**](https://docs.iota.org/developer/iota-101/objects/object-ownership/address-owned) | Only owner can use | `AdminCap`, `CallCap` | Authorization objects | | [**Shared**](https://docs.iota.org/developer/iota-101/objects/object-ownership/shared) | Anyone can reference | `OApp`, `EndpointV2` | Protocol state objects | | [**Immutable**](https://docs.iota.org/developer/iota-101/objects/object-ownership/immutable) | Anyone can read | `CoinMetadata` | Published packages | **OApp on IOTA**: ```rust wrap theme={null} /// Shared OApp configuration object /// Contains peer configuration and enforced options for crosschain messaging /// The delegate (authorized by the OApp owner) can update these settings public struct OApp has key { id: UID, oapp_cap: CallCap, // Embedded capability for authentication admin_cap: address, // Reference to owned AdminCap for admin operations peer: Peer, // Embedded peer config (trusted remote OApp addresses) // ... } // Create and share let oapp = OApp { /* ... */ }; transfer::share_object(oapp); // Now accessible to everyone /// Owned object - only owner can use public struct AdminCap has key, store { id: UID, } // Transfer to admin transfer::public_transfer(admin_cap, admin_address); ``` ### Capabilities vs msg.sender **What are Capabilities?** [Capabilities](https://docs.iota.org/developer/iota-101/move-overview/patterns/capabilities) are special owned objects that grant specific permissions. Owning a capability object proves you have authorization to perform certain operations. **EVM Authorization** uses `msg.sender`: ```solidity wrap theme={null} // EVM: Check caller modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; } function setConfig() external onlyOwner { // only owner can call } ``` **IOTA Authorization** uses capability objects: ```rust wrap theme={null} // IOTA: Require capability object public fun set_config( oapp: &mut OApp, admin_cap: &AdminCap, // Must own this object to call config: Config, ) { // Owning AdminCap proves authorization // No need to check msg.sender oapp.config = config; } ``` **Benefits**: * **Transferable**: Can give capabilities to other addresses * **Composable**: Capabilities can be stored in other objects * **Type-safe**: Different capabilities for different permissions * **No spoofing**: Can't fake capability ownership ### Programmable Transaction Blocks While EVM executes one function call per transaction, IOTA enables complex multi-step workflows in a single atomic transaction: ```solidity wrap theme={null} // EVM: Separate transactions tx1: token.approve(spender, amount); tx2: spender.transferFrom(user, recipient, amount); tx3: recipient.stake(amount); ``` **IOTA uses [Programmable Transaction Blocks (PTBs)](https://docs.iota.org/developer/iota-101/transactions/ptb/programmable-transaction-blocks)** - up to [1,024 commands](https://docs.iota.org/developer/iota-101/transactions/ptb/programmable-transaction-blocks#transaction-type) in one atomic transaction: ```typescript wrap theme={null} const tx = new Transaction(); // All in one atomic transaction: tx.moveCall({ target: `${pkg}::token::approve`, ... }); tx.moveCall({ target: `${pkg}::spender::transfer_from`, ... }); tx.moveCall({ target: `${pkg}::staking::stake`, ... }); await client.signAndExecuteTransaction({ transaction: tx }); ``` **For LayerZero**: * Quote fees * Send message * Route through Endpoint/ULN/Workers * Confirm and extract receipt * All in one PTB, atomically ### No Dynamic Dispatch (Call Pattern) EVM can dynamically call contracts: ```solidity wrap theme={null} // EVM: delegatecall allows dynamic invocation contract Endpoint { function lzReceive(address oapp, ...) { // Call back into OApp without knowing it at compile time (bool success, ) = oapp.delegatecall( abi.encodeWithSignature("_lzReceive(...)", ...) ); } } ``` **IOTA has no dynamic dispatch.** Instead, LayerZero uses the **Call pattern**: ```rust wrap theme={null} /// Call object (hot potato - must be consumed) public struct Call { // Has NO drop or store ability // Must be explicitly destroyed } // Endpoint creates Call targeting OApp public fun lz_receive(...): Call { call::create(executor_cap, oapp_address, true, param, ctx) } // OApp must destroy Call to process public fun lz_receive(oapp: &mut OApp, call: Call) { let (callee, param, _) = call.destroy(&oapp.oapp_cap); // Validate and process... } ``` The `Call` object has **no abilities at all**: * No `drop` ability → Can't be ignored (must be consumed) * No `store` ability → Can't be saved in structs * No `copy` ability → Can't be forged or copied (prevents reentrancy) * No `key` ability → Can't be stored globally in the ledger This lack of abilities enforces the hot potato pattern - the `Call` must be explicitly destroyed before the transaction ends, routing through the PTB to the destination module. This achieves similar functionality to dynamic dispatch while maintaining type safety and preventing reentrancy attacks. ## Prerequisites Before you start building, you'll need to set up your development environment. ### Install IOTA CLI Install the IOTA CLI from the official repository: ```bash wrap theme={null} # Install IOTA CLI (uses sui binary) cargo install --locked --git https://github.com/iotaledger/iota.git --branch main sui ``` Verify installation: ```bash wrap theme={null} sui --version # sui 1.54.1-... or later (IOTA uses Sui binary) ``` ### Install Node.js and TypeScript SDK For PTB construction and SDK usage in your OApp/OFT project: ```bash wrap theme={null} # Install as project dependencies (not global) # Note: IOTA uses the same Sui SDKs as it's built on Sui's Move implementation npm install @iota/iota-sdk @layerzerolabs/lz-iotal1-sdk-v2 @layerzerolabs/lz-iotal1-oft-sdk-v2 ``` These packages are required for building PTBs, configuring your OApp, and interacting with deployed contracts. ### Set Up IOTA Wallet Create or import a wallet: ```bash wrap theme={null} # Create new wallet iota client new-address ed25519 # Or import existing iota client import ``` ### Get Testnet IOTA For testing on IOTA testnet, see [IOTA Faucet documentation](https://docs.iota.org/developer/getting-started/get-coins): ```bash wrap theme={null} # Switch to testnet iota client switch --env testnet # Get IOTA from faucet curl --location --request POST 'https://faucet.testnet.iota.cafe/gas' \ --header 'Content-Type: application/json' \ --data-raw '{ "FixedAmountRequest": { "recipient": "" } }' ``` ## Understanding Package IDs vs Object IDs One of the most important concepts for LayerZero on IOTA is the distinction between package IDs and object IDs: | Type | What It Is | When to Use | Example | | -------------- | ------------------------------------- | ------------------------------------- | --------------- | | **Package ID** | Address of published code (immutable) | Move call targets, **peer addresses** | `0x061a47bf...` | | **Object ID** | Address of object instance (state) | Function arguments via `tx.object()` | `0xf1ab4be...` | **Finding Package ID**: ```bash wrap theme={null} # From object type field iota client object --json | jq '.data.type' # Returns: "0xPACKAGE_ID::module::StructName" ``` **Critical for LayerZero**: * **Peer addresses = Package ID** (where code is deployed) * **Not Object ID** (instance of OApp/OFT) See [Peer Address Configuration](/v2/developers/iota/oapp/overview#step-7-set-peer-address) for details. For general peer concepts, see [Peer in Glossary](/v2/concepts/glossary#peer). ### Understanding the Registry System When you deploy and register an OApp with the LayerZero Endpoint, understanding the registry architecture is crucial: **What happens during registration**: 1. **Endpoint stores your package ID** in its registry (not object ID) 2. **MessagingChannel.oapp field** = your package ID 3. **Remote chains send messages** to your package ID 4. **Endpoint looks up package ID** → finds your MessagingChannel → routes message **Example deployment flow**: ```bash wrap theme={null} # 1. Deploy your OApp package iota client publish --gas-budget 1000000000 # Output includes: # - Package ID: 0x061a47bf... (your code location) # - OApp Object ID: 0x242952... (instance of OApp) # 2. Register with Endpoint # Endpoint stores: registry[0x061a47bf...] = MessagingChannel # 3. Remote chain configuration # Remote chain must use: peer = 0x061a47bf... (package ID) ``` **This is why peers must be package IDs.** ### CallCap and Package Identity LayerZero OApps use **Package CallCaps** (not Individual CallCaps): ```rust wrap theme={null} // From call_cap module public enum CapType { Individual, // ID = object's UID address Package(address), // ID = package address ← OApps use this } // When OApp calls callCap.id(): // Returns the package address, not the object UID! ``` **Impact on LayerZero**: * `callCap.id()` returns your package address * Registry keys by this package address * All lookups expect package address * Remote chains must use this as peer address **Finding your package ID from an object**: ```bash wrap theme={null} # Method 1: From object type iota client object 0x242952... --json | jq '.data.type' # Output: "0x061a47bf...::oapp::OApp" # ^^^^^^^^^^^^ # This is your package ID # Method 2: From publish output # Look for "packageId" in the transaction result ``` **Common errors when using wrong ID**: | Error | Cause | Fix | | ---------------------------------------------------- | -------------------------- | ---------------------------- | | `oapp_registry::get_messaging_channel abort code: 1` | Used object ID as peer | Use package ID instead | | `oapp_registry::get_oapp_info abort code: 1` | Registry lookup failed | Ensure OApp is registered | | Message delivery fails | Peer not found in registry | Verify package ID is correct | ## Next Steps Choose your path: ### Build an OApp For custom crosschain logic: * [OApp Overview](/v2/developers/iota/oapp/overview) - Architecture and patterns * [OApp Protocol Details](/v2/developers/iota/protocol-overview) - Deep technical dive * [Technical Overview](/v2/developers/iota/technical-overview) - IOTA fundamentals ### Build an OFT For crosschain tokens: * [OFT Overview](/v2/developers/iota/oft/overview) - Token architecture * [OFT SDK](/v2/developers/iota/oft/sdk) - TypeScript SDK integration and methods * [Configuration Guide](/v2/developers/iota/configuration/dvn-executor-config) - Security and DVN setup ### Understand the Protocol For protocol-level understanding: * [Technical Overview](/v2/developers/iota/technical-overview) - VM architecture and Call pattern * [Protocol Overview](/v2/developers/iota/protocol-overview) - Complete message workflows * [OFT SDK](/v2/developers/iota/oft/sdk) - Available SDK methods and patterns ### Get Help * [Troubleshooting](/v2/developers/iota/troubleshooting/common-errors) - Common issues * [FAQ](/v2/developers/iota/troubleshooting/faq) - Frequently asked questions * [Discord](https://discord.com/invite/ktbvm8Nkcr) - Community support # LayerZero IOTA L1 OApp Source: https://docs.layerzero.network/v2/developers/iota/oapp/overview Overview of IOTA L1 OApp on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure crosschain messaging. The OApp Standard provides developers with a generic message passing interface to send and receive arbitrary pieces of data between contracts existing on different blockchain networks. How the data is interpreted and what actions it triggers depend on the specific OApp implementation. ## What is an OApp on IOTA L1? An OApp on IOTA is a [Move package](https://docs.iota.org/developer/iota-101/move-overview/package-upgrades/introduction) that integrates with the LayerZero protocol to enable crosschain messaging. Unlike EVM OApps that inherit base contracts, IOTA OApps use [shared objects](https://docs.iota.org/developer/iota-101/objects/object-ownership/shared) and explicit function calls within [Programmable Transaction Blocks (PTBs)](https://docs.iota.org/developer/iota-101/transactions/ptb/programmable-transaction-blocks). ### Differences from EVM OApps | Aspect | EVM | IOTA | | ---------------------- | --------------------------------------------- | ---------------------------------------------- | | **Code Organization** | Solidity contracts | Move packages containing modules | | **Integration Method** | Inherit from `OApp` base contract | Use `oapp` package and `Call` pattern | | **Receive Flow** | Endpoint calls `lzReceive` via `delegatecall` | Endpoint creates `Call` object for OApp module | | **Validation** | Implicit via inheritance | Explicit via `CallCap` validation | | **State Model** | Contract storage slots | Shared objects with struct fields | | **OApp Identity** | Contract address | Shared `OApp` object ID | | **Authorization** | `msg.sender` and modifiers | Capability objects (`CallCap`, `AdminCap`) | ## Installation ### Prerequisites * [IOTA CLI](https://docs.iota.org/developer/references/cli) installed (version 1.54.1 or later) * Basic understanding of [Move programming](https://docs.iota.org/developer/iota-101/move-overview/move-overview) * Familiarity with [IOTA's object model](https://docs.iota.org/developer/iota-101/objects/object-model) ### Create a New Project Create a new IOTA Move package: ```bash wrap theme={null} mkdir my-oapp cd my-oapp iota move new my_oapp ``` This creates: ``` my_oapp/ ├── Move.toml ├── sources/ └── tests/ ``` ### Configure Move.toml ### Git Dependencies Not Supported Git dependencies for LayerZero packages currently do not work due to missing Move.toml manifests in subdirectories. Use **local dependencies** instead. **Clone LayerZero Repository**: ```bash wrap theme={null} cd .. git clone https://github.com/LayerZero-Labs/LayerZero-v2.git cd my-oapp ``` **Update `Move.toml` with local paths**: ```toml wrap theme={null} [package] name = "my_oapp" version = "0.0.1" edition = "2024.beta" [dependencies] IOTA = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "mainnet" } # LayerZero packages - use local paths OApp = { local = "../LayerZero-v2/packages/layerzero-v2/iota/contracts/oapps/oapp" } EndpointV2 = { local = "../LayerZero-v2/packages/layerzero-v2/iota/contracts/endpoint-v2" } Call = { local = "../LayerZero-v2/packages/layerzero-v2/iota/contracts/dynamic-call/call" } Utils = { local = "../LayerZero-v2/packages/layerzero-v2/iota/contracts/utils" } [addresses] my_oapp = "0x0" ``` **Alternative: Use Published Package Addresses** If LayerZero packages are published onchain, you can reference them by address: ```toml wrap theme={null} [dependencies] IOTA = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "mainnet" } # Reference by published address (check deployments page for current addresses) OApp = { address = "0xfdc28afc0110cb2edb94e3e57f2b1ce69b5a99c503b06d15e51cfa212de56e24" } # ... other packages [addresses] my_oapp = "0x0" ``` See [Deployed Contracts](/v2/deployments/chains/iota) for current mainnet package addresses. *** ## Working Example: OFT Implementation The **Omnichain Fungible Token (OFT)** is a complete, production-ready implementation of an OApp that demonstrates all core messaging patterns. OFTs extend OApp functionality to enable crosschain token transfers. **To see a working OApp in action**, review the [OFT Overview](/v2/developers/iota/oft/overview) which shows: * Complete initialization and deployment workflow * Message encoding/decoding patterns * Integration with IOTA's object model and coin framework * Production deployment examples with TypeScript SDK **Source Code References**: * [oapp.move](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/iota/contracts/oapps/oapp/sources/oapp.move) - Base OApp implementation * [oft.move](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/iota/contracts/oapps/oft/oft/sources/oft.move) - OFT extending OApp with token logic ## How OApp Messaging Works Understanding how OApps work on IOTA requires understanding several key concepts that differ from EVM implementations. ### Initialization: Creating Your OApp When you initialize an OApp on IOTA, the `oapp::new()` function creates three critical components using the [one-time witness (OTW) pattern](https://docs.iota.org/developer/iota-101/move-overview/one-time-witness): 1. **OApp Shared Object**: Contains configuration state (peers, enforced options) that anyone can read but only admins can modify 2. **CallCap** (owned): Proves ownership of the OApp and is required for all messaging operations 3. **AdminCap** (owned): Grants authority for administrative operations like setting peers and configuring options **Key Point**: The OApp object is automatically made shared (via `transfer::share_object()`), while the capabilities are transferred to the deployer. This separation allows secure access control using IOTA's capability-based authorization system rather than address-based checks. ### Registration: Connecting to the Endpoint After initialization, your OApp must register with the LayerZero Endpoint by calling `endpoint_v2::register_oapp()`. This creates a dedicated `MessagingChannel` shared object that stores your OApp's message state and nonce tracking for each [pathway](/v2/concepts/glossary#channel--lossless-channel). **What the registry stores**: The Endpoint registry maps your **package ID** (not object ID) to your MessagingChannel. This is critical because IOTA OApps use Package CallCaps, which identify by package address. ### Peer Configuration: Establishing Trust [Peers](/v2/concepts/glossary#peer) are trusted OApp addresses on remote chains that are authorized to send messages to your OApp. You configure peers by calling `oapp::set_peer()` with the AdminCap: **Critical: Package ID vs Object ID** On IOTA, peers must be configured using **package IDs**, not object IDs: * **Your IOTA OApp**: Use the package ID published address as the peer on remote chains * **Remote OApp peers**: Use their contract/package addresses (EVM contract address, Solana program ID, etc.) This is because LayerZero's registry and verification systems key by package address for IOTA OApps. Using an object ID will cause the error: `oapp_registry::get_messaging_channel abort code: 1`. ### Sending Messages: The Call Pattern When your OApp sends a message, it creates a **Call object** using `oapp::lz_send()`. This Call object is a "hot potato" - it has no `drop` or `store` abilities, meaning it **must** be consumed before the transaction ends. **The Send Flow**: 1. Your OApp calls `lz_send()` → creates `Call` 2. Call is routed through a PTB to: Endpoint → ULN302 → DVNs & Executor 3. Each component processes and returns the Call 4. Your OApp calls `confirm_lz_send()` to extract the receipt and finalize **Why the Call pattern?** IOTA Move lacks dynamic dispatch (like EVM's `delegatecall`). The Call pattern achieves similar routing functionality while maintaining type safety and preventing reentrancy attacks through Move's ability system. **How OFT Implements Custom Send Logic**: ```rust wrap theme={null} // From oft.move - shows how custom business logic wraps OApp messaging public fun send( self: &mut OFT, oapp: &mut OApp, sender: &OFTSender, send_param: &SendParam, coin_provided: &mut Coin, native_coin_fee: Coin, zro_coin_fee: Option>, refund_address: Option
, clock: &Clock, ctx: &mut TxContext, ): (Call, OFTSendContext) { // 1. Custom business logic: Validate state self.assert_upgrade_version(); self.pausable.assert_not_paused(); // 2. Custom business logic: Debit tokens (burn or escrow) let (amount_sent_ld, amount_received_ld) = self.debit( coin_provided, send_param.dst_eid(), send_param.amount_ld(), send_param.min_amount_ld(), ctx, ); // 3. Custom business logic: Apply rate limits self.inbound_rate_limiter.release_rate_limit_capacity(/*...*/); self.outbound_rate_limiter.try_consume_rate_limit_capacity(/*...*/); // 4. Custom business logic: Build OFT-specific message (recipient + amount) let (message, options) = self.build_msg_and_options(/*...*/); // 5. Call base OApp send functionality let ep_call = oapp.lz_send( &self.oft_cap, // Prove OFT owns this OApp send_param.dst_eid(), // Destination chain message, // Encoded OFT message options, // Execution options native_coin_fee, // Fee payment zro_coin_fee, refund_address, ctx, ); // 6. Return Call and context for confirmation (ep_call, send_context) } ``` This pattern shows how your custom OApp would: 1. Add application-specific validation and state changes 2. Encode your business logic into the message payload 3. Call the base `oapp::lz_send()` function 4. Return the Call for PTB routing **Sequential vs Parallel Sends**: * `lz_send()` + `confirm_lz_send()`: Enforces sequential execution (one send at a time) * `lz_send_and_refund()`: Allows parallel sends in the same PTB (messages can be reordered) ### Receiving Messages: Validation and Processing When a message arrives on IOTA, the Executor calls `endpoint_v2::lz_receive()`, which creates a `Call` object targeting your OApp. Your OApp's `lz_receive()` function must: 1. **Validate the CallCap**: Ensure the Call belongs to this OApp 2. **Check the Endpoint**: Verify the Call came from the authorized LayerZero Endpoint 3. **Verify the Peer**: Confirm the sender matches your configured peer for that source chain 4. **Process the message**: Decode and execute your custom business logic **How OFT Implements Custom Receive Logic**: ```rust wrap theme={null} // From oft.move - shows validation + custom business logic public fun lz_receive( self: &mut OFT, oapp: &OApp, call: Call, clock: &Clock, ctx: &mut TxContext, ) { // 1. Custom business logic: Pre-receive validation self.assert_upgrade_version(); self.pausable.assert_not_paused(); // 2. Base OApp validation (CallCap, Endpoint, Peer) // Returns validated LzReceiveParam let param = oapp.lz_receive(&self.oft_cap, call); // 3. Custom business logic: Decode OFT message let oft_msg = oft_msg_codec::decode(param.message()); let recipient = oft_msg.send_to(); let amount_received_sd = oft_msg.amount_sd(); // 4. Custom business logic: Convert from shared to local decimals let amount_received_ld = self.sd_to_ld(amount_received_sd); // 5. Custom business logic: Credit tokens (mint or release from escrow) let coin_credited = self.credit(amount_received_ld, ctx); // 6. Custom business logic: Apply rate limits self.inbound_rate_limiter.try_consume_rate_limit_capacity( param.src_eid(), amount_received_ld, clock, ); // 7. Custom business logic: Transfer tokens to recipient transfer::public_transfer(coin_credited, recipient); // 8. Emit event for tracking event::emit(OFTReceivedEvent { /* ... */ }); } ``` This pattern shows how your custom OApp would: 1. Delegate security validation to `oapp.lz_receive()` (returns validated params) 2. Decode the message payload to extract your application data 3. Execute your custom business logic (state updates, token transfers, etc.) 4. Handle any post-processing (events, cleanup) ### How This Differs from EVM | Aspect | EVM | IOTA | | ------------------- | --------------------------------------------- | ------------------------------------------------ | | **Message Routing** | Endpoint calls `lzReceive()` via delegatecall | Endpoint creates Call object, PTB routes to OApp | | **Validation** | Implicit via `onlyEndpoint` modifier | Explicit via CallCap and Call pattern | | **Execution Flow** | Single transaction with nested calls | PTB chains multiple function calls atomically | | **Authorization** | Address-based (`msg.sender`) | Capability-based (own the CallCap) | | **Composability** | Vertical (nested calls in one tx) | Horizontal (chained calls in PTB) | *** ## Message Encoding and Business Logic Your OApp is responsible for encoding/decoding message payloads. LayerZero transports raw bytes - how you structure them depends on your application. **Key Principles**: * Use consistent byte order (big-endian recommended for cross-VM compatibility with EVM) * Document your message format clearly * Consider padding for fixed-width fields * Test encoding/decoding on both source and destination chains **Example from OFT**: The [oft\_msg\_codec.move](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/iota/contracts/oapps/oft/oft/sources/codec/oft_msg_codec.move) shows a production codec that encodes recipient address, amount in shared decimals, and optional compose parameters. *** ## Required Components Every OApp on IOTA requires four key components: ### 1. OApp Shared Object Contains configuration state including: * **Peer mappings**: Maps endpoint IDs to trusted remote OApp addresses * **Enforced options**: Minimum gas and execution parameters for each destination * **Embedded CallCap**: Used internally for authentication * **Admin tracking**: Reference to the AdminCap owner address The OApp object is created as a [shared object](https://docs.iota.org/developer/iota-101/objects/object-ownership/shared), making it publicly accessible for reading configuration while restricting modifications to capability holders. ### 2. Capability Objects **CallCap** (owned): Required for all messaging operations (`quote`, `lz_send`). This proves ownership of the OApp and is validated on every call. Typically stored within your application's module or transferred to a specific address. **AdminCap** (owned): Grants authority for configuration operations like setting peers, configuring DVNs, and updating enforced options. Transferable to enable admin rotation. ### 3. MessagingChannel Created during Endpoint registration, this shared object stores state for your OApp's [communication channel](/v2/concepts/glossary#channel--lossless-channel): * Message nonces for ordering * Payload hashes for verification * Channel initialization state per destination EID The registry maps your **package ID** → MessagingChannel, which is why peers must use package IDs. ### 4. Message Codec A module in your package that defines how to encode/decode your business logic into bytes that LayerZero transports crosschain. This is application-specific - OFT uses `oft_msg_codec`, your custom OApp would define its own format. ## Message Flow ### Send Flow ``` ┌─────────────┐ │ OApp │ 1. User calls send() │ (Your App) │ └──────┬──────┘ │ 2. Create SendParam ▼ ┌─────────────┐ │ Endpoint │ 3. Throws Hot Potato └──────┬──────┘ │ 4. PTB routes to ULN ▼ ┌─────────────┐ │ ULN302 │ 5. Assign jobs to workers └──────┬──────┘ │ 6. Throws Hot Potatoes ▼ ┌─────────────────┐ │ DVNs + Executor │ 7. Process and return results └─────────────────┘ ``` ### Receive Flow ``` ┌─────────────┐ │ Executor │ 1. Calls lzReceive └──────┬──────┘ │ 2. Throws Hot Potato ▼ ┌─────────────┐ │ Endpoint │ 3. Routes to OApp └──────┬──────┘ │ 4. Throws Hot Potato ▼ ┌─────────────┐ │ OApp │ 5. Process message │ (Your App) │ 6. Update state └─────────────┘ ``` ## Core Methods These are the primary functions your OApp will call to send messages and receive them from other chains. ### quote() Estimates the fee required to send a crosschain message without actually sending it. Returns a `Call` that must be routed through the Endpoint in a PTB, then confirmed with `confirm_quote()` to extract the fee amount. **When to use**: Before sending to determine how much IOTA to include in the transaction, or to display estimated costs to users. ### lz\_send() Sends a crosschain message to a destination chain. Creates a `Call` that routes through Endpoint → ULN → DVNs/Executor, then must be confirmed with `confirm_lz_send()` to extract the receipt. **Key parameters**: * `dst_eid`: Destination chain endpoint ID * `message`: Your encoded payload (raw bytes) * `options`: Execution parameters (gas limits, msg.value) * `native_token_fee`: IOTA payment for crosschain delivery * `refund_address`: Where to send excess fees **Sequential execution**: Uses internal state tracking (`sending_call`) to enforce one send at a time. Must call `confirm_lz_send()` before initiating another send. ### lz\_send\_and\_refund() Alternative send method that allows parallel message sending within the same PTB. Unlike `lz_send()`, this doesn't track state and doesn't require confirmation, making it suitable for batch operations. Requires a refund address (cannot be optional). **When to use**: When sending multiple messages in one transaction and order doesn't matter. ### lz\_receive() Processes incoming messages delivered by the Executor. This function is called with a `Call` created by the Endpoint. It performs three critical validations: 1. **CallCap validation**: Ensures the Call belongs to this OApp 2. **Endpoint check**: Verifies the Call originated from the authorized LayerZero Endpoint 3. **Peer verification**: Confirms the message sender matches the configured peer for the source chain After validation, it returns `LzReceiveParam` containing the decoded message data for your business logic to process. **Your implementation**: You'll wrap `oapp::lz_receive()` in your own function that adds application-specific processing (see OFT's implementation for reference). ## Best Practices ### Always Validate CallCap Every function that accepts a `CallCap` must call `self.assert_oapp_cap(oapp_cap)` to ensure it belongs to this OApp. This prevents unauthorized calls and ensures type safety. ### Verify Message Senders in lz\_receive Always validate that incoming messages come from configured peers. The `oapp::lz_receive()` base function handles this validation, but custom receive logic must preserve these checks. ### Use One-Time Witness for Initialization Use the [one-time witness pattern](https://docs.iota.org/developer/iota-101/move-overview/one-time-witness) in your module's `init()` function to create the OApp. This guarantees initialization runs exactly once. ### Configure Security Before Setting Peers Set your message libraries and DVN configuration before calling `set_peer()`. Setting a peer opens the pathway for messaging, so security should be configured first. ### Confirm All Call Objects Every `Call` object returned by `quote()`, `lz_send()`, or similar functions must be consumed in a PTB (routed through protocol components) and confirmed to extract results. Unused Call objects will cause transaction failures due to their lack of `drop` ability. ## Configuration Before your OApp can send messages, you must configure: 1. **Initialize Channel**: Create channel state for remote EID 2. **Set Peer**: Define the peer OApp address on the remote chain 3. **Configure DVNs**: Set which DVNs verify your messages (optional, defaults used if not set) 4. **Configure Executor**: Set who executes messages on destination (optional, defaults used if not set) See the [Configuration Guide](/v2/developers/iota/configuration/dvn-executor-config) for details. ## Security Considerations ### Critical Validations * Always validate OApp object in every function * Verify message sender matches configured peer * Check Endpoint address matches stored value * Validate nonce sequence to prevent replay attacks ### Common Pitfalls * Forgetting to call `assert_oapp` * Not validating message sender in `lzReceive` * Incorrect peer address configuration * Missing channel initialization ## Next Steps * [OFT Implementation](/v2/developers/iota/oft/overview) - Token standard built on OApp * [OFT SDK](/v2/developers/iota/oft/sdk) - TypeScript SDK methods and patterns * [Configuration Guide](/v2/developers/iota/configuration/dvn-executor-config) - DVN and executor setup * [Technical Overview](/v2/developers/iota/technical-overview) - IOTA fundamentals and Call pattern * [Protocol Overview](/v2/developers/iota/protocol-overview) - Complete message workflows * [Troubleshooting](/v2/developers/iota/troubleshooting/common-errors) - Common issues and solutions # IOTA L1 OFT Source: https://docs.layerzero.network/v2/developers/iota/oft/overview Overview of IOTA L1 OFT on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure crosschain messaging. 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](/v2/concepts/applications/oft-standard). ## What is an OFT on IOTA? An OFT on IOTA is a [Move package](https://docs.iota.org/developer/iota-101/move-overview/package-upgrades/introduction) that extends the OApp functionality to enable crosschain token transfers. It integrates with IOTA's native [coin type system](https://docs.iota.org/developer/standards/coin) ([`Coin`](https://docs.iota.org/references/framework/coin), [`Balance`](https://docs.iota.org/references/framework/balance), `TreasuryCap`) 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](#integration-with-iota-l1-coin-system). ## Deployment OFT deployment on IOTA uses a **two-package pattern**: your token + pure [LayerZero OFT source](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/iota/contracts/oapps/oft/oft). ### 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](#choosing-mintburn-vs-lockunlock) for details. **Prerequisites**: * IOTA CLI installed (see [installation instructions](#install-iota-cli) below) * Node.js and npm for TypeScript SDK * 1-2 IOTA for gas fees #### Install IOTA CLI ```bash wrap theme={null} # macOS (Homebrew) brew tap iotaledger/tap brew install iota # Verify installation iota --version ``` For other platforms, see the [IOTA Installation Guide](https://docs.iota.org/developer/getting-started/install-iota). #### Create Wallet and Get IOTA ```bash wrap theme={null} # Create new wallet (interactive prompts) iota client new-address # Switch to mainnet or testnet iota client switch --env mainnet iota client switch --env testnet # Check your address iota client active-address # Request testnet IOTA from faucet iota client faucet # Verify balance before proceeding (wait a few seconds after faucet) iota client gas ``` To fund your wallet, use the [IOTA faucet](https://docs.iota.org/developer/getting-started/get-coins) 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](/v2/developers/iota/getting-started) to understand the object model, package structure, and development basics. ### Step 1: Create and Deploy Your Token **Create token package**: ```bash wrap theme={null} mkdir my-token cd my-token iota move new myoft ``` **Implement token** (`sources/myoft.move`): ```rust wrap theme={null} module myoft::myoft; use iota::coin; /// One-time witness for coin creation /// Must be named same as module (MYOFT) and have only `drop` ability public struct MYOFT has drop {} /// Initialize the coin on package publish fun init(otw: MYOFT, ctx: &mut TxContext) { // Create the coin with metadata let (treasury_cap, coin_metadata) = coin::create_currency( otw, // One-time witness 6, // decimals (6 for crosschain compatibility) b"MYOFT", // symbol b"My Omnichain Fungible Token", // name b"A LayerZero OFT on IOTA with mint/burn capabilities", // description option::none(), // icon_url (optional) ctx ); // Freeze the metadata object (makes it immutable and shared) transfer::public_freeze_object(coin_metadata); // Transfer treasury cap to deployer transfer::public_transfer(treasury_cap, ctx.sender()); } // That's it! No OFT logic in token package ``` **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 ```bash wrap theme={null} # From your token directory iota client publish --gas-budget 500000000 --json > token_deploy.json # Extract IDs TOKEN_PACKAGE=$(jq -r '.objectChanges[] | select(.type=="published") | .packageId' token_deploy.json) TREASURY_CAP=$(jq -r '.objectChanges[] | select(.objectType | contains("TreasuryCap")) | .objectId' token_deploy.json) COIN_METADATA=$(jq -r '.objectChanges[] | select(.objectType | contains("CoinMetadata")) | .objectId' token_deploy.json) echo "Token Package: $TOKEN_PACKAGE" echo "Treasury Cap: $TREASURY_CAP" echo "Coin Metadata: $COIN_METADATA" ``` ### Step 2: Deploy LayerZero OFT Package Deploy the pure [LayerZero OFT source](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/iota/contracts/oapps/oft/oft) without modifications. **Recommended approach** (git dependencies): ```bash wrap theme={null} # Copy OFT source to your project mkdir oft cd oft ``` **Create `Move.toml`** with git dependencies: ```toml wrap theme={null} [package] name = "OFT" version = "0.0.1" edition = "2024.beta" license = "MIT" [dependencies] OApp = { git = "https://github.com/LayerZero-Labs/LayerZero-v2.git", subdir = "packages/layerzero-v2/iota/contracts/oapps/oapp", rev = "main" } OFTCommon = { git = "https://github.com/LayerZero-Labs/LayerZero-v2.git", subdir = "packages/layerzero-v2/iota/contracts/oapps/oft/oft-common", rev = "main" } PtbMoveCall = { git = "https://github.com/LayerZero-Labs/LayerZero-v2.git", subdir = "packages/layerzero-v2/iota/contracts/ptb-builders/ptb-move-call", rev = "main" } [addresses] oft = "0x0" [dev-dependencies] SimpleMessageLib = { git = "https://github.com/LayerZero-Labs/LayerZero-v2.git", subdir = "packages/layerzero-v2/iota/contracts/message-libs/simple-message-lib", rev = "main" } ``` **Copy OFT source files**: ```bash wrap theme={null} # Clone LayerZero repository (temporary, just to copy sources) git clone https://github.com/LayerZero-Labs/LayerZero-v2.git --depth 1 cp -r LayerZero-v2/packages/layerzero-v2/iota/contracts/oapps/oft/oft/sources ./ rm -rf LayerZero-v2 ``` **Deploy** (dependencies auto-fetched from GitHub): ```bash wrap theme={null} # From your OFT directory iota client publish --gas-budget 1000000000 --json > oft_deploy.json # Extract the package ID (you'll need this for peer configuration!) OFT_PACKAGE=$(jq -r '.objectChanges[] | select(.type=="published") | .packageId' oft_deploy.json) OAPP_OBJECT=$(jq -r '.objectChanges[] | select(.objectType | contains("::oapp::OApp")) | select(.owner.Shared) | .objectId' oft_deploy.json) INIT_TICKET=$(jq -r '.objectChanges[] | select(.objectType | contains("OFTInitTicket")) | .objectId' oft_deploy.json) echo "OFT Package: $OFT_PACKAGE" # ← SAVE THIS! Use as peer on remote chains echo "OApp Object: $OAPP_OBJECT" echo "Init Ticket: $INIT_TICKET" ``` 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): ```bash wrap theme={null} iota client object --json | jq -r '.data.type' | cut -d':' -f1 ``` **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](#choosing-mintburn-vs-lockunlock) 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`: ```typescript wrap theme={null} import {IotaClient} from '@iota/iota-sdk/client'; import {Transaction} from '@iota/iota-sdk/transactions'; import {Ed25519Keypair} from '@iota/iota-sdk/keypairs/ed25519'; import {SDK} from '@layerzerolabs/lz-iotal1-sdk-v2'; import {OFT} from '@layerzerolabs/lz-iotal1-oft-sdk-v2'; import {Stage} from '@layerzerolabs/lz-definitions'; // Initialize IOTA client const client = new IotaClient({url: 'https://api.mainnet.iota.cafe'}); // Load keypair from private key (supports bech32 'iotaprivkey1...' or hex '0x...' format) const keypair = Ed25519Keypair.fromSecretKey(/* your private key bytes */); const sender = keypair.toIotaAddress(); const sdk = new SDK({client, stage: Stage.MAINNET}); const oft = new OFT(sdk, OFT_PKG, undefined, TOKEN_TYPE, OAPP); const initTx = new Transaction(); // Use initOftMoveCall for mint/burn (includes TreasuryCap) const [adminCap, migrationCap] = oft.initOftMoveCall( initTx, TOKEN_TYPE, // "0xTOKEN_PKG::myoft::MYOFT" TICKET, // OFTInitTicket object ID OAPP, // OApp object ID TREASURY, // TreasuryCap object ID (enables mint/burn) METADATA, // CoinMetadata object ID 6, // shared_decimals ); initTx.transferObjects([adminCap, migrationCap], sender); const result = await client.signAndExecuteTransaction({ transaction: initTx, signer: keypair, options: {showObjectChanges: true}, }); // Extract OFT object ID const OFT_OBJECT = result.objectChanges.find( (c) => c.type === 'created' && c.objectType.includes('oft::OFT<'), ).objectId; await client.waitForTransaction({digest: result.digest}); ``` ### 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](#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`**: Owned coin object with a value ```rust wrap theme={null} public struct Coin has key, store { id: UID, balance: Balance, } ``` **`Balance`**: Storable value (can be held in structs) ```rust wrap theme={null} public struct Balance has store { value: u64, } ``` **`TreasuryCap`**: Authority to mint/burn coins ```rust wrap theme={null} public struct TreasuryCap has key, store { id: UID, total_supply: Supply, } ``` **`CoinMetadata`**: Token information (name, symbol, decimals) ```rust wrap theme={null} public struct CoinMetadata has key, store { id: UID, decimals: u8, name: string::String, symbol: ascii::String, description: string::String, icon_url: Option, } ``` ### OFT Integration The OFT uses these types: ```rust wrap theme={null} public struct OFT has key { // ... treasury: OFTTreasury, // Holds TreasuryCap OR Balance escrow coin_metadata: address, // Reference to CoinMetadata object // ... } ``` **[Phantom Type Parameter](https://move-book.com/move-basics/generics#phantom-type-parameters)**: `` 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: ```rust wrap theme={null} /// Omnichain Fungible Token - enables seamless crosschain token transfers public struct OFT has key { id: UID, upgrade_version: u64, oapp_object: address, // Associated OApp for messaging admin_cap: address, // AdminCap owner address migration_cap: address, // Migration capability oft_cap: CallCap, // Capability for crosschain calls treasury: OFTTreasury, // ← Enum: determines mint/burn vs lock/unlock coin_metadata: address, // Reference to CoinMetadata decimal_conversion_rate: u64, // 10^(local - shared decimals) shared_decimals: u8, // Crosschain precision // Optional features (always present, opt-in to configure) pausable: Pausable, // Starts unpaused (false) fee: OFTFee, // Starts with 0% fees inbound_rate_limiter: RateLimiter, // Starts with no limits outbound_rate_limiter: RateLimiter, // Starts with no limits } ``` 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` enum determines token management strategy: ```rust wrap theme={null} public enum OFTTreasury has store { /// Standard OFT: mints/burns using treasury capability OFT { treasury_cap: TreasuryCap, // Grants mint/burn authority }, /// Adapter OFT: escrows/releases existing tokens OFTAdapter { escrow: Balance, // Token balance pool }, } ``` #### Choosing Mint/Burn vs Lock/Unlock | Model | When to Use | Initialization Method | | --------------- | ------------------------------------- | ----------------------------------------------- | | **Mint/Burn** | You have/control the `TreasuryCap` | `oft.initOftMoveCall()` with TREASURY parameter | | **Lock/Unlock** | You DON'T have the `TreasuryCap` | `oft.initOftAdapterMoveCall()` without TREASURY | #### 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` 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`](https://docs.iota.org/developer/standards/coin#treasury-capability) 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): ```typescript wrap theme={null} // SDK handles internal treasury enum construction const [adminCap, migrationCap] = oft.initOftMoveCall( initTx, TOKEN_TYPE, TICKET, OAPP, TREASURY, // ← Your TreasuryCap transferred to OFT internally METADATA, 6, // shared_decimals ); ``` #### 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` (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): ```typescript wrap theme={null} // SDK handles internal treasury enum construction const [adminCap, migrationCap] = oft.initOftAdapterMoveCall( initTx, TOKEN_TYPE, TICKET, OAPP, METADATA, // ← No TreasuryCap needed for adapter 6, // shared_decimals ); ``` 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. ```rust wrap theme={null} public fun send( self: &mut OFT, oapp: &mut OApp, sender: &OFTSender, // Authorization context (from oft_sender module) send_param: &SendParam, // Complete send parameters coin_provided: &mut Coin, // Coin to debit tokens from native_coin_fee: Coin, // Fee payment in IOTA zro_coin_fee: Option>, // Optional ZRO payment refund_address: Option
, // Optional refund address clock: &Clock, // Clock for rate limiting ctx: &mut TxContext, ): (Call, OFTSendContext) ``` **Returns**: A tuple containing: 1. `Call` - 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. ```rust wrap theme={null} public fun lz_receive( self: &mut OFT, oapp: &OApp, // Associated OApp for validation call: Call,// Call from Executor via Endpoint clock: &Clock, // Clock for rate limiting ctx: &mut TxContext, ) ``` **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](/v2/concepts/technical-reference/oft-reference#shared-decimals). ### 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**: ``` Maximum supply = (2^64 - 1) / (10^decimals) ``` Choose your decimals carefully during token deployment. ### Recommended Configuration for IOTA | Local Decimals | Max Total Supply | Recommendation | | ----------------- | ----------------- | --------------- | | 6 | \~18.4 trillion | ✅ Recommended | | 9 | \~18.4 billion | ✅ Recommended | | 18 (EVM standard) | \~18 whole tokens | ❌ Avoid on IOTA | **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](/v2/concepts/technical-reference/oft-reference#shared-decimals). ## Registration with Endpoint After initializing your OFT, you must register it with the LayerZero Endpoint to enable crosschain messaging. ### Using OFT SDK ```typescript wrap theme={null} import {IotaClient} from '@iota/iota-sdk/client'; import {Transaction} from '@iota/iota-sdk/transactions'; import {Ed25519Keypair} from '@iota/iota-sdk/keypairs/ed25519'; import {SDK} from '@layerzerolabs/lz-iotal1-sdk-v2'; import {OFT} from '@layerzerolabs/lz-iotal1-oft-sdk-v2'; import {Stage} from '@layerzerolabs/lz-definitions'; // Initialize IOTA client const client = new IotaClient({url: 'https://api.mainnet.iota.cafe'}); // For testnet: const client = new IotaClient({ url: 'https://api.testnet.iota.cafe' }); const sdk = new SDK({client, stage: Stage.MAINNET}); const oft = new OFT(sdk, OFT_PKG, OFT_OBJECT, TOKEN_TYPE, OAPP); const regTx = new Transaction(); // SDK auto-generates lz_receive_info internally! await oft.registerOAppMoveCall( regTx, TOKEN_TYPE, // "0xTOKEN_PKG::myoft::MYOFT" OFT_OBJECT, // OFT object ID OAPP, // OApp object ID '0xfe5be5a2d5b11e635e3e4557bb125fb24a3dd09111eded06fd6058b2aee1d054', // OFTComposerManager (IOTA mainnet) ); const regResult = await client.signAndExecuteTransaction({ transaction: regTx, signer: keypair, options: {showObjectChanges: true}, }); // Wait for finality await client.waitForTransaction({digest: regResult.digest}); console.log('✅ Registration complete:', regResult.digest); ``` **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](/v2/deployments/deployed-contracts). ```typescript wrap theme={null} import {IotaClient} from '@iota/iota-sdk/client'; import {Transaction} from '@iota/iota-sdk/transactions'; import {Ed25519Keypair} from '@iota/iota-sdk/keypairs/ed25519'; import { SDK, PACKAGE_ULN_302_ADDRESS, OBJECT_ULN_302_ADDRESS, PACKAGE_DVN_LAYERZERO_ADDRESS, OAppUlnConfigBcs, } from '@layerzerolabs/lz-iotal1-sdk-v2'; import {OFT} from '@layerzerolabs/lz-iotal1-oft-sdk-v2'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {Stage} from '@layerzerolabs/lz-definitions'; // Initialize client and keypair (see Step 3 for details) const client = new IotaClient({url: 'https://api.mainnet.iota.cafe'}); const keypair = Ed25519Keypair.fromSecretKey(/* your private key bytes */); const sdk = new SDK({client, stage: Stage.MAINNET}); const oapp = sdk.getOApp(OFT_PKG); // Use OFT package ID const oft = new OFT(sdk, OFT_PKG, OFT_OBJECT, TOKEN_TYPE, OAPP); // Step 1: Set Send Library (recommended - custom send message library) const sendLibTx = new Transaction(); await oapp.setSendLibraryMoveCall( sendLibTx, 30184, // Destination EID customSendLibraryAddress, ); await client.signAndExecuteTransaction({transaction: sendLibTx, signer: keypair}); // Step 1: Set Receive Library (recommended - custom receive message library) const receiveLibTx = new Transaction(); await oapp.setReceiveLibraryMoveCall( receiveLibTx, 30184, // Source EID customReceiveLibraryAddress, 0, // Grace period ); await client.signAndExecuteTransaction({transaction: receiveLibTx, signer: keypair}); // Step 2: Configure Receive DVN (recommended - receive verification) const receiveConfig = OAppUlnConfigBcs.serialize({ use_default_confirmations: false, use_default_required_dvns: false, use_default_optional_dvns: true, uln_config: { confirmations: 15, // Replace with a non-LayerZero-Labs DVN; see /v2/deployments/dvn-addresses required_dvns: [ PACKAGE_DVN_LAYERZERO_ADDRESS[Stage.MAINNET], PACKAGE_DVN__ADDRESS[Stage.MAINNET], ], optional_dvns: [], optional_dvn_threshold: 0, }, }).toBytes(); const receiveConfigTx = new Transaction(); const receiveConfigCall = await oapp.setConfigMoveCall( receiveConfigTx, PACKAGE_ULN_302_ADDRESS[Stage.MAINNET], 30184, // Destination EID 3, // CONFIG_TYPE_RECEIVE_ULN receiveConfig, ); receiveConfigTx.moveCall({ target: `${PACKAGE_ULN_302_ADDRESS[Stage.MAINNET]}::uln_302::set_config`, arguments: [receiveConfigTx.object(OBJECT_ULN_302_ADDRESS[Stage.MAINNET]), receiveConfigCall], }); await client.signAndExecuteTransaction({transaction: receiveConfigTx, signer: keypair}); // Step 2: Configure Send DVN (recommended - send verification) const sendConfig = OAppUlnConfigBcs.serialize({ use_default_confirmations: false, use_default_required_dvns: false, use_default_optional_dvns: true, uln_config: { confirmations: 15, // Replace with a non-LayerZero-Labs DVN; see /v2/deployments/dvn-addresses required_dvns: [ PACKAGE_DVN_LAYERZERO_ADDRESS[Stage.MAINNET], PACKAGE_DVN__ADDRESS[Stage.MAINNET], ], optional_dvns: [], optional_dvn_threshold: 0, }, }).toBytes(); const sendConfigTx = new Transaction(); const sendConfigCall = await oapp.setConfigMoveCall( sendConfigTx, PACKAGE_ULN_302_ADDRESS[Stage.MAINNET], 30184, 2, // CONFIG_TYPE_SEND_ULN (outbound messages) sendConfig, ); sendConfigTx.moveCall({ target: `${PACKAGE_ULN_302_ADDRESS[Stage.MAINNET]}::uln_302::set_config`, arguments: [sendConfigTx.object(OBJECT_ULN_302_ADDRESS[Stage.MAINNET]), sendConfigCall], }); await client.signAndExecuteTransaction({transaction: sendConfigTx, signer: keypair}); // Step 3: Set Enforced Options (optional - minimum gas requirements) const options = Options.newOptions() .addExecutorLzReceiveOption(80000, 0) // 80k gas for destination .toBytes(); const optionsTx = new Transaction(); await oapp.setEnforcedOptionsMoveCall( optionsTx, 30184, // Destination EID 1, // Message type (1 = SEND) options, ); await client.signAndExecuteTransaction({transaction: optionsTx, signer: keypair}); // Step 4: Configure OFT Settings (optional - rate limits) const rateLimitTx = new Transaction(); await oft.setRateLimitMoveCall( rateLimitTx, 30184, // Destination EID false, // Outbound 1000000n, // 1M tokens per window 86400n, // 24 hours ); await client.signAndExecuteTransaction({transaction: rateLimitTx, signer: keypair}); // Step 4: Configure OFT Settings (optional - fees) const feeTx = new Transaction(); await oft.setFeeBpsMoveCall(feeTx, 30184, 30); // 0.3% fee await client.signAndExecuteTransaction({transaction: feeTx, signer: keypair}); // Step 5: Set Peer LAST (required - opens pathway for messaging) const peerTx = new Transaction(); await oapp.setPeerMoveCall( peerTx, 30184, // Destination EID (e.g., Base) Buffer.from('0000000000000000000000006D2e17A05B9Ac62b8499f4bF4757e261005c03A5', 'hex'), ); await client.signAndExecuteTransaction({transaction: peerTx, signer: keypair}); ``` **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](/v2/developers/iota/configuration/dvn-executor-config). ### 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**: ```solidity wrap theme={null} // On EVM: Use IOTA OFT PACKAGE ID (not object ID!) myOFT.setPeer( 30423, // IOTA L1 mainnet EID bytes32(0x061a47bf...) // Your IOTA OFT Package ID ); ``` **2. Set Enforced Options for IOTA Destination**: Based on gas profiling, configure appropriate gas limits for IOTA: ```solidity wrap theme={null} // On EVM: Set enforced options for IOTA pathway import { Options } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/ExecutorOptions.sol"; bytes memory options = Options.newOptions() .addExecutorLzReceiveOption(5000, 0); // 5k gas units, no msg.value myOFT.setEnforcedOptions( EnforcedOptionParam({ eid: 30423, // IOTA L1 mainnet msgType: SEND, options: options }) ); ``` **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) ```typescript wrap theme={null} import {OFT} from '@layerzerolabs/lz-iotal1-oft-sdk-v2'; // Quote the fee const {nativeFee} = await oft.quote({ dstEid: 30101, // Ethereum to: recipientBytes32, amountLD: BigInt(1000000), // 1 token (6 decimals) options: optionsBytes, }); // Send tokens const receipt = await oft.send({ dstEid: 30101, to: recipientBytes32, amountLD: BigInt(1000000), minAmountLD: BigInt(950000), // 5% slippage nativeFee, options: optionsBytes, }); ``` For more SDK usage, see [OFT SDK Documentation](/v2/developers/iota/oft/sdk). ## 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](/v2/developers/iota/configuration/dvn-executor-config). ## Next Steps * [OFT SDK Documentation](/v2/developers/iota/oft/sdk) - Complete SDK methods and TypeScript integration * [Configuration Guide](/v2/developers/iota/configuration/dvn-executor-config) - DVN, executor, and gas configuration * [OApp Overview](/v2/developers/iota/oapp/overview) - Base messaging standard * [Technical Overview](/v2/developers/iota/technical-overview) - IOTA fundamentals and Call pattern * [Protocol Overview](/v2/developers/iota/protocol-overview) - Complete message workflows * [Troubleshooting](/v2/developers/iota/troubleshooting/common-errors) - Common deployment issues # IOTA L1 OFT SDK Source: https://docs.layerzero.network/v2/developers/iota/oft/sdk The LayerZero IOTA L1 OFT SDK provides TypeScript utilities for interacting with OFT contracts on the IOTA L1 blockchain, enabling seamless crosschain... The LayerZero IOTA L1 OFT SDK provides TypeScript utilities for interacting with OFT contracts on the IOTA L1 blockchain, enabling seamless crosschain token transfers. ## Installation Install both the core IOTA SDK and the OFT-specific SDK: ```bash wrap theme={null} npm install @layerzerolabs/lz-iotal1-sdk-v2 @layerzerolabs/lz-iotal1-oft-sdk-v2 ``` Or with yarn: ```bash wrap theme={null} yarn add @layerzerolabs/lz-iotal1-sdk-v2 @layerzerolabs/lz-iotal1-oft-sdk-v2 ``` ## Setup ### Initialize the SDKs The recommended pattern uses automatic address fetching from the protocol SDK: ```typescript wrap theme={null} import {IOTAClient} from '@iota/iota-sdk/client'; import {SDK} from '@layerzerolabs/lz-iotal1-sdk-v2'; import {OFT} from '@layerzerolabs/lz-iotal1-oft-sdk-v2'; import {Stage} from '@layerzerolabs/lz-definitions'; // Setup IOTA client const client = new IOTAClient({url: 'https://fullnode.mainnet.iota.io:443'}); // Initialize protocol SDK (automatically fetches LayerZero protocol addresses) const sdk = new SDK({client, stage: Stage.MAINNET}); // Initialize OFT SDK with your OFT package ID const oft = new OFT( sdk, // Protocol SDK instance oftPackageId, // Your OFT package ID (NOT OFT CallCap ID!) oftObjectId, // Optional: OFT object ID (set after init) tokenType, // Optional: "0x123::mycoin::MYCOIN" oappObjectId, // Optional: OApp object ID adminCapId, // Optional: Admin cap ID (can query later) ); ``` **Critical Notes**: * **First parameter**: Use your OFT **package ID** (where your code is deployed) * **SDK automatic addresses**: No need to hardcode LayerZero protocol addresses * **Optional parameters**: Can be `undefined` initially and set later * **After initialization**: Update `oft.oftObjectId = newObjectId` **Getting OApp Instance** (for peer/DVN configuration): ```typescript wrap theme={null} // Use SDK to get OApp instance (recommended) const oapp = sdk.getOApp(oftPackageId); // Use package ID // Configure peers and DVNs through OApp await oapp.setPeerMoveCall(tx, dstEid, peerBytes); await oapp.setConfigMoveCall(tx, lib, eid, configType, config); ``` **Do NOT manually instantiate** `new OApp(...)` - this will fail to find your OApp in the registry. Always use `sdk.getOApp(packageId)`. ## SDK Architecture The IOTA OFT SDK consists of two complementary SDKs: ### Base SDK (`@layerzerolabs/lz-iotal1-sdk-v2`) Provides core LayerZero protocol functionality: * **OApp operations**: Peer configuration, messaging, registration * **Endpoint interaction**: Channel initialization, library configuration * **DVN/Executor configuration**: Security stack setup * **Protocol address exports**: All deployed contract addresses **When to use**: For OApp configuration, peer setup, DVN configuration, and general protocol interactions. ### OFT SDK (`@layerzerolabs/lz-iotal1-oft-sdk-v2`) Extends the base SDK with OFT-specific functionality: * **OFT initialization**: `initOftMoveCall()`, `initOftAdapterMoveCall()` * **Registration**: `registerOAppMoveCall()` (auto-generates lz\_receive\_info) * **Rate limiting**: Per-pathway token flow limits * **Fee management**: Crosschain fee configuration * **Pause control**: Emergency pause functionality **When to use**: For OFT deployment, token-specific operations, and OFT lifecycle management. ### Relationship ```typescript wrap theme={null} // Base SDK provides OApp functionality const sdk = new SDK({ client, stage: Stage.MAINNET }); const oapp = sdk.getOApp(packageId); // OApp configuration // OFT SDK extends with token-specific features const oft = new OFT(sdk, oftPackageId, ...); // OFT operations ``` ## SDK Address Exports The SDK provides address exports for all protocol contracts, eliminating hardcoded values: ```typescript wrap theme={null} import { // Object addresses (shared instances everyone uses) OBJECT_ENDPOINT_V2_ADDRESS, OBJECT_ULN_302_ADDRESS, // Package addresses (where code lives) PACKAGE_OAPP_ADDRESS, PACKAGE_ULN_302_ADDRESS, PACKAGE_DVN_LAYERZERO_ADDRESS, // Helpers OAppUlnConfigBcs, Stage, } from '@layerzerolabs/lz-iotal1-sdk-v2'; // Get addresses for your network const endpointObj = OBJECT_ENDPOINT_V2_ADDRESS[Stage.MAINNET]; const uln302Obj = OBJECT_ULN_302_ADDRESS[Stage.MAINNET]; const uln302Pkg = PACKAGE_ULN_302_ADDRESS[Stage.MAINNET]; const dvnLayerZero = PACKAGE_DVN_LAYERZERO_ADDRESS[Stage.MAINNET]; ``` **Benefits**: * **Network switching**: Toggle between mainnet/testnet via `Stage` enum * **SDK updates**: Address changes handled automatically * **No magic numbers**: Self-documenting configuration * **Type safety**: TypeScript ensures correct usage **Available exports**: | Export | Description | Usage | | ------------------------------- | ---------------------- | -------------------------- | | `OBJECT_ENDPOINT_V2_ADDRESS` | Endpoint shared object | Pass to Endpoint functions | | `OBJECT_ULN_302_ADDRESS` | ULN302 shared object | Pass to ULN302 functions | | `PACKAGE_OAPP_ADDRESS` | OApp package ID | Reference for OApp code | | `PACKAGE_ULN_302_ADDRESS` | ULN302 package ID | Use in move call targets | | `PACKAGE_DVN_LAYERZERO_ADDRESS` | LayerZero DVN package | DVN configuration | | `OAppUlnConfigBcs` | Config serializer | Encode DVN configuration | ## Complete Working Example ### Reference Implementation All examples on this page are based on proven mainnet deployments. The complete reference implementation demonstrating these patterns is available in the LayerZero test repository at `test-repo/iota-oft-complete/deploy_with_oft_sdk.mjs`. Based on proven mainnet deployment: ```typescript wrap theme={null} import {IOTAClient} from '@iota/iota-sdk/client'; import {Transaction} from '@iota/iota-sdk/transactions'; import {Ed25519Keypair} from '@iota/iota-sdk/keypairs/ed25519'; import {SDK} from '@layerzerolabs/lz-iotal1-sdk-v2'; import {OFT} from '@layerzerolabs/lz-iotal1-oft-sdk-v2'; import {Stage} from '@layerzerolabs/lz-definitions'; const client = new IOTAClient({url: 'https://fullnode.mainnet.iota.io:443'}); const keypair = Ed25519Keypair.fromSecretKey(secretKeyBytes); // Initialize protocol SDK const sdk = new SDK({client, stage: Stage.MAINNET}); // Initialize OFT SDK const oft = new OFT(sdk, oftPackageId, undefined, tokenType, oappObjectId); // Step 1: Initialize OFT const initTx = new Transaction(); const [adminCap, migrationCap] = oft.initOftMoveCall( initTx, tokenType, ticketObjectId, oappObjectId, treasuryCapId, coinMetadataId, 6, // shared_decimals ); initTx.transferObjects([adminCap, migrationCap], sender); const initResult = await client.signAndExecuteTransaction({ transaction: initTx, signer: keypair, options: {showObjectChanges: true}, }); // ✅ CRITICAL: Wait for finality before referencing created objects await client.waitForTransaction({digest: initResult.digest}); const oftObjectId = initResult.objectChanges.find( (c) => c.type === 'created' && c.objectType.includes('oft::OFT<'), ).objectId; // Update OFT SDK with object ID oft.oftObjectId = oftObjectId; // Step 2: Register (SDK auto-generates lz_receive_info) const regTx = new Transaction(); await oft.registerOAppMoveCall( regTx, tokenType, oftObjectId, oappObjectId, '0xfe5be5a2d5b11e635e3e4557bb125fb24a3dd09111eded06fd6058b2aee1d054', // OFTComposerManager (IOTA mainnet) ); const regResult = await client.signAndExecuteTransaction({transaction: regTx, signer: keypair}); // Wait for finality before next operation await client.waitForTransaction({digest: regResult.digest}); // Step 3: Configure via OApp SDK const oapp = sdk.getOApp(oftPackageId); const peerTx = new Transaction(); await oapp.setPeerMoveCall(peerTx, dstEid, peerBytes); await client.signAndExecuteTransaction({transaction: peerTx, signer: keypair}); console.log('Deployment complete!'); ``` **Key SDK Methods Used**: * `oft.initOftMoveCall()` - Initialize OFT with treasury * `oft.registerOAppMoveCall()` - Register and auto-generate lz\_receive\_info * `sdk.getOApp()` - Get OApp instance for configuration * `oapp.setPeerMoveCall()` - Configure peer addresses * `oapp.setConfigMoveCall()` - Configure DVNs/executors *** ## Available SDK Methods ### Base SDK (OApp Operations) The base SDK provides methods for OApp configuration through `sdk.getOApp(packageId)`: ```typescript wrap theme={null} const oapp = sdk.getOApp(packageId); // Peer Configuration await oapp.setPeerMoveCall(tx, eid, peerBytes); // Set peer for destination await oapp.hasPeer(eid); // Check if peer configured await oapp.getPeer(eid); // Get peer address // DVN/Executor Configuration await oapp.setConfigMoveCall(tx, lib, eid, configType, config); // Set DVN/executor config await oapp.getConfig(lib, eid, configType); // Get current config // OApp Registration await oapp.registerOAppMoveCall(tx, oappObjectId, oappInfo); // Register with Endpoint await oapp.setOAppInfoMoveCall(tx, oappInfo); // Update OApp info // Enforced Options await oapp.setEnforcedOptionsMoveCall(tx, eid, msgType, options); // Set minimum execution params await oapp.getEnforcedOptions(eid, msgType); // Get enforced options await oapp.combineOptions(eid, msgType, extraOptions); // Combine with user options // Admin Operations await oapp.setDelegateMoveCall(tx, newDelegate); // Transfer admin rights await oapp.setSendLibraryMoveCall(tx, dstEid, library); // Set custom send library await oapp.setReceiveLibraryMoveCall(tx, srcEid, library, grace); // Set custom receive library // Channel Management await oapp.initChannelMoveCall(tx, remoteEid, remoteOApp); // Initialize messaging channel await oapp.skipMoveCall(tx, srcEid, sender, nonce); // Skip stuck message await oapp.clearMoveCall(tx, srcEid, sender, nonce, guid, msg); // Clear verified message ``` ### OFT SDK Methods The OFT SDK provides token-specific operations: ```typescript wrap theme={null} const oft = new OFT(sdk, oftPackageId, oftObjectId, tokenType, oappObjectId); // Initialization initOftMoveCall(tx, coinType, ticket, oapp, treasury, metadata, sharedDecimals); initOftAdapterMoveCall(tx, coinType, ticket, oapp, metadata, sharedDecimals); // Registration (auto-generates lz_receive_info!) await registerOAppMoveCall(tx, coinType, oftObj, oappObj, composerMgr); // Rate Limiting await setRateLimitMoveCall(tx, eid, inbound, limit, windowSeconds); // Set rate limit await unsetRateLimitMoveCall(tx, eid, inbound); // Remove rate limit await rateLimitConfig(eid, inbound); // Get config await rateLimitCapacity(eid, inbound); // Get remaining capacity await rateLimitInFlight(eid, inbound); // Get current usage // Fee Management await setFeeBpsMoveCall(tx, eid, feeBps); // Set fee for pathway await setDefaultFeeBpsMoveCall(tx, feeBps); // Set default fee await setFeeDepositAddressMoveCall(tx, address); // Set fee recipient await unsetFeeBpsMoveCall(tx, eid); // Remove pathway fee await effectiveFeeBps(eid); // Get effective fee await defaultFeeBps(); // Get default fee await feeDepositAddress(); // Get fee recipient await hasOftFee(eid); // Check if fee configured // Pause Control await setPauseMoveCall(tx, paused); // Pause/unpause OFT await isPaused(); // Check pause status // Queries await sharedDecimals(); // Get shared decimals await decimalConversionRate(); // Get conversion rate await isAdapter(); // Check if adapter mode await adminCap(); // Get AdminCap address await oappObject(); // Get OApp object ID await oftVersion(); // Get OFT version await coinMetadata(); // Get metadata ID ``` *** ## Core Methods ### quote() Get a fee quote for sending tokens crosschain: ```typescript wrap theme={null} const {nativeFee, lzTokenFee} = await oft.quote( client, { payer: keypair.toIOTAAddress(), tokenMint: '0x...', // Coin type tokenEscrow: '0x...', // OFT escrow object }, { dstEid: 30101, // Destination endpoint ID (e.g., Ethereum) to: Buffer.from('0x' + '1'.repeat(64), 'hex'), // 32-byte recipient address amountLD: BigInt(1000000), // Amount in local decimals minAmountLD: BigInt(950000), // Minimum amount (slippage) options: Buffer.from([]), // Execution options composeMsg: undefined, // Optional compose message payInLzToken: false, // Pay fee in native or LZ token }, ); console.log(`Native fee: ${nativeFee} wei`); console.log(`LZ token fee: ${lzTokenFee} wei`); ``` ### send() Send tokens crosschain: ```typescript wrap theme={null} const receipt = await oft.send( client, { payer: keypair, // Signer keypair tokenMint: '0x...', // Coin type tokenEscrow: '0x...', // OFT escrow object tokenSource: '0x...', // Source token account }, { dstEid: 30101, to: Buffer.from(recipientBytes32), amountLD: BigInt(1000000), minAmountLD: BigInt(950000), options: Buffer.from([]), composeMsg: undefined, nativeFee: nativeFee, lzTokenFee: BigInt(0), }, ); console.log('Transaction:', receipt.digest); ``` ### getOFTConfig() Read OFT configuration: ```typescript wrap theme={null} const config = await oft.getOFTConfig(client); console.log('Token type:', config.tokenType); console.log('Shared decimals:', config.sharedDecimals); console.log('Endpoint:', config.endpoint); ``` ### getPeer() Get peer OFT address for a specific chain: ```typescript wrap theme={null} const peer = await oft.getPeer(client, 30101); // Ethereum console.log('Peer address:', Buffer.from(peer).toString('hex')); ``` ## Building Execution Options Use the `Options` helper from the core SDK: ```typescript wrap theme={null} import {Options} from '@layerzerolabs/lz-v2-utilities'; // For EVM destination const options = Options.newOptions() .addExecutorLzReceiveOption(60000, 0) // gas limit, msg.value .toBytes(); // For IOTA/Solana destination with ATA/object creation const optionsWithValue = Options.newOptions() .addExecutorLzReceiveOption(200000, 2039280) // gas + rent .toBytes(); ``` ### Gas Limits by Destination | Destination | Recommended Gas Limit | Notes | | ----------- | --------------------- | -------------------------------------- | | EVM chains | 60,000 - 200,000 | Higher for complex logic | | Solana | 200,000 | May need msg.value for ATA | | IOTA | 200,000+ | May need msg.value for object creation | | Aptos | 100,000 | Adjust based on complexity | ### msg.value Considerations When sending **to IOTA**, you may need to include `msg.value` for: * Creating a new coin object for the recipient * Storage rent for the new object Calculate rent based on object size (typically \~0.002 IOTA). ## Complete Example ### Sending Tokens from IOTA to Ethereum ```typescript wrap theme={null} import {IOTAClient, getFullnodeUrl} from '@iota/iota-sdk/client'; import {Ed25519Keypair} from '@iota/iota-sdk/keypairs/ed25519'; import {OFT} from '@layerzerolabs/lz-iotal1-oft-sdk-v2'; import {Options} from '@layerzerolabs/lz-v2-utilities'; async function sendTokens() { // Setup const client = new IOTAClient({url: getFullnodeUrl('mainnet')}); const keypair = Ed25519Keypair.deriveKeypair(process.env.MNEMONIC!); const oft = new OFT({ client, oftAddress: process.env.OFT_PACKAGE!, oftStoreId: process.env.OFT_STORE!, }); // Prepare params const dstEid = 30101; // Ethereum const recipient = '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'; // Remove 0x, pad to 32 bytes const recipientBytes32 = Buffer.from(recipient.padStart(64, '0'), 'hex'); const amount = BigInt(1_000000); // 1 token (6 decimals) const minAmount = BigInt(950000); // 5% slippage // Build options const options = Options.newOptions().addExecutorLzReceiveOption(60000, 0).toBytes(); // Quote fee console.log('Getting quote...'); const {nativeFee} = await oft.quote( client, { payer: keypair.toIOTAAddress(), tokenMint: process.env.TOKEN_TYPE!, tokenEscrow: process.env.OFT_ESCROW!, }, { dstEid, to: recipientBytes32, amountLD: amount, minAmountLD: minAmount, options: Buffer.from(options), composeMsg: undefined, payInLzToken: false, }, ); console.log(`Fee: ${nativeFee / BigInt(1e9)} IOTA`); // Send tokens console.log('Sending tokens...'); const receipt = await oft.send( client, { payer: keypair, tokenMint: process.env.TOKEN_TYPE!, tokenEscrow: process.env.OFT_ESCROW!, tokenSource: process.env.TOKEN_ACCOUNT!, }, { dstEid, to: recipientBytes32, amountLD: amount, minAmountLD: minAmount, options: Buffer.from(options), composeMsg: undefined, nativeFee, lzTokenFee: BigInt(0), }, ); console.log('- Sent!'); console.log('Transaction:', receipt.digest); console.log('Track at: https://layerzeroscan.com'); } sendTokens().catch(console.error); ``` ## Reading Token Balances Check OFT token balances using the IOTA client: ```typescript wrap theme={null} import {IOTAClient} from '@iota/iota-sdk/client'; const client = new IOTAClient({url: getFullnodeUrl('mainnet')}); // Get all coins of a specific type for an address const coins = await client.getCoins({ owner: '0x...', coinType: '0x...::token::TOKEN', }); const totalBalance = coins.data.reduce((sum, coin) => sum + BigInt(coin.balance), BigInt(0)); console.log('Balance:', totalBalance.toString()); ``` ## Integration with Core SDK The OFT SDK builds on the core IOTA SDK: ```typescript wrap theme={null} import {createEndpointClient, createOAppClient} from '@layerzerolabs/lz-iotal1-sdk-v2'; // For lower-level Endpoint interactions const endpoint = createEndpointClient({ address: '0x...', }); // For OApp functionality const oapp = createOAppClient({ address: '0x...', }); ``` ## Error Handling Common errors and how to handle them: ```typescript wrap theme={null} try { const receipt = await oft.send(/* ... */); } catch (error) { if (error.message.includes('Insufficient funds')) { console.error('Not enough tokens or IOTA for gas'); } else if (error.message.includes('Invalid peer')) { console.error('Peer not configured for destination chain'); } else if (error.message.includes('Channel not initialized')) { console.error('Must initialize channel first'); } else { console.error('Unknown error:', error); } } ``` ## Admin Functions The SDK provides admin functions for OFT management (requires `AdminCap`): ### Pause/Unpause ```typescript wrap theme={null} // Pause OFT operations (emergency) await oft.setPauseMoveCall(tx, true); // Unpause await oft.setPauseMoveCall(tx, false); ``` ### Fee Configuration ```typescript wrap theme={null} // Set default fee rate (in basis points, 10000 = 100%) await oft.setDefaultFeeBpsMoveCall(tx, 30); // 0.3% fee // Set fee for specific destination await oft.setFeeBpsMoveCall(tx, 30101, 50); // 0.5% for Ethereum // Set fee deposit address await oft.setFeeDepositAddressMoveCall(tx, feeRecipientAddress); ``` ### Rate Limiting ```typescript wrap theme={null} // Set outbound rate limit await oft.setOutboundRateLimitMoveCall(tx, { dstEid: 30101, limit: BigInt(1000000), // Max tokens per window window: 86400, // 24 hours in seconds }); // Set inbound rate limit await oft.setInboundRateLimitMoveCall(tx, { srcEid: 30101, limit: BigInt(1000000), window: 86400, }); ``` ### Peer Configuration ```typescript wrap theme={null} // Set peer OFT on destination chain await oft.setPeerMoveCall(tx, 30101, peerBytes32); ``` ## Best Practices 1. **Always Quote First**: Get fee estimates before sending 2. **Set Slippage**: Use `minAmountLD` to protect against dust/precision loss 3. **Check Balances**: Verify sufficient tokens and IOTA for gas 4. **Use TypeScript**: Leverage type safety for parameter validation 5. **Test on Testnet**: Always test on testnet before mainnet deployments 6. **Monitor Rate Limits**: Configure appropriate limits for production 7. **Secure Admin Cap**: Use multisig or hardware wallet for admin operations ## Next Steps * [OFT Overview](/v2/developers/iota/oft/overview) - OFT architecture and deployment guide * [Configuration Guide](/v2/developers/iota/configuration/dvn-executor-config) - DVN, executor, and gas setup * [OApp Overview](/v2/developers/iota/oapp/overview) - Base messaging standard * [Technical Overview](/v2/developers/iota/technical-overview) - IOTA fundamentals and architecture * [Protocol Overview](/v2/developers/iota/protocol-overview) - Complete message workflows * [Troubleshooting](/v2/developers/iota/troubleshooting/common-errors) - Common SDK issues # LayerZero V2 IOTA L1 Packages Source: https://docs.layerzero.network/v2/developers/iota/overview Overview of IOTA L1 Packages on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure crosschain messaging. The LayerZero Protocol on IOTA L1 consists of several Move packages designed to facilitate the secure movement of data, tokens, and digital assets between different blockchain environments. LayerZero provides [**IOTA L1 Move Packages**](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/iota/contracts) that can communicate directly with the equivalent [Solidity Contract Libraries](/v2/developers/evm/overview) and other blockchain implementations deployed across supported chains. ## IOTA L1 and LayerZero IOTA L1 uses the [Move programming language](https://docs.iota.org/developer/iota-101/move-overview/move-overview) and employs a unique execution model based on [**Programmable Transaction Blocks (PTBs)**](https://docs.iota.org/developer/iota-101/transactions/ptb/programmable-transaction-blocks) and the **Call pattern** (a hot potato implementation using Move's [ability system](https://move-book.com/reference/abilities) where objects without `drop` or `store` must be explicitly consumed) to achieve crosschain functionality without traditional dynamic dispatch. ### IOTA L1 Move Packages Learn how the LayerZero V2 Protocol operates on the IOTA L1 blockchain. Deep dive into IOTA L1 object model, Call pattern, and PTB execution. Build the instructions necessary for sending arbitrary data and external function calls crosschain on IOTA L1. Create and send Omnichain Fungible Tokens (OFTs) on the IOTA L1 blockchain. Use the TypeScript SDK to interact with IOTA OFTs programmatically. #### IOTA Protocol Configurations Configure which decentralized verifier networks (DVNs) secure your messages. Configure who executes your messages on the destination chain. Set the amount of gas to deliver to the destination chain.
You can find all [**LayerZero IOTA L1 Packages**](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/iota/contracts) here. ### Tooling and Resources IOTA L1 development relies on the [Move programming language](https://docs.iota.org/developer/iota-101/move-overview/move-overview) and the [IOTA CLI](https://docs.iota.org/developer/references/cli). For comprehensive information, see the [IOTA Documentation](https://docs.iota.org/). LayerZero provides developer tooling to simplify the package development, testing, and deployment process: [LayerZero Scan](/v2/developers/layerzero-scan-explorer): a comprehensive crosschain explorer, search, API, and analytics platform for tracking and debugging your omnichain transactions. **TypeScript SDKs**: * [`@layerzerolabs/lz-iotal1-sdk-v2`](https://www.npmjs.com/package/@layerzerolabs/lz-iotal1-sdk-v2): Core SDK for interacting with LayerZero on IOTA L1 * [`@layerzerolabs/lz-iotal1-oft-sdk-v2`](https://www.npmjs.com/package/@layerzerolabs/lz-iotal1-oft-sdk-v2): OFT-specific SDK for token operations You can also ask for help or follow development in the [Discord](https://discord.com/invite/ktbvm8Nkcr). # LayerZero V2 IOTA L1 Protocol Implementation Source: https://docs.layerzero.network/v2/developers/iota/protocol-overview Overview of IOTA L1 Protocol Implementation on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure... This page provides a deep technical dive into the LayerZero V2 protocol implementation on IOTA, documenting the complete message lifecycle with actual contract code, function signatures, and transaction analysis. **What you'll find**: * Complete send workflow (7 steps from OApp to MessagingReceipt) * DVN verification and commit process with storage management * Executor delivery and OApp receive handling * Real transaction analysis from mainnet * Event emissions and monitoring * Recovery operations (skip, clear, nilify, burn) **Target audience**: Developers who understand IOTA basics and want to deeply understand the protocol implementation. ### Prerequisites Before reading this page, familiarize yourself with IOTA fundamentals in [Technical Overview](/v2/developers/iota/technical-overview). For SDK usage and practical implementation, see [OFT SDK](/v2/developers/iota/oft/sdk) or implementation guides for [OApp](/v2/developers/iota/oapp/overview) and [OFT](/v2/developers/iota/oft/overview). *** This page documents the complete message lifecycle with contract-level implementation details: * **Send Workflow:** Message initiation, fee calculation, nonce management, and packet dispatch * **Verification Workflow:** DVN submission, threshold checking, and verification commitment * **Receive Workflow:** Executor delivery, payload clearing, and OApp processing ## Send Overview When a user sends a crosschain message, the following high-level steps occur within a single Programmable Transaction Block (PTB): 1. **OApp Initiates Send:** User calls the OApp module's `send()` function, which creates a `Call` object 2. **Endpoint Processes:** The Endpoint increments the outbound nonce, constructs a packet with GUID, and routes to the send library 3. **ULN302 Assigns Jobs:** The message library creates child `Call` objects for the executor and each DVN 4. **Workers Calculate Fees:** Executor and DVNs estimate their fees and return `FeeRecipient` results 5. **Confirmation Chain:** Results flow back through confirm functions, aggregating fees and emitting events 6. **OApp Finalizes:** The OApp confirms the send call to extract the `MessagingReceipt` ### Endpoint Send The `EndpointV2` module orchestrates message sending through its shared object. #### EndpointV2 Shared Object ```rust wrap theme={null} /// The main endpoint object that coordinates all crosschain messaging operations public struct EndpointV2 has key { id: UID, eid: u32, // This chain's LayerZero endpoint ID call_cap: CallCap, // Capability for creating calls oapp_registry: OAppRegistry, // Registry of all registered OApps composer_registry: ComposerRegistry, // Registry for compose handlers message_lib_manager: MessageLibManager, // Manages send/receive libraries } ``` #### MessagingChannel per OApp Each OApp gets a dedicated `MessagingChannel` [shared object](https://docs.iota.org/developer/iota-101/objects/object-ownership/shared) for parallel execution: ```rust wrap theme={null} /// Shared object managing message channels for a specific OApp public struct MessagingChannel has key { id: UID, oapp: address, // OApp owner of this channel channels: Table, // Maps (eid, remote_oapp) → channel state is_sending: bool, // Prevents reentrancy } /// Composite key identifying a specific channel path public struct ChannelKey has copy, drop, store { remote_eid: u32, // Destination endpoint ID remote_oapp: Bytes32, // Remote OApp address (32 bytes) } /// State for a specific channel path public struct Channel has store { outbound_nonce: u64, // Next nonce for sends lazy_inbound_nonce: u64, // Last cleared (executed) nonce inbound_payload_hashes: Table, // Verified messages awaiting execution } ``` #### Step 1: OApp Creates Send Call The OApp module creates a `Call` object targeting the Endpoint: ```rust wrap theme={null} /// From oapp::send() public fun send( self: &mut OApp, oapp_cap: &CallCap, // Proves OApp ownership dst_eid: u32, // Destination endpoint ID message: vector, // Message payload options: vector, // Execution options native_fee: Coin, // Fee payment in IOTA lz_token_fee: Option>, // Optional ZRO payment refund_address: address, // Address for refunds ctx: &mut TxContext, ): Call { self.assert_oapp_cap(oapp_cap); // Lookup peer address for destination let receiver = self.peer.get_peer(dst_eid); // Combine enforced options with provided options let final_options = self.enforced_options.combine_options(dst_eid, SEND_MSG_TYPE, options); // Create send parameters let send_param = endpoint_send::create_param( dst_eid, receiver, message, final_options, native_fee, lz_token_fee, refund_address, ); // Create Call object targeting the Endpoint call::create(oapp_cap, endpoint!(), false, send_param, ctx) } ``` **Key Points**: * Returns a `Call` object (hot potato—must be consumed) * The `Call` has no `drop` or `store` abilities * PTB must route this `Call` to the Endpoint module * `oapp_cap` validates ownership via ID comparison #### Step 2: Endpoint Processes Send The Endpoint receives the `Call`, manages state, and delegates to the send library: ```rust wrap theme={null} /// From endpoint_v2::send() public fun send( self: &EndpointV2, messaging_channel: &mut MessagingChannel, call: &mut Call, ctx: &mut TxContext, ): Call { // Validate Call came from the OApp that owns this channel call.assert_caller(messaging_channel.oapp()); // Get the configured send library for this destination let (send_lib, _) = self.message_lib_manager.get_send_library( call.caller(), call.param().dst_eid() ); // Create outbound packet with incremented nonce // This is where the nonce++ happens: let send_param = messaging_channel.send(self.eid(), call.param()); // Create child Call targeting the send library call.create_single_child(&self.call_cap, send_lib, send_param, ctx) } ``` **Inside `messaging_channel.send()`**: ```rust wrap theme={null} /// From messaging_channel::send() public(package) fun send( self: &mut MessagingChannel, src_eid: u32, param: &EndpointSendParam, ): MessageLibSendParam { assert!(!self.is_sending, ESendReentrancy); self.is_sending = true; // Get or create the channel for this destination let channel_key = ChannelKey { remote_eid: param.dst_eid(), remote_oapp: param.receiver() }; if (!self.channels.contains(channel_key)) { // Initialize channel if first send let channel = Channel { outbound_nonce: 0, lazy_inbound_nonce: 0, inbound_payload_hashes: table::new(ctx), }; self.channels.add(channel_key, channel); }; // Increment nonce let channel = &mut self.channels[channel_key]; channel.outbound_nonce = channel.outbound_nonce + 1; // Build packet with GUID let packet = outbound_packet::create( channel.outbound_nonce, src_eid, self.oapp, // Sender address param.dst_eid(), param.receiver(), param.message(), ); // Create send parameters for message library message_lib_send::create_param( packet, param.options(), param.pay_in_zro(), param.native_fee_ref(), param.lz_token_fee_ref(), ) } ``` **GUID Generation**: Uses [keccak256 hashing](https://docs.iota.org/references/framework/hash) with [BCS-encoded](https://github.com/iotaledger/iota/blob/main/docs/content/concepts/cryptography/system) parameters: ```rust wrap theme={null} /// From outbound_packet module public fun create(...): OutboundPacket { let guid = hash::keccak256!( &vector[ nonce.to_be_bytes(), // Convert to bytes (big-endian) src_eid.to_be_bytes(), sender.to_bytes(), dst_eid.to_be_bytes(), receiver.data(), message, ] ); OutboundPacket { nonce, src_eid, sender, dst_eid, receiver, guid, message } } ``` #### Step 3: ULN302 Assigns Jobs to Workers The ULN302 message library creates child `Call` objects for the executor and DVNs: ```rust wrap theme={null} /// From uln_302::send() public fun send( self: &Uln302, call: &mut Call, ctx: &mut TxContext, ): (Call, MultiCall) { call.assert_caller(endpoint!()); assert!(self.is_supported_eid(call.param().base().packet().dst_eid()), EUnsupportedEid); // Get executor and DVN parameters from SendUln let (executor, executor_param, dvns, dvn_params) = self.send_uln.send(call.param()); // Create a new child batch (capacity: 1 executor + N DVNs) call.new_child_batch(&self.call_cap, 1); // Create child Call for each DVN let dvn_calls = dvns.zip_map!(dvn_params, |dvn, param| call.create_child(&self.call_cap, dvn, param, false, ctx) ); // Create child Call for executor (marked as last child) let executor_call = call.create_child(&self.call_cap, executor, executor_param, true, ctx); // Return executor call and MultiCall wrapper for DVN calls (executor_call, multi_call::create(&self.call_cap, dvn_calls)) } ``` **Inside `send_uln::send()`**: ```rust wrap theme={null} /// From send_uln::send() - prepares worker parameters public(package) fun send( self: &SendUln, param: &MessageLibSendParam, ): (address, ExecutorAssignJobParam, vector
, vector) { let packet = param.base().packet(); let sender = packet.sender(); let dst_eid = packet.dst_eid(); // Get effective executor configuration (OApp-specific or default) let executor_config = self.get_executor_config(sender, dst_eid); // Validate message size assert!(packet.message().length() <= executor_config.max_message_size(), EInvalidMessageSize); // Split options into executor and DVN options let (executor_options, dvn_options) = worker_options::split_worker_options(param.base().options()); // Create executor job parameters let executor_param = executor_assign_job::create_param( packet.guid(), packet.dst_eid(), sender, packet.message().length(), executor_options, ); // Get effective ULN configuration (OApp-specific or default) let uln_config = self.get_uln_config(sender, dst_eid); // Encode packet header for DVN verification let packet_header = packet_v1_codec::encode_packet_header(packet); let payload_hash = packet_v1_codec::payload_hash(packet); // Create DVN job parameters for each configured DVN let (dvns, dvn_params) = self.create_dvn_params( packet.guid(), packet.dst_eid(), sender, packet_header, payload_hash, uln_config, dvn_options, ); (executor_config.executor(), executor_param, dvns, dvn_params) } ``` #### Step 4: Workers Process Job Assignments **Executor Assignment**: ```rust wrap theme={null} /// From executor::assign_job() public fun assign_job( self: &Executor, call: &mut Call, ctx: &mut TxContext, ): Call { // Extract parameters let param = *call.param().base(); // Create child call to fee library for fee calculation self.create_feelib_get_fee_call(call, param, ctx) } /// Executor confirms fee calculation public fun confirm_assign_job( self: &Executor, executor_call: &mut Call, feelib_call: Call, ) { // Destroy fee library call and extract fee let (_, _, fee) = executor_call.destroy_child(self.worker.worker_cap(), feelib_call); // Complete executor call with FeeRecipient executor_call.complete( self.worker.worker_cap(), fee_recipient::create(fee, self.worker.deposit_address()) ); } ``` **DVN Assignment** (similar pattern): ```rust wrap theme={null} /// From dvn::assign_job() public fun assign_job( self: &DVN, call: &mut Call, ctx: &mut TxContext, ): Call { let param = *call.param().base(); self.create_feelib_get_fee_call(call, param, ctx) } ``` #### Step 5: ULN302 Confirms Send The ULN302 destroys worker `Call` objects and aggregates fees: ```rust wrap theme={null} /// From uln_302::confirm_send() public fun confirm_send( self: &Uln302, endpoint: &EndpointV2, treasury: &Treasury, messaging_channel: &mut MessagingChannel, endpoint_call: &mut Call, mut send_library_call: Call, executor_call: Call, dvn_multi_call: MultiCall, ctx: &mut TxContext, ) { send_library_call.assert_caller(endpoint!()); // Destroy DVN calls and collect fee recipients let (mut dvns, mut dvn_recipients) = (vector[], vector[]); dvn_multi_call.destroy(&self.call_cap).do!(|dvn_call| { let (dvn, _, dvn_recipient) = send_library_call.destroy_child(&self.call_cap, dvn_call); dvns.push_back(dvn); dvn_recipients.push_back(dvn_recipient); }); // Destroy executor call and collect fee recipient let (executor, _, executor_recipient) = send_library_call.destroy_child(&self.call_cap, executor_call); // Calculate total fees and encoded packet let send_result = send_uln::confirm_send( send_library_call.param(), executor, executor_recipient, dvns, dvn_recipients, treasury, ); send_library_call.complete(&self.call_cap, send_result); // Call endpoint for final confirmation let (native_token, zro_token) = endpoint.confirm_send( &self.call_cap, messaging_channel, endpoint_call, send_library_call, ctx, ); // Distribute fees to workers send_uln::handle_fees(treasury, executor_recipient, dvn_recipients, native_token, zro_token, ctx); } ``` **Inside `send_uln::confirm_send()`**: ```rust wrap theme={null} public(package) fun confirm_send( param: &MessageLibSendParam, executor: address, executor_recipient: FeeRecipient, dvns: vector
, dvn_recipients: vector, treasury: &Treasury, ): MessageLibSendResult { let packet = param.base().packet(); // Aggregate worker fees let mut native_recipients = vector[executor_recipient]; native_recipients.append(dvn_recipients); let total_native_fee = native_recipients.fold!(0, |acc, r| acc + r.fee()); // Calculate treasury fee let (treasury_recipient, zro_recipient) = treasury.quote_treasury_fee( packet.sender(), packet.dst_eid(), total_native_fee, param.base().pay_in_zro() ); if (treasury_recipient.fee() > 0) { native_recipients.push_back(treasury_recipient); }; let zro_recipients = if (zro_recipient.fee() > 0) { vector[zro_recipient] } else { vector[] }; // Encode packet for event emission let encoded_packet = packet_v1_codec::encode_packet(packet); // Emit events event::emit(ExecutorFeePaidEvent { guid: packet.guid(), executor, fee: executor_recipient }); event::emit(DVNFeePaidEvent { guid: packet.guid(), dvns, fees: dvn_recipients }); // Return result with fee recipients message_lib_send::create_result(encoded_packet, native_recipients, zro_recipients) } ``` #### Step 6: Endpoint Finalizes Send ```rust wrap theme={null} /// From endpoint_v2::confirm_send() public fun confirm_send( self: &EndpointV2, send_library: &CallCap, // Library's capability (static call) messaging_channel: &mut MessagingChannel, endpoint_call: &mut Call, send_library_call: Call, ctx: &mut TxContext, ): (Coin, Coin) { messaging_channel.assert_ownership(endpoint_call.caller()); // Destroy library call and extract results let (send_lib, param, result) = endpoint_call.destroy_child(&self.call_cap, send_library_call); assert!(send_lib == send_library.id(), EUnauthorizedSendLibrary); // Process fee payment and emit events let (receipt, paid_native_token, paid_zro_token) = messaging_channel.confirm_send( send_lib, endpoint_call.param_mut(&self.call_cap), param, result, ctx, ); // Complete the Call with MessagingReceipt endpoint_call.complete(&self.call_cap, receipt); // Return collected fees for distribution (paid_native_token, paid_zro_token) } ``` **Inside `messaging_channel.confirm_send()`**: ```rust wrap theme={null} public(package) fun confirm_send( self: &mut MessagingChannel, send_library: address, endpoint_param: &mut EndpointSendParam, message_lib_param: MessageLibSendParam, result: MessageLibSendResult, ctx: &mut TxContext, ): (MessagingReceipt, Coin, Coin) { // Extract fee recipients let (encoded_packet, native_recipients, zro_recipients) = result.destroy(); // Calculate total fees required let total_native_fee = native_recipients.fold!(0, |acc, r| acc + r.fee()); let total_zro_fee = zro_recipients.fold!(0, |acc, r| acc + r.fee()); // Split coins from endpoint_param let paid_native = coin::split(endpoint_param.native_fee_mut(), total_native_fee, ctx); let paid_zro = if (total_zro_fee > 0) { coin::split(endpoint_param.lz_token_fee_mut().borrow_mut(), total_zro_fee, ctx) } else { coin::zero(ctx) }; // Emit PacketSentEvent event::emit(PacketSentEvent { encoded_packet, options: *message_lib_param.base().options(), send_library, }); // Create receipt let packet = message_lib_param.base().packet(); let receipt = messaging_receipt::create(packet.guid(), packet.nonce(), total_native_fee, total_zro_fee); // Reset sending flag self.is_sending = false; (receipt, paid_native, paid_zro) } ``` #### Step 7: OApp Extracts Receipt The OApp confirms the send call to extract the receipt: ```rust wrap theme={null} /// From oapp::confirm_lz_send() public fun confirm_lz_send( self: &OApp, oapp_cap: &CallCap, call: Call, ): (SendParam, MessagingReceipt) { self.assert_oapp_cap(oapp_cap); // Destroy the Call and extract results let (endpoint, param, receipt) = call.destroy(oapp_cap); assert!(endpoint == endpoint!(), EOnlyEndpoint); (param, receipt) } ``` ### Example PTB for Send (from actual transaction) Based on transaction `HXZqH1RdANEkstz3MTFGMuLQ74CfgAkwQCq1YW8TMHHH`: ```javascript wrap theme={null} // PTB commands in order: 1. SplitCoins - Split fee from sender's IOTA 2. MoveCall - bytes32::from_bytes (convert recipient to Bytes32) 3. MoveCall - send_param::create (create SendParam struct) 4. MoveCall - oft_sender::tx_sender (create OFTSender context) 5. SplitCoins - Split token amount from sender's coin 6. MoveCall - oft::send (initiate OFT send, returns Call) 7. MoveCall - endpoint::send (route Call to endpoint) 8. MoveCall - uln_302::send (create worker calls) 9. MoveCall - executor::assign_job (executor processes) 10. MoveCall - dvn::assign_job (each DVN processes) 11. MoveCall - executor::confirm_assign_job 12. MoveCall - dvn::confirm_assign_job (for each DVN) 13. MoveCall - uln_302::confirm_send 14. MoveCall - oft::confirm_send (extract receipt) // Events emitted: - ExecutorFeePaidEvent - DVNFeePaidEvent - PacketSentEvent - OFTSentEvent ``` ### Send Limitations #### Max Message Size The `maxMessageSize` is configured per executor and OApp: ```rust wrap theme={null} /// From ExecutorConfig struct public struct ExecutorConfig has copy, drop, store { executor: address, // Executor address max_message_size: u64, // Maximum message bytes (default varies by network) } ``` Default is typically 10,000 bytes, but OApps can configure custom limits. #### Fee Payment Model Unlike EVM's direct fee transfer, IOTA uses `Coin` object splitting: ```rust wrap theme={null} // Split exact fee amount from provided coins let paid_native = coin::split(native_fee_coin, required_fee, ctx); // Transfer to fee recipient transfer::public_transfer(paid_native, recipient_address); // Refund excess transfer::public_transfer(remaining_coin, refund_address); ``` *** ## Verification Workflow After the `PacketSentEvent` is emitted on the source chain, DVNs independently verify the message on the destination chain. ### DVN Verification Process #### Step 1: DVN Monitors Source Chain DVNs watch for `PacketSentEvent` and wait for the configured number of block confirmations (finality). #### Step 2: DVN Submits Verification Each DVN calls the `verify()` function on the ULN302: ```rust wrap theme={null} /// From uln_302::verify() public fun verify( self: &Uln302, verification: &mut Verification, // Shared verification object call: Call, ) { let dvn = call.caller(); // DVN's CallCap proves identity let param = call.complete_and_destroy(&self.call_cap); // Store verification in the Verification shared object receive_uln::verify( verification, dvn, *param.packet_header(), param.payload_hash(), param.confirmations() ) } ``` **Inside `receive_uln::verify()`**: ```rust wrap theme={null} /// From receive_uln::verify() public(package) fun verify( self: &mut Verification, dvn: address, packet_header: vector, payload_hash: Bytes32, confirmations: u64, ) { // Create confirmation key let key = ConfirmationKey { header_hash: hash::keccak256!(&packet_header), payload_hash, dvn, }; // Store confirmations in Table table_ext::upsert!(&mut self.confirmations, key, confirmations); // Emit event for monitoring event::emit(PayloadVerifiedEvent { dvn, header: packet_header, confirmations, proof_hash: payload_hash, }); } ``` **Verification Storage**: ```rust wrap theme={null} /// Shared object storing all DVN confirmations public struct Verification has key { id: UID, // Maps (header_hash, payload_hash, dvn) → confirmation_count confirmations: Table, } ``` ### Commit Verification After sufficient DVNs have verified (meeting the X of Y of N threshold), anyone can call `commit_verification()`: ```rust wrap theme={null} /// From uln_302::commit_verification() public fun commit_verification( self: &Uln302, endpoint: &EndpointV2, verification: &mut Verification, messaging_channel: &mut MessagingChannel, packet_header: vector, payload_hash: Bytes32, clock: &Clock, ) { // Verify and reclaim storage from Verification object let header = self.receive_uln.verify_and_reclaim_storage( verification, endpoint.eid(), packet_header, payload_hash, ); // Call endpoint to insert verified payload hash endpoint.verify( &self.call_cap, // Library capability messaging_channel, // Destination OApp's channel header.src_eid(), // Source endpoint ID header.sender(), // Source OApp address header.nonce(), // Message nonce payload_hash, // Payload hash clock, // For timeout validation ); } ``` **Inside `receive_uln::verify_and_reclaim_storage()`**: ```rust wrap theme={null} public(package) fun verify_and_reclaim_storage( self: &ReceiveUln, verification: &mut Verification, local_eid: u32, encoded_packet_header: vector, payload_hash: Bytes32, ): PacketHeader { // Decode and validate packet header let header = packet_v1_codec::decode_header(encoded_packet_header); assert!(header.dst_eid() == local_eid, EInvalidEid); let header_hash = hash::keccak256!(&encoded_packet_header); let receiver = header.receiver(); let src_eid = header.src_eid(); // Get effective ULN configuration let uln_config = self.get_uln_config(receiver, src_eid); // Check all required DVNs have verified let mut verified_count = 0; uln_config.required_dvns().do!(|dvn| { let key = ConfirmationKey { header_hash, payload_hash, dvn: *dvn }; assert!(verification.confirmations.contains(key), EVerifying); // Remove confirmation (reclaim storage) verification.confirmations.remove(key); verified_count = verified_count + 1; }); // Check optional DVN threshold is met if (uln_config.optional_dvn_count() > 0) { let mut optional_verified = 0; uln_config.optional_dvns().do!(|dvn| { let key = ConfirmationKey { header_hash, payload_hash, dvn: *dvn }; if (verification.confirmations.contains(key)) { verification.confirmations.remove(key); optional_verified = optional_verified + 1; }; }); assert!(optional_verified >= uln_config.optional_dvn_threshold(), EVerifying); }; header } ``` ### Endpoint Verify The Endpoint inserts the verified payload hash into the messaging channel: ```rust wrap theme={null} /// From endpoint_v2::verify() public fun verify( self: &EndpointV2, receive_library: &CallCap, // Library's capability messaging_channel: &mut MessagingChannel, src_eid: u32, sender: Bytes32, nonce: u64, payload_hash: Bytes32, clock: &Clock, ) { // Validate receive library is authorized self.message_lib_manager.assert_receive_library( messaging_channel.oapp(), src_eid, receive_library.id(), clock ); // Insert payload hash into messaging channel messaging_channel.verify(src_eid, sender, nonce, payload_hash); } ``` **Inside `messaging_channel::verify()`**: ```rust wrap theme={null} public(package) fun verify( self: &mut MessagingChannel, src_eid: u32, sender: Bytes32, nonce: u64, payload_hash: Bytes32, ) { assert!(payload_hash != EMPTY_PAYLOAD_HASH, EInvalidPayloadHash); // Get or create channel for this pathway let channel_key = ChannelKey { remote_eid: src_eid, remote_oapp: sender }; if (!self.channels.contains(channel_key)) { self.init_channel(src_eid, sender); }; // Insert payload hash into the channel let channel = &mut self.channels[channel_key]; channel.inbound_payload_hashes.add(nonce, payload_hash); // Emit verification event event::emit(PacketVerifiedEvent { src_eid, sender, nonce, receiver: self.oapp, payload_hash, }); } ``` **Message State Transition**: ``` Send → PacketSentEvent emitted on source chain ↓ DVNs monitor and verify (off-chain) ↓ DVNs call verify() (onchain submission) ↓ Verification confirmations stored in Verification object ↓ commit_verification() checks threshold ↓ Payload hash inserted into MessagingChannel ↓ Message ready for execution ``` *** ## Receive Workflow After verification is committed, the Executor can deliver the message to the destination OApp. ### Executor Delivery The Executor initiates message delivery by constructing a PTB with all required objects. #### Step 1: Executor Queries OApp Metadata The Executor queries the OApp's execution metadata to determine which objects are needed: ```rust wrap theme={null} // OApp implements this to provide execution metadata public fun get_oapp_info(oapp: &OApp): vector { // Returns encoded OAppInfoV1 containing required Move calls } ``` #### Step 2: Executor Creates PTB Based on the transaction `9fqmkJYFQyQs6u1vVmMSuqhZyobpSW7P4i7MaNVzbSFg`, the PTB contains: ```javascript wrap theme={null} 1. MoveCall - bytes32::from_bytes (decode sender) 2. MoveCall - bytes32::from_bytes (decode receiver) 3. MoveCall - option::none> (no value transfer) 4. MoveCall - executor_worker::execute_lz_receive (entry point) 5. MoveCall - counter::lz_receive (OApp business logic) Objects passed: - Executor shared object (immutable reference) - Executor capability (owned object) - EndpointV2 shared object (immutable reference) - MessagingChannel shared object (mutable reference) - Clock object (for validation) - Counter OApp shared object (mutable reference) - Counter Peer object (immutable reference) ``` #### Step 3: Executor Calls execute\_lz\_receive ```rust wrap theme={null} /// From executor::execute_lz_receive() public fun execute_lz_receive( self: &Executor, endpoint: &EndpointV2, messaging_channel: &mut MessagingChannel, src_eid: u32, sender: Bytes32, nonce: u64, guid: Bytes32, message: vector, extra_data: vector, value: Option>, ctx: &mut TxContext, ): Call { // Create lz_receive call via endpoint endpoint.lz_receive( &self.worker.worker_cap(), // Executor's capability messaging_channel, src_eid, sender, nonce, guid, message, extra_data, value, ctx, ) } ``` #### Step 4: Endpoint Creates lz\_receive Call ```rust wrap theme={null} /// From endpoint_v2::lz_receive() public fun lz_receive( self: &EndpointV2, executor: &CallCap, // Executor's capability messaging_channel: &mut MessagingChannel, src_eid: u32, sender: Bytes32, nonce: u64, guid: Bytes32, message: vector, extra_data: vector, value: Option>, ctx: &mut TxContext, ): Call { // Clear the payload first (prevents reentrancy) messaging_channel.clear(src_eid, sender, nonce, guid, &message); // Create lz_receive parameters let lz_receive_param = lz_receive::create_param( src_eid, sender, nonce, guid, message, extra_data, value, ); // Create Call object targeting the OApp call::create( executor, // Executor creates the Call messaging_channel.oapp(), // Target: OApp address true, // One-way call (no result expected) lz_receive_param, ctx, ) } ``` **Inside `messaging_channel::clear()`**: ```rust wrap theme={null} public(package) fun clear( self: &mut MessagingChannel, src_eid: u32, sender: Bytes32, nonce: u64, guid: Bytes32, message: &vector, ) { let channel_key = ChannelKey { remote_eid: src_eid, remote_oapp: sender }; let channel = &mut self.channels[channel_key]; // Lazy nonce update: clear all messages up to this nonce if (nonce > channel.lazy_inbound_nonce) { let mut i = channel.lazy_inbound_nonce + 1; while (i <= nonce) { assert!(channel.inbound_payload_hashes.contains(i), EInvalidNonce); i = i + 1; }; channel.lazy_inbound_nonce = nonce; }; // Verify payload hash matches verified hash let expected_hash = channel.inbound_payload_hashes[nonce]; let actual_hash = hash::keccak256!(&vector[guid.data(), *message]); assert!(expected_hash == actual_hash, EPayloadHashNotFound); // Remove from storage (prevents double execution) channel.inbound_payload_hashes.remove(nonce); // Emit delivery event event::emit(PacketDeliveredEvent { src_eid, sender, receiver: self.oapp, nonce, }); } ``` **Key Security Features**: 1. **Lazy nonce validation**: Ensures all prior messages have been verified 2. **Payload hash verification**: Confirms executor provided the correct message 3. **Storage cleanup**: Removes hash to prevent double execution 4. **Event emission**: Signals successful delivery #### Step 5: OApp Processes Message The OApp's `lz_receive()` function is invoked via the `Call` object: ```rust wrap theme={null} /// Example from counter OApp public fun lz_receive( self: &mut Counter, peer: &Peer, call: Call, ) { // Validate Call came from Endpoint let (callee, param, _) = call.complete_and_destroy(&self.call_cap); assert!(callee == endpoint_address(), EOnlyEndpoint); // Validate sender is configured peer assert!(param.sender() == peer.address, EOnlyPeer); // Process message (application-specific logic) self.count = self.count + 1; // Note: No need to manually call clear() - already done by Endpoint } ``` **OApp Responsibilities**: * * Validate `Call` came from authorized Endpoint * * Validate sender matches configured peer * * Process message and update state * * No need to call `clear()` (done by Endpoint before Call creation) ### Example PTB for Receive (from actual transaction) Based on transaction `9fqmkJYFQyQs6u1vVmMSuqhZyobpSW7P4i7MaNVzbSFg`: ```javascript wrap theme={null} // PTB commands in order: 1. MoveCall - bytes32::from_bytes (decode sender parameter) 2. MoveCall - bytes32::from_bytes (decode guid parameter) 3. MoveCall - option::none> (no native token transfer) 4. MoveCall - executor_worker::execute_lz_receive - Passes: Executor, Endpoint, MessagingChannel, src_eid, sender, nonce, guid, message - Returns: Call 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 \ --gas-budget 10000000 ``` The Endpoint creates a dedicated `MessagingChannel` shared object for each OApp. Use recovery entry functions on the Endpoint (requires `AdminCap`): **Skip a message** (increment nonce without execution): ```bash wrap theme={null} iota client call \ --package \ --module endpoint_v2 \ --function skip \ --args \ --gas-budget 10000000 ``` **Clear a message** (mark as delivered without execution): ```bash wrap theme={null} iota client call \ --function clear \ --args \ --gas-budget 10000000 ``` See [Common Errors](/v2/developers/iota/troubleshooting/common-errors) for more recovery options. ## Security Questions Follow these capability-based security practices: 1. **Validate CallCap in All Functions**: ```rust wrap theme={null} public fun send( self: &OApp, oapp_cap: &CallCap, // - Require capability ... ) { self.assert_oapp_cap(oapp_cap); // - Validate ownership // ... } fun assert_oapp_cap(self: &OApp, cap: &CallCap) { assert!(self.oapp_cap.id() == cap.id(), EInvalidOAppCap); } ``` 2. **Validate Call Objects**: ```rust wrap theme={null} public fun lz_receive(self: &mut OApp, call: Call) { // - Validate Call came from authorized Endpoint let (callee, param, _) = call.destroy(&self.oapp_cap); assert!(callee == endpoint_address(), EOnlyEndpoint); // - Validate sender is configured peer let peer = self.peer.get_peer(param.src_eid); assert!(param.sender == peer, EOnlyPeer); } ``` 3. **Secure Capability Objects**: * Store `CallCap` in package module storage (not transferred) * Use multisig or hardware wallet for `AdminCap` * Never expose capabilities publicly * Transfer `AdminCap` carefully (use `transfer::public_transfer`) 4. **Protect UpgradeCap**: * Keep upgrade authority secure * Consider freezing upgrades after deployment (`package::make_immutable`) * Use multisig for mainnet upgrade authority * * Missing `CallCap` validation in functions * * Not validating `Call` object source (callee address) * * Skipping peer validation in `lz_receive` * * Losing capability objects (no recovery possible) * * Wrong peer addresses configured * * Exposing `AdminCap` or `CallCap` publicly See [OApp Best Practices](/v2/developers/iota/oapp/overview#best-practices) for details. ## Next Steps * [Common Errors](/v2/developers/iota/troubleshooting/common-errors) * [Technical Overview](/v2/developers/iota/technical-overview) * [Configuration Guide](/v2/developers/iota/configuration/dvn-executor-config) * [IOTA Guidance](/v2/developers/iota/technical-reference/iota-guidance) # LayerZero Scan Explorer Source: https://docs.layerzero.network/v2/developers/layerzero-scan-explorer Use LayerZero Scan to track crosschain messages. Monitor transaction status, verify deliveries, and debug omnichain applications. Step-by-step instructions ... # Solana Composers Source: https://docs.layerzero.network/v2/developers/solana/composer/overview Overview of Solana Composers on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure crosschain messaging. Crosschain composability enables multi-step workflows that span multiple chains. LayerZero V2 supports composing follow-up actions as separate messages, improving reliability and flexibility for complex flows. This page explains how to implement composability for Solana programs using the `lz_compose_types_v2` typed discovery flow. ## Prerequisites * Familiarity with [Anchor](https://www.anchor-lang.com/) and Solana CPIs. * Read the conceptual guide: [Omnichain Composers](/v2/concepts/applications/composer-standard) * Understanding of [Solana OApps](/v2/developers/solana/oapp/overview) and the PDA used * Understanding of the `lz_receive` v2 flow (see [lz\_receive\_types\_v2](/v2/developers/solana/oapp/overview#lz_receive_types_v2)) ## Why composability matters For a clear, high-level explanation of both the how and the why behind composed messaging, see the conceptual guide: [Omnichain Composers](/v2/concepts/applications/composer-standard). ## How composability works A message may or may not contain a compose message. When it does, we refer to it as a **composed message**. The following is the workflow for a composed message when the destination chain is Solana. A composed flow is split into distinct steps across messages: 1. **Sending Application**: The sender OApp sends a message with a `composeMsg` attached. For composed messages to Solana, the recipient address should be set to the **Composer PDA**. 2. **Receiving Application**: The destination OApp's `lz_receive` is executed, and since there is a `composeMsg`, a `send_compose` CPI is made to the Endpoint program to send the compose message to the Composer PDA. 3. **Composer Application**: The Executor calls `lz_compose` on the Composer Program (the program that owns the Composer PDA). > A **Composer** is the smart contract that is responsible for executing a compose message. This separation reduces call-stack complexity and allows non-critical reverts in the composed step without rolling back the initial receive. ## Installation You can either extend your existing OApp or OFT program to turn it into a Composer, or create a standalone program to serve as the Composer. In this example, we will scaffold a basic Solana OApp, and extend it to also be a Composer. Use the CLI to scaffold a Solana OApp project you can extend with compose: ```bash wrap theme={null} LZ_ENABLE_SOLANA_OAPP_EXAMPLE=1 npx create-lz-oapp@latest --example oapp-solana ``` ## Usage The accounts and instructions needed are similar to those outlined in [lz\_receive\_types\_v2](/v2/developers/solana/oapp/overview#lz_receive_types_v2): * `composer` - a PDA that can be used to store static addresses needed for `lz_compose` execution, and more importantly, whose address is used as the 'Composer address' * `LzComposeTypesAccounts` - a PDA that contains the accounts needed to call `lz_compose_types` * `lz_compose_types_info`- an instruction that provides versioning info that helps the Executor understand how to proceed with `lz_compose_types`/`lz_compose_types_v2` * `lz_compose_types_v2` - an instruction that returns the list of accounts and execution plan needed to execute `lz_compose` * `lz_compose` - the instruction that contains the actual business logic that must be executed for a `composeMsg` ## `lz_compose_types_v2` `lz_compose_types_v2` achieves similar goals to `lz_receive_types_v2` (supports more accounts, multiple instructions, multiple signers) but for compose messages. The flow is similar: discover versioned accounts via `lz_compose_types_info`, return a compact, ALT-aware execution plan via `lz_compose_types_v2`, then the Executor builds and submits the transaction that includes the `lz_compose` instruction. Note that for this example, we will assume that the Composer is integrated into the OApp program itself. Whether you adopt this design as well depends on your use case. You may also choose to have a standalone Composer program. ### Implementing `lz_compose_types_v2` #### Composer PDA Define the composer PDA seed in your program's `lib.rs`: ```rust wrap theme={null} const COMPOSER_SEED: &[u8] = b"Composer"; ``` Define the struct of the PDA that will hold the static addresses or fields that will be used in `lz_compose_types` later: ```rust wrap theme={null} #[account] pub struct Composer { pub endpoint_program: Pubkey, // if you need to namespace your Composer PDA, you can add the identifier here // you can add other fields as needed pub bump: u8, } impl Composer { // 8 (discriminator) + 1 Pubkey + 1 bump pub const SIZE: usize = 8 + 1 * 32 + 1; } ``` #### LzComposeTypesAccounts Define the struct of PDA that holds versioned compose-type discovery data, e.g. `LzComposeTypesAccounts`: ```rust wrap theme={null} /// LzComposeTypesAccounts includes accounts that are used in the LzComposeTypesV2 instruction. #[account] #[derive(InitSpace)] pub struct LzComposeTypesAccounts { pub composer: Pubkey, // Note: The Composer PDA. // Example: You can also store a single ALT (or change to Vec for many) pub alt: Pubkey, // You may add more Pubkeys here per your use case pub bump: u8, } ``` Initialize the `LzComposeTypesAccounts` PDA in your `init` instruction: ```rust wrap theme={null} use crate::{ state::LzComposeTypesAccounts, STORE_SEED, }; use anchor_lang::prelude::*; use anchor_lang::solana_program::address_lookup_table::program::ID as ALT_PROGRAM_ID; use oapp::{ endpoint::{instructions::RegisterOAppParams}, LZ_COMPOSE_TYPES_SEED, }; #[derive(Accounts)] pub struct Init<'info> { // .. existing accounts including payer, store, lz_receive_types_accounts and its ALT /// PDA holding all the static pubkeys for lz_compose execution #[account( init, payer = payer, space = Composer::SIZE, seeds = [COMPOSER_SEED], // Note: if the composer needs to be namespaced, add the identifier into the seed here bump )] pub composer: Account<'info, Composer>, #[account( init, payer = payer, space = 8 + LzComposeTypesAccounts::INIT_SPACE, seeds = [LZ_COMPOSE_TYPES_SEED, composer.key().as_ref()], bump )] pub lz_compose_types_accounts: Account<'info, LzComposeTypesAccounts>, // Note: For simplicity, we will use the same ALT for both lz_receive_types_accounts and lz_compose_types_accounts, but you can also accept two separate ALTs if you want the ability to specify them separately. // If you choose to specify them separately, then you can name them something like lz_receive_alt and lz_compose_alt, and just modify the instruction handler below to use the right account keys #[account(owner = ALT_PROGRAM_ID)] pub alt: Option>, // optional. pub system_program: Program<'info, System>, } impl Init<'_> { pub fn apply(ctx: &mut Context, params: &InitParams) -> Result<()> { // existing code ctx.accounts.composer.endpoint_program = params.endpoint_program; ctx.accounts.composer.bump = ctx.bumps.composer; ctx.accounts.lz_compose_types_accounts.composer = ctx.accounts.composer.key(); ctx.accounts.lz_compose_types_accounts.alt = ctx.accounts.alt.as_ref().map(|a| a.key()).unwrap_or_default(); ctx.accounts.lz_compose_types_accounts.bump = ctx.bumps.lz_compose_types_accounts; // existing code } } ``` * If you need the Composer PDA to be namespaced by a certain identifier, you can add it into its `seeds`. The convention is to also store that value in the Composer PDA itself, which requires amending its struct that was defined earlier. * The above assumes that the existing `init` instruction that was already responsible for initializing `lz_receive_types_accounts` is extended to also initialize `lz_compose_types_accounts` * The `init` function can be arbitrarily named, as long as it is called #### lz\_compose\_types\_info Create an `lz_compose_types_info` instruction that returns the version and the accounts needed to construct `lz_compose_types_v2`: ```rust wrap theme={null} use oapp::{ lz_compose_types_v2::{LzComposeTypesV2Accounts, LZ_COMPOSE_TYPES_VERSION}, LzComposeParams, LZ_COMPOSE_TYPES_SEED, }; use crate::*; #[derive(Accounts)] pub struct LzComposeTypesInfo<'info> { #[account(seeds = [COMPOSER_SEED], bump = composer.bump)] pub composer: Account<'info, Composer>, #[account(seeds = [LZ_COMPOSE_TYPES_SEED, composer.key().as_ref()], bump = lz_compose_types_accounts.bump)] pub lz_compose_types_accounts: Account<'info, LzComposeTypesAccounts>, } impl LzComposeTypesInfo<'_> { /// Returns (version, versioned_data) used by the Executor pub fn apply( ctx: &Context, _params: &LzComposeParams, ) -> Result<(u8, LzComposeTypesV2Accounts)> { let composer = &ctx.accounts.composer; let compose_types_account = &ctx.accounts.lz_compose_types_accounts; let required_accounts = if compose_types_account.alt == Pubkey::default() { vec![ // 1) composer PDA compose_types_account.composer ] } else { vec![ // 1) composer PDA compose_types_account.composer, // ALT will be passed under remaining_accounts compose_types_account.alt ] }; Ok((LZ_COMPOSE_TYPES_VERSION, LzComposeTypesV2Accounts { accounts: required_accounts })) } } ``` #### lz\_compose\_types\_v2 Implement `lz_compose_types_v2` and return a compact execution plan including exactly one `Instruction::LzCompose`: ```rust wrap theme={null} use crate::*; use oapp::{ common::{compact_accounts_with_alts, AccountMetaRef, AddressLocator, EXECUTION_CONTEXT_VERSION_1}, lz_compose_types_v2::{get_accounts_for_clear_compose, Instruction, LzComposeTypesV2Result}, LzComposeParams, }; #[derive(Accounts)] #[instruction(params: LzComposeParams)] pub struct LzComposeTypesV2<'info> { // 1) Composer PDA #[account(seeds = [COMPOSER_SEED], bump = composer.bump)] pub composer: Account<'info, Composer>, } impl LzComposeTypesV2<'_> { /// Returns the execution plan for lz_compose with a minimal account set. pub fn apply( ctx: &Context, params: &LzComposeParams, ) -> Result { let mut accounts = vec![ // 0) payer AccountMetaRef { pubkey: AddressLocator::Payer, is_writable: true }, // 1) endpoint program AccountMetaRef { pubkey: ctx.accounts.composer.endpoint_program, is_writable: false }, // 2) composer PDA AccountMetaRef { pubkey: ctx.accounts.composer.key().into(), is_writable: false }, ]; // Endpoint helper accounts for compose let accounts_for_composing = get_accounts_for_clear_compose( ctx.accounts.composer.endpoint_program, ¶ms.from, &ctx.accounts.composer.key(), ¶ms.guid, params.index, ¶ms.message, ); accounts.extend(accounts_for_composing); Ok(LzComposeTypesV2Result { context_version: EXECUTION_CONTEXT_VERSION_1, alts: ctx.remaining_accounts.iter().map(|alt| alt.key()).collect(), instructions: vec![ Instruction::LzCompose { // In this example, ALTs are passed in via remaining_accounts // This decision allows for flexibility in terms of passing in any number of ALTs without needing to change the accounts struct // However, if you need stronger schema guarantees and require only a single ALT, you may opt to have it passed in explicitly via ctx.accounts.alt (or similar) accounts: compact_accounts_with_alts(&ctx.remaining_accounts, accounts)?, }, ], }) } } ``` ### Composed Message Execution Options Longer composed flows increase the cost of executing `lz_receive` on the destination and the follow-up `lz_compose` call. You must set sufficient gas and value in your Message Options for both steps. * Add extra gas for the lzReceive step: ```ts wrap theme={null} // addExecutorLzReceiveOption(uint128 _gas, uint128 _value) Options.newOptions().addExecutorLzReceiveOption(50000, 0); ``` * Also set gas/value for the composer execution: ```ts wrap theme={null} // addExecutorLzComposeOption(uint16 _index, uint128 _gas, uint128 _value) Options.newOptions().addExecutorLzReceiveOption(50000, 0).addExecutorLzComposeOption(0, 30000, 0); ``` Parameters: * `_index`: the index of the `composeMsg`. * `_gas`: gas/compute budget for the destination execution. * `_value`: native value forwarded with the call if needed. If insufficient limits are provided, execution will not proceed and a manual retry with higher limits will be required. For an overview of how options work, see [Message Options](/v2/concepts/message-options). ### Composing an OFT #### Sending an OFT from Solana with a compose message The [OFT program (Solana)](/v2/developers/solana/oft/overview) supports attaching an optional [compose\_msg](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/programs/oft/src/instructions/send.rs#L172) to a crosschain send. ```rust wrap theme={null} pub struct SendParams { pub dst_eid: u32, pub to: [u8; 32], pub amount_ld: u64, pub min_amount_ld: u64, pub options: Vec, pub compose_msg: Option>, // <---- Optional compose_msg param pub native_fee: u64, pub lz_token_fee: u64, } ``` #### Handling a compose message for an OFT on Solana The reference [Solana OFT program](https://github.com/LayerZero-Labs/devtools/tree/main/examples/oft-solana/programs/oft)'s `lz_receive` already [includes a call to `Endpoint::send_compose`for when a message contains a `compose_msg`](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/programs/oft/src/instructions/lz_receive.rs#L155-L173) : ```rust wrap theme={null} if let Some(message) = msg_codec::compose_msg(¶ms.message) { oapp::endpoint_cpi::send_compose( ctx.accounts.oft_store.endpoint_program, ctx.accounts.oft_store.key(), &ctx.remaining_accounts[Clear::MIN_ACCOUNTS_LEN..], seeds, SendComposeParams { to: ctx.accounts.to_address.key(), guid: params.guid, index: 0, // only 1 compose msg per lzReceive message: compose_msg_codec::encode( params.nonce, params.src_eid, amount_received_ld, &message, ), }, )?; } ``` The OFT Program comes with a default [compose\_msg\_codec](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/programs/oft/src/compose_msg_codec.rs) that's used for sending and receiving composed messages: ```rust wrap theme={null} const NONCE_OFFSET: usize = 0; const SRC_EID_OFFSET: usize = 8; const AMOUNT_LD_OFFSET: usize = 12; const COMPOSE_FROM_OFFSET: usize = 20; const COMPOSE_MSG_OFFSET: usize = 52; pub fn encode( nonce: u64, src_eid: u32, amount_ld: u64, compose_msg: &Vec, // [composeFrom][composeMsg] ) -> Vec { let mut encoded = Vec::with_capacity(20 + compose_msg.len()); // 8 + 4 + 8 encoded.extend_from_slice(&nonce.to_be_bytes()); encoded.extend_from_slice(&src_eid.to_be_bytes()); encoded.extend_from_slice(&amount_ld.to_be_bytes()); encoded.extend_from_slice(&compose_msg); encoded } pub fn nonce(message: &[u8]) -> u64 { let mut nonce_bytes = [0; 8]; nonce_bytes.copy_from_slice(&message[NONCE_OFFSET..SRC_EID_OFFSET]); u64::from_be_bytes(nonce_bytes) } pub fn src_eid(message: &[u8]) -> u32 { let mut src_eid_bytes = [0; 4]; src_eid_bytes.copy_from_slice(&message[SRC_EID_OFFSET..AMOUNT_LD_OFFSET]); u32::from_be_bytes(src_eid_bytes) } pub fn amount_ld(message: &[u8]) -> u64 { let mut amount_ld_bytes = [0; 8]; amount_ld_bytes.copy_from_slice(&message[AMOUNT_LD_OFFSET..COMPOSE_FROM_OFFSET]); u64::from_be_bytes(amount_ld_bytes) } pub fn compose_from(message: &[u8]) -> [u8; 32] { let mut compose_from = [0; 32]; compose_from.copy_from_slice(&message[COMPOSE_FROM_OFFSET..COMPOSE_MSG_OFFSET]); compose_from } pub fn compose_msg(message: &[u8]) -> Vec { if message.len() > COMPOSE_MSG_OFFSET { message[COMPOSE_MSG_OFFSET..].to_vec() } else { Vec::new() } } ``` The reference [Solana OFT program](https://github.com/LayerZero-Labs/devtools/tree/main/examples/oft-solana/programs/oft), however, does not include a dedicated `lz_compose` handler as the decision of whether to extend the OFT program or have a separate Composer program is for the developer to decide. If your product needs compose flows, use `lz_compose_types_v2` to describe execution and choose one of these patterns: * Create a separate composer program (recommended for separation of concerns). * Modify the OFT program to also act as the composer (tighter coupling). ### Composing an OApp As shown above in ["Composing an OFT"](#composing-an-oft), the `send_compose` call is issued from within `lz_receive` after `Endpoint::clear`. Custom OApps follow the same pattern: detect a compose payload, then forward it via `send_compose` to your Composer PDA (or destination PDA). Ensure your discovery accounts stay in sync with the execution plan described by [`lz_compose_types_v2`](#lz_compose_types_v2). For general receive flow details, see OApp [lz\_receive — business logic + Endpoint::clear](/v2/developers/solana/oapp/overview). ## Execution notes and troubleshooting * Ensure `lz_receive_types` and `lz_compose_types` PDAs are initialized and kept in sync with your handlers. * Always call `Endpoint::clear` before touching user state in `lz_receive`. * The Executor enforces a fee-limited execution context; keep instruction counts and compute within limits. * If account ordering mismatches, you may see `AccountNotWritable`/`InvalidProgramId`—double-check discovery (return of `lz_compose_types`) vs execution handler (`lz_compose`) expectations. ## Next steps * Review the [Solana OApp Reference](/v2/developers/solana/oapp/overview) for flows and PDAs. * See EVM counterpart: [Omnichain Composers (EVM)](/v2/developers/evm/composer/overview) for conceptual parity. # Solana DVN and Executor Configuration Source: https://docs.layerzero.network/v2/developers/solana/configuration/dvn-executor-config Configure Solana DVN and Executor Configuration for your LayerZero application. Set up DVNs, executors, and pathway settings for crosschain messaging. Before setting your DVN and Executor Configuration, you should review the [Security Stack Core Concepts](../../../concepts/modular-security/security-stack-dvns). **Production deployments should use multiple required DVNs from independent operators.** A single-DVN configuration means a compromise of that one verifier results in unrestricted forged messages on the pathway. See the [Integration Checklist](../../../tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway) for production DVN guidance. You can manually configure your Solana OApp’s Send and Receive settings by: * **Reading Defaults:** Use the `get_config` method to see default configurations. * **Setting Libraries:** Call `set_send_library` and `set_receive_library` to choose the correct Message Library version. * **Setting Configs:** Use the `set_config` instruction to update your custom DVN and Executor settings. For both Send and Receive configurations, make sure that for a given [channel](../../../concepts/glossary#channel--lossless-channel): * **Send (Chain A) settings** match the **Receive (Chain B) settings.** * DVN addresses are provided in alphabetical order. * Block confirmations are correctly set to avoid mismatches. ### Use the LayerZero CLI The LayerZero CLI has abstracted these calls for every supported chain. See the [**CLI Setup Guide**](../../../get-started/create-lz-oapp/start) to easily deploy, configure, and send messages using LayerZero. ### Getting the Default Config If you had set up your project using the LayerZero CLI, run the following to view the default configs: ```bash wrap theme={null} npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` Alternatively, you can also retrieve it via the following script. ```typescript wrap theme={null} import {UlnProgram} from '@layerzerolabs/lz-solana-sdk-v2'; import {Connection} from '@solana/web3.js'; const connection = new Connection('https://api.devnet.solana.com'); // replace with the desired Solana cluster's RPC URL const uln: UlnProgram.Uln = new UlnProgram.Uln(UlnProgram.PROGRAM_ID); const defaultSendConfig = await uln.getDefaultSendConfigState(connection, dstEid); const defaultReceiveConfig = await uln.getDefaultReceiveConfigState(connection, dstEid); console.log({ defaultSendConfig, defaultReceiveConfig, }); ```
The script will return both the default SendLib and ReceiveLib configurations. In the SendLib is also the `executor` address. ```bash wrap theme={null} { defaultSendConfig: _SendConfig { bump: 255, uln: { confirmations: , requiredDvnCount: 1, optionalDvnCount: 0, optionalDvnThreshold: 0, requiredDvns: [Array], optionalDvns: [] }, executor: { maxMessageSize: 10000, executor: [PublicKey [PublicKey(AwrbHeCyniXaQhiJZkLhgWdUCteeWSGaSN1sTfLiY7xK)]] } }, defaultReceiveConfig: _ReceiveConfig { bump: 255, uln: { confirmations: , requiredDvnCount: 1, optionalDvnCount: 0, optionalDvnThreshold: 0, requiredDvns: [Array], optionalDvns: [] } } } ``` The important takeaway is that every LayerZero Endpoint can be used to send and receive messages. Because of that, **each Endpoint has a separate Send and Receive Configuration**, which an OApp can configure by the target destination Endpoint. In the above example, the default Send Library configurations control how messages emit from the **Solana Endpoint** to the BNB Endpoint. The default Receive Library configurations control how the **Solana Endpoint** filters received messages from the BNB Endpoint. For a configuration to be considered correct, **the Send Library configurations on Chain A must match Chain B's Receive Library configurations for filtering messages.** **Challenge:** Confirm that the Solana Endpoint's Send Library ULN configuration matches the Ethereum Endpoint's Receive Library ULN Configuration using the methods above. ## Custom Configuration ### LayerZero CLI The [**create-lz-oapp**](../../../get-started/create-lz-oapp/start#configuring-layerzero-contracts) (LayerZero CLI) npx package is the recommended way to start and maintain your project. For EVM and Solana projects, you will not need to write any custom scripting in order to view or set your OApp's configs. For projects created using the LayerZero CLI, all custom configurations are managed via the [LZ Config](/v2/concepts/glossary#lz-config) file (typically named `layerzero.config.ts`). You would modify the values in the LZ Config file and then run the `wire` command: ``` npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` The wire command would take care of preparing and submitting all transactions required to apply your configurations. It goes through each pathway and will submit transactions to each chain in your mesh. Regardless of how many pathways you have, you will only need to run the wire command once. We recommmend you to use the LayerZero CLI unless you have a custom use case that is not supported by it. ## Debugging Configurations A **correct** OApp configuration example: | SendUlnConfig (A to B) | ReceiveUlnConfig (B to A) | | ------------------------------------------------------- | ------------------------------------------------------- | | confirmations: 15 | confirmations: 15 | | optionalDVNCount: 0 | optionalDVNCount: 0 | | optionalDVNThreshold: 0 | optionalDVNThreshold: 0 | | optionalDVNs: Array(0) | optionalDVNs: Array(0) | | requiredDVNCount: 2 | requiredDVNCount: 2 | | requiredDVNs: Array(DVN1\_Address\_A, DVN2\_Address\_A) | requiredDVNs: Array(DVN1\_Address\_B, DVN2\_Address\_B) | The sending OApp's **SendLibConfig** (OApp on Chain A) and the receiving OApp's **ReceiveLibConfig** (OApp on Chain B) match! #### Block Confirmation Mismatch An example of an **incorrect** OApp configuration: | SendUlnConfig (A to B) | ReceiveUlnConfig (B to A) | | ------------------------------- | ------------------------------- | | **confirmations: 5** | **confirmations: 15** | | optionalDVNCount: 0 | optionalDVNCount: 0 | | optionalDVNThreshold: 0 | optionalDVNThreshold: 0 | | optionalDVNs: Array(0) | optionalDVNs: Array(0) | | requiredDVNCount: 2 | requiredDVNCount: 2 | | requiredDVNs: Array(DVN1, DVN2) | requiredDVNs: Array(DVN1, DVN2) | The above configuration has a **block confirmation mismatch**. The sending OApp (Chain A) will only wait 5 block confirmations, but the receiving OApp (Chain B) will not accept any message with less than 15 block confirmations. Messages will be blocked until either the sending OApp has increased the outbound block confirmations, or the receiving OApp decreases the inbound block confirmation threshold. #### DVN Mismatch Another example of an incorrect OApp configuration: | SendUlnConfig (A to B) | ReceiveUlnConfig (B to A) | | ----------------------------- | ----------------------------------- | | confirmations: 15 | confirmations: 15 | | optionalDVNCount: 0 | optionalDVNCount: 0 | | optionalDVNThreshold: 0 | optionalDVNThreshold: 0 | | optionalDVNs: Array(0) | optionalDVNs: Array(0) | | **requiredDVNCount: 1** | **requiredDVNCount: 2** | | **requiredDVNs: Array(DVN1)** | **requiredDVNs: Array(DVN1, DVN2)** | The above configuration has a **DVN mismatch**. The sending OApp (Chain A) only pays DVN 1 to listen and verify the packet, but the receiving OApp (Chain B) requires both DVN 1 and DVN 2 to mark the packet as verified. Messages will be blocked until either the sending OApp has added DVN 2's address on Chain A to the SendUlnConfig, or the receiving OApp removes DVN 2's address on Chain B from the ReceiveUlnConfig. #### [Dead DVN](../../../concepts/glossary#dead-dvn) This configuration includes a **Dead DVN**: | SendUlnConfig (A to B) | ReceiveUlnConfig (B to A) | | ----------------------------------- | ---------------------------------------- | | confirmations: 15 | confirmations: 15 | | optionalDVNCount: 0 | optionalDVNCount: 0 | | optionalDVNThreshold: 0 | optionalDVNThreshold: 0 | | optionalDVNs: Array(0) | optionalDVNs: Array(0) | | **requiredDVNCount: 2** | **requiredDVNCount: 2** | | **requiredDVNs: Array(DVN1, DVN2)** | **requiredDVNs: Array(DVN1, DVN\_DEAD)** | The above configuration has a **Dead DVN**. Similar to a DVN Mismatch, the sending OApp (Chain A) pays DVN 1 and DVN 2 to listen and verify the packet, but the receiving OApp (Chain B) has currently set DVN 1 and a Dead DVN to mark the packet as verified. Since a Dead DVN for all practical purposes should be considered a null address, no verification will ever match the dead address. Messages will be blocked until the receiving OApp removes or replaces the Dead DVN from the ReceiveUlnConfig. # Getting Started with LayerZero V2 on Solana Source: https://docs.layerzero.network/v2/developers/solana/getting-started Get started with Getting Started with on Solana. Step-by-step tutorial for building omnichain applications on LayerZero V2. LayerZero enables secure... Any data, whether it's a fungible token transfer, an NFT, or some other smart contract input can be encoded onchain as a bytes array, and delivered to a destination chain to trigger some action using LayerZero. Because of this, any blockchain that broadly supports state propagation and events can be connected to LayerZero, like **Solana**. If you're new to LayerZero, we recommend reviewing [**"What is LayerZero?"**](/v2/concepts/getting-started/what-is-layerzero) before continuing.
LayerZero provides sister **Solana Programs** that can communicate with the equivalent [Solidity Contract Libraries](/v2/developers/evm/overview) you deploy on the Ethereum Virtual Machine (EVM). These programs, like their solidity counterparts, simplify calling the [LayerZero Endpoint](../../concepts/protocol/layerzero-endpoint), provide message handling, interfaces for protocol configurations, and other utilities for interoperability: * **Omnichain Fungible Token (OFT)**: an extension of `OApp` built for handling and supporting omnichain SPL Token transfers. * **Omnichain Application (OApp)**: the base program utilities for omnichain messaging and configuration. Each of these programs standards implement common functions for **sending** and **receiving** omnichain messages. ## Differences from the Ethereum Virtual Machine The full differences between Solidity and Solana are outside the scope of this overview (e.g., see [A Complete Guide to Solana Development for Ethereum Developers](https://solana.com/developers/evm-to-svm/complete-guide) or [60 Days of Solana by RareSkills](https://www.rareskills.io/solana-tutorial). Skip this section if you already feel comfortable working within the Solana Virtual Machine (SVM) and the Solana Account Model. ### Writing Smart Contracts on Solana To create a new ERC20 tokens on an EVM-compatible blockchain, a developer will have to inherit and redeploy the ERC20 smart contract. **Solana is different.** Direct translation of Solidity contract inheritance to Solana is not possible because Rust does not have classes like Solidity. Instead, the [Solana Account Model](https://solana.com/docs/core/accounts) enables program reusability. Diagram comparing EVM ERC20 token deployment versus Solana SPL token creation, showing that Solana uses a single Token Program to create multiple Mint Accounts rather than deploying separate contracts Diagram comparing EVM ERC20 token deployment versus Solana SPL token creation, showing that Solana uses a single Token Program to create multiple Mint Accounts rather than deploying separate contracts Rather than deploying a new ERC20 smart contract for every new token you want to issue, you will instead send an [instruction](https://solana.com/docs/terminology#instruction) to the **Solana Token Program** and create a new account, known as the **Mint Account**, which defines a set of values based off the program's interface (e.g., the number of tokens in circulation, decimal points, who can mint more tokens, and who can freeze tokens). Diagram showing how the Solana Token Program creates Mint Accounts that define token properties like supply, decimals, mint authority, and freeze authority Diagram showing how the Solana Token Program creates Mint Accounts that define token properties like supply, decimals, mint authority, and freeze authority An account on Solana either is an executable program (i.e. a smart contract) or holds state data (e.g. how many tokens you have). Sometimes you’ll see Solana tokens referred to as “**SPL tokens**.” SPL stands for Solana Program Library, which is a set of Solana programs that the Solana team has deployed onchain. SPL tokens are similar to ERC20 tokens, since every SPL token has a standard set of functionality.
A [Program Derived Address (PDA)](https://solana.com/docs/core/pda#breadcrumbs) can then be used as the address (unique identifier) for an onchain account, providing a method to easily store, map, and fetch program state. For example, a user's wallet and the SPL Token Mint can be used to derive the [Token Account](https://solana.com/docs/core/tokens#token-account). Diagram illustrating Program Derived Addresses (PDAs) on Solana, showing how a user wallet and SPL Token Mint can be combined to derive a Token Account address Diagram illustrating Program Derived Addresses (PDAs) on Solana, showing how a user wallet and SPL Token Mint can be combined to derive a Token Account address To be compatible with the Solana Account Model, the **Omnichain Fungible Token (OFT) Program** extends the existing SPL token standard to interact with the LayerZero Endpoint smart contract. Architecture diagram showing how the OFT Program extends the SPL Token standard to interact with the LayerZero Endpoint for crosschain token transfers Architecture diagram showing how the OFT Program extends the SPL Token standard to interact with the LayerZero Endpoint for crosschain token transfers The typical path for Solana program development involves interacting with or deploying executable code that defines your specific implementation, and then having other developers mint accounts that want to use that interface (e.g., the **SPL Token Program** defines how tokens behave, and the **Mint Accounts** define the different brands of SPL tokens). The OFT Program is different in this respect. Because every Solana Program has an [Upgrade Authority](https://solana.com/docs/programs/deploying#overview-of-the-upgradeable-bpf-loader), and this authority can change or modify the implementation of all child accounts, developers wishing to create crosschain tokens on Solana will need to deploy their own instance of the **OFT Program** that will have their own **OFT Store** Account. Diagram showing the relationship between a deployed OFT Program and its OFT Store Account, illustrating that developers deploy their own OFT Program instance to maintain control over the Upgrade Authority Diagram showing the relationship between a deployed OFT Program and its OFT Store Account, illustrating that developers deploy their own OFT Program instance to maintain control over the Upgrade Authority This decision was made so that tokens minted off of the OFT Program will own their OFT's **Upgrade Authority**, rather than depend on LayerZero Labs to maintain a single, mutable OFT Program for all OFT Stores. See ["Why Auditing the Code is Not Enough: A Discussion on Solana Upgrade Authorities"](https://neodyme.io/en/blog/solana_upgrade_authority/#intro) for more information on how Upgrade Authorities behave on Solana. LayerZero Labs may eventually in the future maintain with another entity a version of the OFT Program which users can use to create OFT Store Accounts from, but for now developers should consider deploying their own version of the OFT Program. ### Writing Solana Programs Solana Programs are most commonly developed with Rust. LayerZero OApp Programs should also be written in Rust to take advantage of LayerZero Solana libraries. See an [Overview of Developing Onchain Programs](https://solana.com/docs/programs/overview) to learn more about Solana. While some initiatives exist to enable developers to write Solana programs in Solidity, compiling LayerZero Solidity Libraries using compilers like [**Neon EVM**](https://neon-labs.org/) or [**Solang**](https://solang.readthedocs.io/en/latest/) will **NOT** work with the Solana LayerZero Endpoint, because the LayerZero Rust Endpoint does not match 1:1 the Solidity Endpoint interface. # Interactive Solana Program Playground Source: https://docs.layerzero.network/v2/developers/solana/instructions-playground Test LayerZero Solana programs directly from your browser. Build and send instructions without writing code. Explore key program instructions for message... Test LayerZero Solana programs directly from your browser. Build and send instructions without writing code. Explore key program instructions for message fee calculation, sending, receiving, and configuration management. For production use, please use the official SDKs which provides proper type-safe instruction builders: * [@layerzerolabs/lz-solana-sdk-v2](https://www.npmjs.com/package/@layerzerolabs/lz-solana-sdk-v2) * [@layerzerolabs/oft-v2-solana-sdk](https://www.npmjs.com/package/@layerzerolabs/oft-v2-solana-sdk) ### Real Onchain Methods All instructions shown in this playground are **real methods** available in the LayerZero Solana programs today: * **Endpoint Program**: [Source Code](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/solana/programs/programs/endpoint/src) * **OFT Program**: [Source Code](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/programs/oft/src) We only document OApp-relevant instructions, excluding admin-only functions. State variables are clearly marked as direct account data reads, not instructions. ## LayerZero EndpointV2 The main entry point for all crosschain messaging operations on Solana. This program handles message routing, fee calculation, and configuration management. ### Message Routing Core functions for sending and receiving messages between smart contracts. #### quote() - Get Fee Estimates #### send() - Send Messages #### clear() - Clear Payload ### Why is clear() under Message Routing instead of Message Recovery? On Solana, the `clear()` instruction placement differs from EVM: * **EVM**: The Endpoint handles clearing payloads directly during message delivery * **Solana**: The OApp must explicitly call `clear()` via CPI in its `lzReceive` implementation This architectural difference means Solana developers need to implement the CPI call to `clear()` within their OApp's message handling logic, giving them more control over the message lifecycle but requiring explicit implementation. See the architectural note at the top of this page for the complete message flow. #### sendCompose() - Send Compose Messages #### clearCompose() - Clear Compose Message ### Architectural Difference: lzReceive If you're looking for `lzReceive` or `lzCompose` instructions in the Endpoint (as you might expect from EVM), note that on Solana these are implemented directly in the OApp (e.g., OFT program) due to architectural differences. **On Solana:** * The executor calls `lzReceive` directly on the OApp/OFT program * The OApp then makes a CPI (Cross-Program Invocation) call to the Endpoint's `clear` instruction to validate and clear the payload * After validation, the OApp continues with its business logic (e.g., minting tokens) This is different from EVM where the Endpoint calls back to the OApp's `_lzReceive` function. On Solana, the flow is inverted with the OApp calling into the Endpoint. See the [OFT lzReceive implementation](https://github.com/LayerZero-Labs/devtools/blob/73f732acf683ca18b74cd6c6adb3d656d2e0f36a/examples/oft-solana/programs/oft/src/instructions/lz_receive.rs#L72-L94) for an example. ### Configuration Management Functions for setting custom verification, execution, and pathway management. #### registerOapp() - Register OApp #### setDelegate() - Set Delegate Address #### setSendLibrary() - Configure Send Library #### setReceiveLibrary() - Configure Receive Library #### setConfig() - Set Configuration Parameters #### initNonce() - Initialize Nonce #### initVerify() - Initialize Verification #### initSendLibrary() - Initialize Send Library #### initReceiveLibrary() - Initialize Receive Library #### initConfig() - Initialize Configuration #### setReceiveLibraryTimeout() - Set Library Timeout ### Message Recovery & Security Functions for handling message exceptions, security threats, and recovery scenarios. #### burn() - Permanently Block Message #### skip() - Skip Inbound Nonce #### nilify() - Mark Message as Nil #### withdrawRent() - Withdraw Rent ### Status Checks Functions for querying current configuration settings, library assignments, nonce tracking, and message states. #### eid() - Get Endpoint ID #### endpointAdmin() - Get Endpoint Admin #### oappDelegate() - Get OApp Delegate #### outboundNonce() - Get Outbound Nonce #### inboundNonce() - Get Inbound Nonce ## Omnichain Fungible Token (OFT) Omnichain Fungible Token (OFT) enables seamless crosschain token transfers. Deploy once and bridge your SPL tokens to any supported blockchain. Since Solana uses a rent-based storage model rather than EVM's gas-per-bytecode deployment costs, and has no restrictive contract size limits (like EVM's 24KB limit), we can include all of these extensions in the same program. While some OFT instances may not utilize all features (like fees or rate limits), having them built-in provides maximum flexibility without the contract splitting requirements common in EVM development. ### Default OFT Program For Solana Mainnet, we use **PENGU OFT** as the default example: * **Program**: `EfRMrTJWU2CYm52kHmRYozQNdF8RH5aTi3xyeSuLAX2Y` * **OFT Store**: `qMNo1RFo11J9ZLGuq7dVmWAssuCZaNsSamk8g2q4UZA` You can replace these with your own OFT deployment addresses. ### lzReceive Implementation The `lzReceive` instruction is implemented here in the OFT program (not in the Endpoint). This is a key architectural difference from EVM: * On Solana, executors call `lzReceive` directly on the OApp/OFT * The OFT program then makes a CPI call to the Endpoint's `clear` instruction to validate the message * After successful validation, the OFT continues with its logic (minting tokens, updating balances, etc.) The flow is: Executor → OFT.lzReceive → Endpoint.clear (via CPI) → Continue OFT logic ### Send Tokens #### quoteSend() - Get Transfer Fees #### quoteOft() - Get Detailed Transfer Quote #### send() - Transfer Tokens #### lzReceiveTypes() - Get Receive Account Types #### lzReceive() - Receive Tokens ### Token Details The **OFT Store** account is a [Program Derived Address (PDA)](https://solana.com/docs/core/pda), not the token mint itself. This account stores essential OFT related state variables. #### oftAdmin() - Get OFT Admin #### tokenMint() - Get Token Mint #### tokenEscrow() - Get Token Escrow #### sharedDecimals() - Get Shared Decimals #### decimalConversionRate() - Get Decimal Conversion Rate #### oftVersion() - Get OFT Version #### tvlLd() - Get Total Value Locked #### isPaused() - Check Pause State #### defaultFeeBps() - Get Default Fee ### Peer Configuration These functions read peer-specific configuration from PeerConfig accounts: #### peerAddress() - Get Peer Address #### enforcedOptions() - Check Enforced Options #### peerFeeBps() - Get Peer Fee #### outboundRateLimiter() - Check Outbound Rate Limiter #### inboundRateLimiter() - Check Inbound Rate Limiter ### Finding PeerConfig Account The PeerConfig account is a PDA (Program Derived Address) derived from: * OFT program ID * Seeds: `[b"peer_config", oft_store.key().as_ref(), &dst_eid.to_be_bytes()]` You'll need to derive this address using the OFT store account and the destination chain's endpoint ID. ### Management Functions #### initOft() - Initialize OFT #### setPeerConfig() - Configure Remote Peer #### setOftConfig() - Update OFT Settings #### setPause() - Pause/Unpause OFT #### withdrawFee() - Withdraw Collected Fees ## Events and Errors ### Endpoint Events Key events emitted by the Endpoint program during crosschain operations. #### PacketSentEvent - Message Sent Emitted when a packet is sent through the endpoint. Contains the encoded packet data and execution options for tracking crosschain messages. #### PacketVerifiedEvent - Message Verified Emitted when an inbound message has been verified by the DVNs and is ready for execution. Indicates the message passed all security checks. #### PacketDeliveredEvent - Message Delivered Emitted when a message is successfully delivered to the destination OApp. This confirms the crosschain transaction completed. #### ComposeSentEvent - Compose Message Queued Emitted when a compose message is queued for sequential execution after the primary message. Used for complex multi-step operations. #### ComposeDeliveredEvent - Compose Message Executed Emitted when a compose message is successfully executed. Indicates the secondary operation completed successfully. #### DelegateSetEvent - Delegate Updated Emitted when an OApp delegate is set or changed. The delegate can configure settings on behalf of the OApp. #### SendLibrarySetEvent - Send Library Configured Emitted when the send library is configured for a specific destination. Tracks library changes for outbound messages. #### ReceiveLibrarySetEvent - Receive Library Configured Emitted when the receive library is configured for a specific source. Tracks library changes for inbound messages. #### OAppRegisteredEvent - OApp Registration Emitted when a new OApp is registered with the endpoint. This establishes the OApp's ability to send and receive messages. ### Endpoint Errors Common errors returned by the Endpoint program. #### Unauthorized - Permission Denied Thrown when the caller lacks required permissions for the operation. Only authorized addresses can perform certain actions. #### InvalidNonce - Nonce Mismatch Thrown when processing a message with an invalid nonce. Ensures messages are processed in the correct sequential order. #### InvalidSender - Unauthorized Sender Thrown when receiving a message from an unauthorized sender. Only configured peers can send messages to the OApp. #### InvalidReceiver - Invalid Destination Thrown when the specified receiver address is invalid or not configured properly for the destination chain. #### LzTokenUnavailable - LayerZero Token Error Thrown when LayerZero token operations fail or tokens are unavailable for fee payment. ### OFT Events Key events emitted by the OFT program during token operations. ([Source](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/programs/oft/src/events.rs)) #### OFTSent - Tokens Sent Crosschain Emitted when tokens are sent to another chain. Contains the message GUID, destination chain ID, sender and recipient addresses, and the amount sent in both shared and local decimals. #### OFTReceived - Tokens Received Emitted when tokens are received from another chain. Contains the source chain ID, sender address, and the amount received after decimal conversion. ### OFT Errors Common errors returned by the OFT program. ([Source](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/programs/oft/src/errors.rs)) #### Paused - OFT Operations Paused Thrown when attempting operations while the OFT is paused. No transfers can be initiated until unpaused by the designated unpauser. #### SlippageExceeded - Insufficient Received Amount Thrown when the received amount after crosschain transfer falls below the minimum acceptable amount due to decimal conversions or fees. #### RateLimitExceeded - Transfer Rate Limit Hit Thrown when a transfer exceeds the configured rate limits for the peer connection. Wait for the rate limit window to reset. #### InvalidOptions - Invalid Message Options Thrown when the provided LayerZero message options are invalid or incompatible with the OFT configuration. #### InvalidAmount - Invalid Transfer Amount Thrown when the transfer amount is zero, exceeds limits, or is otherwise invalid for the operation. #### InvalidPeer - Peer Not Configured Thrown when attempting to interact with a destination chain where no peer OFT has been configured. Use setPeerConfig() first. ## Usage Tips ### Getting Started 1. **Connect Your Wallet**: Click "Connect Phantom Wallet" to connect your Solana wallet 2. **Select Network**: Choose between Solana Mainnet and Devnet 3. **Custom RPC (Optional)**: If you encounter rate limits (403 errors), add a custom RPC URL: * [Helius](https://helius.dev) - Generous free tier * [QuickNode](https://quicknode.com) - Free tier available * [Alchemy](https://alchemy.com) - Professional services ### Common Workflows #### Sending Tokens Crosschain (OFT) 1. Initialize your OFT with `initOft()` 2. Configure peers with `setPeerConfig()` 3. Get a quote with `quoteOft()` or `quoteSend()` 4. Send tokens with `send()` #### Setting Up Messaging (Endpoint) 1. Register your OApp with `registerOapp()` 2. Initialize nonce tracking with `initNonce()` 3. Set up libraries with `initSendLibrary()` and `initReceiveLibrary()` 4. Configure DVNs/executors with `setConfig()` 5. Get quotes with `quote()` and send messages with `send()` ### Troubleshooting * **403 Errors**: Use a custom RPC URL instead of public endpoints * **"Account does not exist"**: Ensure all required accounts have been initialized * **"Invalid arguments"**: Check that byte arrays are properly formatted (0x prefix) * **Simulation failures**: This playground uses simplified encoding - use official SDKs for production For production applications, always use the official LayerZero SDKs which provide proper type safety and encoding. # LayerZero V2 Solana OApp Reference Source: https://docs.layerzero.network/v2/developers/solana/oapp/overview Overview of Solana OApp Reference on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure crosschain... The OApp Standard provides developers with a *generic message passing interface* to **send** and **receive** arbitrary pieces of data between contracts existing on different blockchain networks. How exactly the data is interpreted and what other actions they trigger, depend on the specific OApp implementation. ## Quickstart ### Example For the step-by-step instructions on how to build, deploy and wire a Solana OApp, view the [Solana OApp example](https://github.com/LayerZero-Labs/devtools/tree/main/examples/oapp-solana). ### Scaffold Spin up a new Solana OApp project (based on the example) in seconds: ```bash wrap theme={null} LZ_ENABLE_SOLANA_OAPP_EXAMPLE=1 npx create-lz-oapp@latest ``` Specify the directory, select `OApp (Solana)` and proceed with the installation. The example contains a string-passing OApp that works across **Solana** and **EVM**. Follow the provided README instructions to deploy the example and make your first crosschain message between Solana and an EVM chain. The following sections will highlight the several code excerpts of the Solana OApp that are essential to it functioning. ## Developing Solana OApps vs EVM OApps Due to VM and programming paradigm differences, developing OApps on Solana works differently from doing the same on EVM chains. On EVM chains, OApps can simply inherit the OApp contract standard to unlock crosschain functionality. On Solana, there is no similar inheritance. The table below outlines the main differences when developing: | Theme | EVM | Solana | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Endpoint integration pattern | OApp contracts inherit base contracts such as `OAppSender` and `OAppReceiver` which hides all endpoint calls. | No inheritance. Your OApp program **must** directly include the code that [CPIs](/v2/concepts/glossary#cpi-cross-program-invocation) the endpoint. | | Execution model & account discovery | Dynamic dispatch ⇒ `endpoint.lzReceive()` can `delegatecall` back into your contract without pre-knowing storage slots. | **All accounts required** for execution of `lz_receive()` must be listed up-front. This is done via the Executor calling the `lz_receive_types_v2` instruction. | | OApp identity & addressing | The OApp contract's address is used as the OApp address | The OApp program's address is not used as the OApp address. Instead, a [PDA](/v2/concepts/glossary#pda-program-derived-address) owned by the OApp program is used as the OApp address. For example, for OFTs, the OApp address is the OFT Store's address, which is owned by the OFT program. | | Configuration storage & ownership | Pathway configs are set in the storage of the Endpoint contract. | Pathway configs are stored in [PDAs](/v2/concepts/glossary#pda-program-derived-address) owned by the Endpoint program or the Send/Library program. | ## High-level message flow Due to the need for the inclusion of accounts, the flow of an inbound message on Solana is different from that on an EVM chain: ```text wrap theme={null} ┌────────┐ (1) msg packet to receiver PDA Source │ Src │ ──────────────────────────────────────────► Solana Chain │ OApp │ │ └────────┘ ▼ ┌─────────────────────────┐ │ Executor program │ └─────────────────────────┘ │ (2) Account + Instructions Discovery ──────┘ │ │ (3) CPI: `lz_receive` + other instructions ──────┘ │ │ ▼ ┌────────┐ │ Dst │ │ OApp │ └────────┘ (4) Endpoint and OApp state updated ``` ## Accounts and Instructions Discovery The discovery of accounts and instructions is handled by `lz_receive_types_v2`. ### What `lz_receive_types_v2` introduces `lz_receive_types_v2` has the following key features: * **Support for multiple ALTs**: Expands the capacity for account inclusion by allowing multiple Address Lookup Tables, increasing the number of accounts accessible within a transaction. * **Compact and flexible account reference model**: Implements the [AddressLocator](https://github.com/LayerZero-Labs/LayerZero-v2/blob/2ff4988f85b5c94032eb71bbc4073e69c078179d/packages/layerzero-v2/solana/programs/libs/oapp/src/common.rs#L59), enabling OApps to reference accounts with a leaner, more efficient structure while maintaining adaptability for future upgrades. * **Context account from the Executor**: Provides runtime metadata at execution, ensuring that OApps have contextual awareness without requiring redundant account fetching or manual setup. In the code, this is referred to as the [ExecutionContext](https://github.com/LayerZero-Labs/LayerZero-v2/blob/2ff4988f85b5c94032eb71bbc4073e69c078179d/packages/layerzero-v2/solana/programs/libs/oapp/src/common.rs#L13). * **Explicit support for multiple EOA signers**: Enables dynamic data account initialization by allowing multiple externally owned accounts (EOAs) to participate in signing and setup. This improves flexibility for multi-party or multi-step workflows. * **Multi-instruction execution model**: Empowers OApps to compose complex workflows within a single atomic transaction, combining several instructions while preserving consistency and rollback guarantees. ### How `lz_receive_types_v2` works Overall, `lz_receive_types_v2` has the following execution flow: ``` OAppAccount LzReceiveTypesV2Accounts | (1) lz_receive_types_info | v (2) lz_receive_types_v2 | | Full list of instructions for lz_receive + ALTs v (3) build and submit transaction (including lz_receive) ``` 1. `lz_receive_types_info` * requires two accounts in this exact order (must not be changed): * `oapp_account` - the OApp identity/account. In the code examples here, the `oapp_account` is the `store` account. * `lz_receive_types_accounts` - PDA derived with `seeds = [LZ_RECEIVE_TYPES_SEED, &oapp_account.key().to_bytes()]`. * returns `(version, versioned_data)` * `version: u8` — A protocol-defined version identifier for the `LzReceiveType` logic and return type, starting from 2. * `versioned_data: Any` — A value of type `Any`, representing a version-specific structure. The Executor decodes this payload based on the version and uses it to construct the full set of accounts needed to invoke `lz_receive_types_v2` (`LzReceiveTypesV2Accounts`). 2. `lz_receive_types_v2` - this instruction is called with `LzReceiveTypesV2Accounts` as the supplied accounts and returns: * the `context_version` * ALTs used (if any) * The full list of instructions for `lz_receive` 3. `build and submit transaction` - now the Executor can prepare the full transaction and submit it based on what was returned by `lz_receive_types_v2`. Before submitting, the Executor will also prepend (`pre_execute`) and append (`post_execute`) instructions to prepare the execution context and ensure safety: * [pre\_execute](https://github.com/LayerZero-Labs/LayerZero-v2/blob/solana/sync-executor/packages/layerzero-v2/solana/programs/programs/executor/src/instructions/pre_execute.rs) - Initializes a fee-limited execution context at the start of the transaction (allowing up to two ComputeBudget instructions), records the payer’s starting balance, and ensures a matching PostExecute will end the transaction. * [post\_execute](https://github.com/LayerZero-Labs/LayerZero-v2/blob/solana/sync-executor/packages/layerzero-v2/solana/programs/programs/executor/src/instructions/post_execute.rs) - validates pairing with PreExecute, enforces the fee limit against the payer’s balance change, checks signer invariants, and resets the context. All the instructions above are called by the Executor. As the OApp developer, you only need to ensure that you implement the required instructions and PDAs in your OApp program. ## Required PDAs The following are the [PDAs](/v2/concepts/glossary#pda-program-derived-address) that are required for a Solana OApp. | Component | What it is | Seed / Derivation | Why it matters | | -------------------------------- | ------------------------------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------ | | **OApp Store** | Zero-copy account holding program state (admin, bump, endpoint id, user data …) | `[b"Store"]` (customizable) | Acts as *receiver address* and signer seed for Endpoint [CPIs](/v2/concepts/glossary#cpi-cross-program-invocation) | | **Peer Config(s)** | One per remote chain; stores the peer address allowed to send | `[b"Peer", store, src_eid]` | Used to authenticate `params.sender` inside `lz_receive` | | **lz\_receive\_types\_accounts** | PDA that stores the set of accounts required for `lz_receive_types_v2`. | `[b"LzReceiveTypes", store]` | Used by the Executor to look up and provide the correct account list for execution. | Note that except for the Store PDA, the PDA seeds are not customizable. > Peer Config PDAs are initialized by the wiring step. ## Required Instructions | Instruction | Purpose | When it is called | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `lz_receive_types_info` | Returns the version and the accounts set used to construct the `lz_receive_types_v2` call. | Called off-chain by the Executor to discover which accounts to use when calling `lz_receive_types_v2`. | | `lz_receive_types_v2` | Returns the full execution plan (`LzReceiveTypesV2Result`) including ALTs and the instructions needed to run `lz_receive`. | Called off-chain by the Executor after `lz_receive_types_info` to obtain the accounts/ALTs and instructions used to build the final transaction. | | `lz_receive` | Executes the OApp’s business logic and calls `Endpoint::clear` (and optionally `send_compose`) for an inbound message. | Invoked by the Executor inside the transaction built from the `lz_receive_types_v2` execution plan. | | `init` | Initializes the OApp Store and related PDAs, and registers the OApp with the Endpoint. | Called from your deployment script or client before any crosschain messages are processed. | ## Initialize the OApp PDA This `init` instruction initializes 2 required PDAs: OApp Store and lz\_receive\_types\_accounts. ```rust wrap theme={null} use crate::{ state::{LzReceiveTypesAccounts}, STORE_SEED, }; use anchor_lang::prelude::*; use anchor_lang::solana_program::address_lookup_table::program::ID as ALT_PROGRAM_ID; use oapp::{ endpoint::{instructions::RegisterOAppParams, ID as ENDPOINT_ID}, LZ_RECEIVE_TYPES_SEED, }; #[derive(Accounts)] pub struct Init<'info> { #[account(mut)] pub payer: Signer<'info>, #[account( init, payer = payer, space = 8 + Store::INIT_SPACE, seeds = [STORE_SEED], bump )] pub store: Account<'info, Store>, #[account( init, payer = payer, space = 8 + LzReceiveTypesAccounts::INIT_SPACE, seeds = [LZ_RECEIVE_TYPES_SEED, store.key().as_ref()], bump )] pub lz_receive_types_accounts: Account<'info, LzReceiveTypesAccounts>, #[account(owner = ALT_PROGRAM_ID)] pub alt: Option>, pub system_program: Program<'info, System>, } impl Init<'_> { pub fn apply(ctx: &mut Context, params: &InitParams) -> Result<()> { ctx.accounts.store.endpoint_program = if let Some(endpoint_program) = params.endpoint_program { endpoint_program } else { ENDPOINT_ID }; ctx.accounts.store.bump = ctx.bumps.store; ctx.accounts.lz_receive_types_accounts.store = ctx.accounts.store.key(); // Set ALT if provided, otherwise default to Pubkey::default() ctx.accounts.lz_receive_types_accounts.alt = ctx.accounts.alt.as_ref().map(|a| a.key()).unwrap_or_default(); ctx.accounts.lz_receive_types_accounts.bump = ctx.bumps.lz_receive_types_accounts; let seeds: &[&[u8]] = &[STORE_SEED, &[ctx.accounts.store.bump]]; // Register the oapp oapp::endpoint_cpi::register_oapp( ctx.accounts.store.endpoint_program, ctx.accounts.store.key(), ctx.remaining_accounts, seeds, RegisterOAppParams { delegate: params.default_admin }, )?; Ok(()) } } ``` **Key points** * You must call `oapp::endpoint_cpi::register_oapp` to register your OApp * Registration is **one-time**; afterwards the Executor knows this Store PDA = OApp. * You **do not** pass a "trusted remote" mapping here—that's what the `Peer` PDAs enforce. * You can extend your OApp's store PDA with other fields required by your use case. ## Implement `lz_receive` — business logic + `Endpoint::clear` The `lz_receive` instruction is where your OApp's business logic is defined and also where the call to `Endpoint::clear` is made. Additionally, it is also where compose messages are handled, if implemented. ```rust wrap theme={null} #[derive(Accounts)] #[instruction(params: LzReceiveParams)] pub struct LzReceive<'info> { #[account(mut)] pub payer: Signer<'info>, #[account(mut, seeds = [STORE_SEED], bump = store.bump)] pub store: Account<'info, Store>, #[account( seeds = [PEER_SEED, store.key().to_bytes(), ¶ms.src_eid.to_be_bytes()], bump = peer.bump, constraint = params.sender == peer.peer_address )] pub peer: Account<'info, PeerConfig> } pub fn apply(ctx: &mut Context, params: &LzReceiveParams) -> Result<()> { // 1. replay-protection (handled inside clear) let seeds = &[STORE_SEED, &[ctx.accounts.store.bump]]; // The first Clear::MIN_ACCOUNTS_LEN remaining accounts are exactly what // get_accounts_for_clear() returned earlier. let clear_accounts = &ctx.remaining_accounts[..Clear::MIN_ACCOUNTS_LEN]; oapp::endpoint_cpi::clear( ENDPOINT_ID, ctx.accounts.store.key(), // payer (seeds above) clear_accounts, seeds, ClearParams { receiver: ctx.accounts.store.key(), src_eid: params.src_eid, sender: params.sender, nonce: params.nonce, guid: params.guid, message: params.message.clone(), }, )?; // You should have app-specific logic to determine whether a message is a compose message // e.g. you can have part of the payload be a u8 where 1 = regular message, 2 = compose message // or, especially if your regular payload has a fixed length, determine the presence of a compose message based on presence of data after an offset (example: https://github.com/LayerZero-Labs/devtools/blob/main/examples/lzapp-migration/programs/oft202/src/msg_codec.rs#L60) oapp::endpoint_cpi::send_compose( ENDPOINT_ID, ctx.accounts.store.key(), &ctx.remaining_accounts[Clear::MIN_ACCOUNTS_LEN..], seeds, SendComposeParams { to: ctx.accounts.store.key(), // self guid: params.guid, index: 0, message: params.message.clone(), }, )?; // 2. Your app-specific logic // ... Ok(()) } ``` **Rules of thumb** * Call `clear()` **before** touching any user state—this burns the nonce and prevents re-entry. * Use `ctx.remaining_accounts` instead of hard-wiring anything—keeps `lz_receive_types_v2` and `lz_receive` perfectly in sync. * Don’t forget `is_signer: true` zero-pubkey placeholders for ATA init or rent payer. **Security Reminders** * Validate the `Peer` account first (`constraint = params.sender == peer.address`). * **Store the Endpoint ID inside state** (`store.endpoint_program`) and **assert** it every CPI. ## Implement `lz_receive_types_v2` Define `LzReceiveTypesAccounts` anywhere in your state module tree: ```rust wrap theme={null} /// LzReceiveTypesAccounts includes accounts that are used in the LzReceiveTypes instruction. #[account] #[derive(InitSpace)] pub struct LzReceiveTypesAccounts { pub store: Pubkey, // Note: This is used as your OApp address. pub alt: Pubkey, // Note: in this example, we store a single ALT. You can modify this to store a Vec of Pubkeys too. pub bump: u8, // Note: you may add more account Pubkeys into this struct, per your use case. } ``` > `store` here is the OApp account referred to as `oapp_account` in the flow description. Create an `lz_receive_types_info` instruction: ```rust wrap theme={null} use oapp::{ lz_receive_types_v2::{LzReceiveTypesV2Accounts, LZ_RECEIVE_TYPES_VERSION}, LzReceiveParams, LZ_RECEIVE_TYPES_SEED, }; use crate::*; /// LzReceiveTypesInfo instruction implements the versioning mechanism introduced in V2. /// /// This instruction addresses the compatibility risk of the original LzReceiveType V1 design, /// which lacked any formal versioning mechanism. The LzReceiveTypesInfo instruction allows /// the Executor to determine how to interpret the structure of the data returned by /// lz_receive_types() for different versions. /// /// Returns (version, versioned_data): /// - version: u8 — A protocol-defined version identifier for the LzReceiveType logic and return /// type /// - versioned_data: Any — A version-specific structure that the Executor decodes based on the /// version /// /// For Version 2, the versioned_data contains LzReceiveTypesV2Accounts which provides information /// needed to construct the call to lz_receive_types_v2. #[derive(Accounts)] pub struct LzReceiveTypesInfo<'info> { #[account(seeds = [STORE_SEED], bump = store.bump)] pub store: Account<'info, Store>, /// PDA account containing the versioned data structure for V2 /// Contains the accounts needed to construct lz_receive_types_v2 instruction #[account(seeds = [LZ_RECEIVE_TYPES_SEED, &store.key().to_bytes()], bump = lz_receive_types_accounts.bump)] pub lz_receive_types_accounts: Account<'info, LzReceiveTypesAccounts>, } impl LzReceiveTypesInfo<'_> { /// Returns the version and versioned data for LzReceiveTypes /// /// Version Compatibility: /// - Forward Compatibility: Executors must gracefully reject unknown versions /// - Backward Compatibility: Version 1 OApps do not implement lz_receive_types_info; Executors /// may fall back to assuming V1 if the version instruction is missing or unimplemented /// /// For V2, returns: /// - version: 2 (u8) /// - versioned_data: LzReceiveTypesV2Accounts containing the accounts needed for /// lz_receive_types_v2 pub fn apply( ctx: &Context, params: &LzReceiveParams, ) -> Result<(u8, LzReceiveTypesV2Accounts)> { let receive_types_account = &ctx.accounts.lz_receive_types_accounts; let required_accounts = if receive_types_account.alt == Pubkey::default() { vec![ receive_types_account.store // You can include more accounts here if necessary ] } else { vec![ receive_types_account.store, receive_types_account.alt, // You can include more accounts here if necessary ] }; Ok((LZ_RECEIVE_TYPES_VERSION, LzReceiveTypesV2Accounts { accounts: required_accounts })) } } ``` Implement `lz_receive_types_v2`: ```rust wrap theme={null} use crate::*; use anchor_lang::solana_program; use oapp::{ common::{ compact_accounts_with_alts, AccountMetaRef, AddressLocator, EXECUTION_CONTEXT_VERSION_1, }, lz_receive_types_v2::{ Instruction, LzReceiveTypesV2Result, }, LzReceiveParams, }; #[derive(Accounts)] #[instruction(params: LzReceiveParams)] pub struct LzReceiveTypesV2<'info> { #[account(seeds = [STORE_SEED], bump = store.bump)] pub store: Account<'info, Store>, // Note: include more accounts here if you had done so in the previous steps } impl LzReceiveTypesV2<'_> { /// Returns the execution plan for lz_receive with a minimal account set. pub fn apply( ctx: &Context, params: &LzReceiveParams, ) -> Result { // Derive peer PDA from src_eid let peer_seeds = [PEER_SEED, ¶ms.src_eid.to_be_bytes()]; let (peer, _) = Pubkey::find_program_address(&peer_seeds, ctx.program_id); // Event authority used for logging let (event_authority_account, _) = Pubkey::find_program_address(&[oapp::endpoint_cpi::EVENT_SEED], &ctx.program_id); let accounts = vec![ // payer AccountMetaRef { pubkey: AddressLocator::Payer, is_writable: true }, // peer AccountMetaRef { pubkey: peer.into(), is_writable: false }, // event authority account - used for event logging AccountMetaRef { pubkey: event_authority_account.into(), is_writable: false }, // system program AccountMetaRef { pubkey: solana_program::system_program::ID.into(), is_writable: false, }, // program id - the program that is executing this instruction AccountMetaRef { pubkey: crate::ID.into(), is_writable: false }, ]; // Return the execution plan (no clear/compose helper accounts) Ok(LzReceiveTypesV2Result { context_version: EXECUTION_CONTEXT_VERSION_1, alts: ctx.remaining_accounts.iter().map(|alt| alt.key()).collect(), instructions: vec![ Instruction::LzReceive { // In this example, ALTs are passed in via remaining_accounts // This decision allows for flexibility in terms of passing in any number of ALTs without needing to change the accounts struct // However, if you need stronger schema guarantees and require only a single ALT, you may opt to have it passed in explicitly via ctx.accounts.alt (or similar) accounts: compact_accounts_with_alts(&ctx.remaining_accounts, accounts)?, }, ], }) } } ``` Ensure that you have registered the new instruction handlers in the program module in your `lib.rs`: ```rust wrap theme={null} #[program] pub mod my_oapp { use super::*; // ...the existing instructions pub fn lz_receive_types_v2( ctx: Context, params: LzReceiveParams, ) -> Result { LzReceiveTypesV2::apply(&ctx, ¶ms) } pub fn lz_receive_types_info( ctx: Context, params: LzReceiveParams, ) -> Result<(u8, LzReceiveTypesV2Accounts)> { LzReceiveTypesInfo::apply(&ctx, ¶ms) } } ``` ## OApp-Specific Message Codec Since at its core, the OApp Standard simply gives you the interface for generic message passing (raw bytes), you need to implement for yourself how the raw bytes are interpreted. In the Solana OApp example and also in the [OFT implementation](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/programs/oft/src/msg_codec.rs), this logic is encapusalated in a `msg_codec.rs` file. ```rust wrap theme={null} use anchor_lang::prelude::error_code; use std::str; // Just like OFT, we don't need an explicit MSG_TYPE param // Instead, we'll check whether there's data after the string ends pub const LENGTH_OFFSET: usize = 0; pub const STRING_OFFSET: usize = 32; #[error_code] pub enum MsgCodecError { /// Buffer too short to even contain the 32‐byte length header InvalidLength, /// Header says "string is N bytes" but buffer < 32+N BodyTooShort, /// Payload bytes aren’t valid UTF-8 InvalidUtf8, } fn decode_string_len(buf: &[u8]) -> Result { if buf.len() < STRING_OFFSET { return Err(MsgCodecError::InvalidLength); } let mut string_len_bytes = [0u8;32]; string_len_bytes.copy_from_slice(&buf[LENGTH_OFFSET..LENGTH_OFFSET+32]); Ok(u32::from_be_bytes(string_len_bytes[28..32].try_into().unwrap()) as usize) } pub fn encode(string: &str) -> Vec { let string_bytes = string.as_bytes(); let mut msg = Vec::with_capacity( STRING_OFFSET + // length word (fixed) string_bytes.len() // string length ); // 4-byte length msg.extend(std::iter::repeat(0).take(28)); // padding msg.extend_from_slice(&(string_bytes.len() as u32).to_be_bytes()); // string msg.extend_from_slice(string_bytes); msg } pub fn decode(message: &[u8]) -> Result { // Read the declared payload length from the header let string_len = decode_string_len(message)?; let start = STRING_OFFSET; // Safely compute end index and check for overflow let end = start .checked_add(string_len) .ok_or(MsgCodecError::InvalidLength)?; // Ensure the buffer actually contains the full payload if end > message.len() { return Err(MsgCodecError::BodyTooShort); } // Slice out the payload bytes let payload = &message[start..end]; // Attempt to convert to &str, returning an error if invalid UTF-8 match str::from_utf8(payload) { Ok(s) => Ok(s.to_string()), Err(_) => Err(MsgCodecError::InvalidUtf8), } } ``` **Key points** * Every OApp would have its own Message Codec implementation * The above Message Codec example involves an OApp that expects the message to contain only a `length` and the actual `string` * If sending across VMs, ensure the codec on the other VM matches. ## Gotchas & common errors | Error | Usual cause | | ------------------------------ | ----------------------------------------------------------------------------- | | `AccountNotSigner` on slot N | You omitted a signer placeholder or swapped two accounts. | | `InvalidProgramId` (Endpoint) | Wrong Endpoint ID; check you passed the same constant everywhere. | | Transaction > 1232 bytes | Too many accounts in the transaction → ensure you use ALTs. | | Executor halts at `lz_receive` | Your `lz_receive_types_v2` returned fewer accounts than `lz_receive` expects. | # Solana OFT Source: https://docs.layerzero.network/v2/developers/solana/oft/overview Overview of Solana OFT on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure crosschain messaging. 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](../../../concepts/applications/oft-standard). While the typical path for Solana program development involves interacting with or deploying executable code that defines your specific implementation, and then minting accounts that want to use that interface (e.g., the [SPL Token Program](https://spl.solana.com/token)), the [OFT Program](#the-oft-program) is different in this respect. Because every Solana Program has an Upgrade Authority, and this authority can change or modify the implementation of all child accounts, developers wishing to create crosschain tokens on Solana should deploy their own instance of the [OFT Program](#the-oft-program) to create new [OFT Store](#oft-account-model) accounts, so that they own their OFT's Upgrade Authority. End-to-end instruction on how to deploy a Solana OFT can be found in the README at [https://github.com/LayerZero-Labs/devtools/tree/main/examples/oft-solana](https://github.com/LayerZero-Labs/devtools/tree/main/examples/oft-solana), which will be the README of your project when you setup using the LayerZero CLI. ## Quickstart ### Example For the step-by-step instructions on how to build, deploy and wire a Solana OFT, view the [Solana OFT example](https://github.com/LayerZero-Labs/devtools/tree/main/examples/oft-solana). ### Scaffold Spin up a new OFT workspace (based on the example) in seconds: ```bash wrap theme={null} LZ_ENABLE_SOLANA_OFT_EXAMPLE=1 npx create-lz-oapp@latest ``` Specify the directory, select `OFT (Solana)` and proceed with the installation. Follow the provided README instructions to make your first crosschain OFT transfer between Solana and an EVM chain. The rest of this page contains additional information that you should read before deploying to mainnet. ## Prerequisite Knowledge Understanding the following will help you with the rest of this page: * [Mint Authority and Freeze Authority](https://solana.com/docs/core/tokens#mint-account) * [Token Metadata](https://solana.com/developers/guides/token-extensions/metadata-pointer#token-metadata-interface-overview) * [Solana Account Model](https://solana.com/docs/core/accounts) * [Solana Program Library](https://spl.solana.com/token) and the [Token-2022](https://spl.solana.com/token-2022) ## The OFT Program The **OFT Program** interacts with the **Solana Token Program** to allow new or existing Fungible Tokens on Solana to transfer balances between different chains. Solana now has two token programs. The original [Token Program](https://spl.solana.com/token) (commonly referred to as 'SPL token') and the newer [Token-2022](https://spl.solana.com/token-2022) program. LayerZero's **OFT Standard** introduces the **OFT Store**, a Program Derived Address (PDA) account responsible for storing your token's specific LayerZero configuration and enabling crosschain transfers for Solana tokens. Diagram showing the relationship between the OFT Program and OFT Store Account, illustrating that the OFT Store is a Program Derived Address storing LayerZero configuration for crosschain token transfers Diagram showing the relationship between the OFT Program and OFT Store Account, illustrating that the OFT Store is a Program Derived Address storing LayerZero configuration for crosschain token transfers Each **OFT Store** Account is managed by an **OFT Program**, which you would have already deployed in the previous step. To read more on the various programs and accounts involved in creating a Solana OFT, refer to the below section on the [OFT Account Model](#oft-account-model). You can use the same **OFT Program** to create multiple Solana OFTs. If using the same repo, you will need to rename the existing `deployments/solana-/OFT.json` as it will be overwritten otherwise. You will also need to either rename the existing `layerzero.config.ts` or use a different config file for the subsequent OFTs. ## OFT Account Model Before creating a new OFT, you should first understand the [Solana Account Model](https://solana.com/docs/core/accounts) which is used for the OFT Standard on Solana. The **Solana OFT Standard** uses 6 main accounts: | Account Name | Executable | Description | | ----------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | OFT Program | `true` | The OFT Program itself, the executable, stateless code which controls how OFTs interact with the LayerZero Endpoint and the SPL Token. | | Mint Account | `false` | This is the [Mint Account](https://solana.com/docs/core/tokens#mint-account) for the OFT's SPL Token. Stores the key metadata for a specific token, such as total supply, decimal precision, mint authority, freeze authority and update authority. | | Mint Authority Multisig | `false` | A 1 of N [Multisig](https://spl.solana.com/token#example-mint-with-multisig-authority) that serves as the Mint Authority for the SPL Token. The OFT Store is always required as a signer. It's also possible to add additional signers. | | Escrow | `false` | The **Token Account** for the corresponding **Mint Account**, owned by the **OFT Store**. For **OFT Adapter** deployments and also for storing fees, if fees are enabled. For both OFT and OFT Adapter, the Escrow address is part of the derivation for the OFT Store PDA. Escrow is a regular Token Account and not an Associated Token Account. | | OFT Store | `false` | A [PDA](https://solana.com/docs/core/pda) account that stores data about each OFT such as the underlying SPL Token Mint, the SPL Token Program, Endpoint Program, the OFT's fee structure, and extensions. Is the owner for the Escrow account. The OFT Store is a signer for the Mint Authority multisig. | | PeerConfig | `false` | A [PDA](https://solana.com/docs/core/pda) account that stores configuration for each remote chain, including peer addresses, enforced options, rate limiters, and fee settings. This account is derived from the OFT Store and remote [EID](/v2/concepts/glossary#endpoint-id). | The SPL [Token Program](https://spl.solana.com/token) handles all creation and management of SPL tokens on the Solana blockchain. An OFT's deployment interacts with this program to create the Mint Account. ## Message Execution Options `_options` are a generated bytes array with specific instructions for the [DVNs](../../../concepts/modular-security/security-stack-dvns) and [Executor](../../../concepts/permissionless-execution/executors) when handling crosschain messages. Note that you must have at least either `enforcedOptions` set for your OApp or `extraOptions` passed in for a particular transaction. If both are absent, the transaction will fail. For sends from EVM chains, `quoteSend()` will revert. For sends from Solana, you will see a `ZeroLzReceiveGasProvided` error. If you had set `enforcedOptions`, then you can pass an empty bytes array (`0x` if sending from EVM, `Buffer.from('')` if sending from Solana). If you did not set `enforcedOptions`, then continue reading. ### Setting Extra Options Any `_options` passed in the `send` call itself is considered as `_extraOptions`. `_extraOptions` can specify additional handling within the same message type. These `_options` will then be combined with `enforcedOption` if set. You can find how to generate all the available `_options` in [Message Execution Options](../../evm/configuration/options), but for this tutorial you should focus primarily on using [`@layerzerolabs/lz-v2-utilities`](https://www.npmjs.com/package/@layerzerolabs/lz-v2-utilities?activeTab=code), specifically the `Options` class. As outlined above, decide on whether you need an application wide option via `enforcedOptions` or a call specific option using `extraOptions`. Be specific in what `_options` you use for both parameters, as your transactions will reflect the exact settings you implement. Your `enforcedOptions` will always be charged to a user when calling send. Any `extraOptions` passed in the send call will be charged on top of the enforced settings. Passing identical `_options` in both `enforcedOptions` and `extraOptions` will charge the caller twice on the source chain, because LayerZero interprets duplicate `_options` as two separate requests for gas. ### Setting Options Inbound to EVM chains A typical OFT's `lzReceive` call and mint will use `60000` gas on most EVM chains, so you can enforce this option to require callers to pay a `60000` gas limit in the source chain transaction to prevent out of gas issues on destination. To pass in `extraOptions` for Solana to EVM (Sepolia, in our example) transactions, modify ` tasks/solana/sendOFT.ts` Refer to the sample code diff below: ```typescript wrap theme={null} import {addressToBytes32, Options} from '@layerzerolabs/lz-v2-utilities'; // ... // add the following 3 lines anywhere before the `oft.quote()` call const GAS_LIMIT = 60_000 // Gas limit for the executor const MSG_VALUE = 0 // msg.value for the lzReceive() function on destination in wei const _options = Options.newOptions().addExecutorLzReceiveOption(GAS_LIMIT, MSG_VALUE) // ... // replace the options value in oft.quote() const { nativeFee } = await oft.quote( umi.rpc, { payer: umiWalletSigner.publicKey, tokenMint: mint, tokenEscrow: umiEscrowPublicKey, }, { payInLzToken: false, to: Buffer.from(recipientAddressBytes32), dstEid: toEid, amountLd: BigInt(amount), minAmountLd: 1n, options: _options.toBytes(), // <--- here composeMsg: undefined, }, // ... // replace the options value in oft.send() const ix = await oft.send( umi.rpc, { payer: umiWalletSigner, tokenMint: mint, tokenEscrow: umiEscrowPublicKey, tokenSource: tokenAccount[0], }, { to: Buffer.from(recipientAddressBytes32), dstEid: toEid, amountLd: BigInt(amount), minAmountLd: (BigInt(amount) * BigInt(9)) / BigInt(10), options: _options.toBytes(), // <--- here composeMsg: undefined, nativeFee, }, // ... ``` We will call this script later in [Message Execution Options](#message-execution-options). `ExecutorLzReceiveOption` specifies a quote paid in advance on the source chain by the `msg.sender` for the equivalent amount of native gas to be used on the destination chain. If the actual cost to execute the message is less than what was set in `_options`, there is no default way to refund the sender the difference. Application developers need to thoroughly profile and test gas amounts to ensure consumed gas amounts are correct and not excessive. ### Setting Options Inbound to Solana When sending to Solana, a `msg.value` is only required if the recipient address does not already have the [Associated Token Account (ATA)](https://www.alchemy.com/overviews/associated-token-account) for your mint. There are two ways to provide this value: * Enforced Options (app-level default): set `value` in `enforcedOptions` for the pathway. This guarantees the amount is always included, but it will waste lamports for recipients that already have an ATA. Use with caution in production. * Extra Options (per-transaction): set `msg.value` in `extraOptions` only when needed after checking whether the recipient’s ATA exists. This is the recommended approach to avoid unnecessary costs. How much `value` to provide: * SPL Token accounts: the rent-exempt amount is `2_039_280` lamports (0.00203928 SOL). * Token-2022 accounts: the required value depends on the token account size, which varies by the enabled extensions. You can inspect the size of your token's token account and [calculate the rent amount needed](https://www.quicknode.com/guides/solana-development/getting-started/understanding-rent-on-solana). If setting via `enforcedOptions` in `layerzero.config.ts`, the parameter is `value`. If building per-transaction options in TypeScript, it is the second parameter to `addExecutorLzReceiveOption(gas_limit, msg_value)`. See the next section for how to detect ATA existence and attach `msg.value` conditionally via `extraOptions`. For Solana OFTs that use Token2022, you will need to increase `value` to a higher amount, which depends on the token account size, which in turn depends on the extensions that you enable. Unlike EVM addresses, every Solana Account requires a minimum balance of the native gas token to exist rent free. To send tokens to Solana, you will need a minimum amount of lamports to execute and initialize the account within the transaction when the recipient’s ATA does not already exist. For EVM → Solana sends, enforce the compute units ("gas") at the application level using `enforcedOptions`. When attaching per-transaction value for ATA creation via `extraOptions`, set gas to `0` and only provide `msg.value` as needed. The protocol combines `extraOptions` with your enforced baseline at execution time. #### Conditional msg.value for ATA creation For sends to Solana, you can avoid overpaying rent by setting your enforced options `value` to 0 and supplying `msg.value` only when the recipient’s [Associated Token Account (ATA)](https://www.alchemy.com/overviews/associated-token-account) is missing. This pattern is useful when recipients may or may not have an ATA for your mint. Steps: * Set `enforcedOptions` value to 0 in `layerzero.config.ts` for pathways that deliver to Solana. * Before constructing `extraOptions` for a specific send to Solana, check if the recipient’s ATA exists. * If ATA exists: set `msg.value = 0` in `addExecutorLzReceiveOption`. * If ATA does not exist: set `msg.value` to the rent-exempt minimum for the token account (e.g., `2_039_280` lamports for SPL; Token-2022 may require more depending on enabled extensions). Example: check ATA existence using Umi and mpl-toolbox, then set options conditionally. ```typescript wrap theme={null} import {createUmi} from '@metaplex-foundation/umi-bundle-defaults'; import {findAssociatedTokenPda, safeFetchToken} from '@metaplex-foundation/mpl-toolbox'; import {publicKey} from '@metaplex-foundation/umi'; import {Options} from '@layerzerolabs/lz-v2-utilities'; const umi = createUmi('https://api.mainnet-beta.solana.com'); const mint = publicKey(''); const owner = publicKey(''); // derive ATA PDA const ata = findAssociatedTokenPda(umi, {mint, owner}); // check if it exists const account = await safeFetchToken(umi, ata); if (account) { console.log('ATA exists at', ata.toString()); } else { console.log('ATA not found'); } // set per-tx options based on ATA existence // gas is enforced at app-level; set 0 here to avoid double-charging const GAS_LIMIT = 0; const SPL_TOKEN_ACCOUNT_RENT_VALUE = 2039280; // rent-exempt lamports for SPL token account (ATA) const MSG_VALUE = account ? 0 : SPL_TOKEN_ACCOUNT_RENT_VALUE; // if Token2022, use a higher value based on account size const options = Options.newOptions().addExecutorLzReceiveOption(GAS_LIMIT, MSG_VALUE); // rest of your code; calls to quote/send ``` Use `options.toHex()` (EVM) or `options.toBytes()` (Solana) when populating `extraOptions`/`options` in your send call. These values will be combined with any `enforcedOptions` configured at the app level. If your mint is Token-2022, compute the rent-exempt minimum from the token account size (varies by enabled extensions) and replace `SPL_TOKEN_ACCOUNT_RENT_VALUE` accordingly. ## Precautions ### One OFT Adapter per OFT deployment/mesh Multiple OFT Adapters break omnichain unified liquidity by effectively creating token pools. If you create OFT Adapters on multiple chains, you have no way to guarantee finality for token transfers due to the fact that the source chain has no knowledge of the destination pool's supply (or lack of supply). This can create race conditions where if a sent amount exceeds the available supply on the destination chain, those sent tokens will be permanently lost. ### Token Transfer Precision The OFT Standard also handles differences in decimal precision before every crosschain transfer by "**cleaning**" the amount from any decimal precision that cannot be represented in the shared system. The OFT Standard defines these small token transfer amounts as "**dust**". #### Example ERC20 OFTs use a local decimal value of `18` (the norm for ERC20 tokens), and a shared decimal value of `6` (the norm for Solana tokens). ``` decimalConversionRate = 10^(localDecimals − sharedDecimals) = 10^(18−6) = 10^12 ``` This means the conversion rate is `10^12`, which indicates the smallest unit that can be transferred is `10^-12` in terms of the token's local decimals. For example, if you `send` a value of `1234567890123456789` (a token amount with 18 decimals), the OFT Standard will: 1. Divides by `decimalConversionRate`: ``` 1234567890123456789 / 10^12 = 1234567.890123456789 = 1234567 ``` Remember that solidity performs integer arithmetic. This means when you divide two integers, the result is also an integer with the fractional part discarded.
2. Multiplies by `decimalConversionRate`: ``` 1234567 * 10^12 = 1234567000000000000 ``` This process removes the last 12 digits from the original amount, effectively "**cleaning**" the amount from any "**dust**" that cannot be represented in a system with 6 decimal places. ### Choosing the right local decimals value Be careful when selecting your Solana token's local decimals. Although the default is `9` (SPL standard), choosing a value that is too high can severely limit your maximum mintable supply because Solana balances are `u64`. For instance, setting `18` decimals (common on EVM) would cap your supply to roughly \~18 whole tokens on Solana. Prefer the smallest decimals that satisfy your UX and supply requirements (many projects use `6` or `9`). This is independent from `sharedDecimals` (default `6`), which governs crosschain precision and dust handling. See the detailed guidance and max-supply table in [Deciding the number of local decimals for your Solana OFT](../technical-reference/solana-guidance#deciding-the-number-of-local-decimals-for-your-solana-oft). ## (Optional) Verify the OFT Program To continue, you must first install [solana-verify](https://github.com/Ellipsis-Labs/solana-verifiable-build). You can learn about how program verification works in the [official Solana program verification guide](https://solana.com/developers/guides/advanced/verified-builds#how-does-it-work). The commands given below assume that you did not make any modifications to the Solana OFT program source code. If you did, you can refer to the instructions in [solana-verify](https://github.com/Ellipsis-Labs/solana-verifiable-build) directly. Verification is done via the OtterSec API, which builds the program contained in the repo provided. If you did not modify the OFT program, you can reference LayerZero's devtools repo, which removes the need for you to host your own public repo for verification purposes. By referencing LayerZero's devtools repo, you also benefit from the LayerZero OFT program's audited status. Normally, each Anchor program requires its own repository for verification because the program ID provided to `declare_id!` is embedded in the bytecode, altering its hash. We solve this by having you supply the program ID as an environment variable during build time. This variable is then read by the `program_id_from_env` function in the OFT program's `lib.rs` snippet. Below is the relevant code snippet: ``` declare_id!(Pubkey::new_from_array(program_id_from_env!( "OFT_ID", "9UovNrJD8pQyBLheeHNayuG1wJSEAoxkmM14vw5gcsTT" ))); ``` The above is used via providing `OFT_ID` as an environment variable when running `solana-verify`, which is demonstrated in the following sections. ### Compare locally If you wish to, you can view the program hash of the locally built OFT program: ```bash wrap theme={null} solana-verify get-executable-hash ./target/verifiable/oft.so ``` Compare with the onchain program hash: ``` solana-verify get-program-hash -u devnet ``` ### Verify against a repository and submit verification data onchain Run the following command to verify against the repo that contains the program source code: ```bash wrap theme={null} solana-verify verify-from-repo -ud --program-id --mount-path examples/oft-solana https://github.com/LayerZero-Labs/devtools --library-name oft -b solanafoundation/solana-verifiable-build:2.1.0 -- --config env.OFT_ID=\'\' ``` > You can also pass in `--commit-hash ` to pin the verification to a commit The above instruction runs against the Solana Devnet as it uses the `-ud` flag. To run it against Solana Mainnet, replace `-ud` with `-um`. #### Submit verification data when the Upgrade Authority is your local keypair Upon successful verification, you will be prompted with the following: ``` Program hash matches ✅ Do you want to upload the program verification to the Solana Blockchain? (y/n) ``` Respond with `y` to proceed with uploading of the program verification data onchain. #### Submit verification data when the Upgrade Authority is a Multisig The steps are similar to the above, except you will not be able to submit a valid verification PDA when prompted since the Upgrade Authority is not your local keypair. Instead, run the following, after having run `verify-from-repo`: ```bash wrap theme={null} solana-verify export-pda-tx https://github.com/LayerZero-Labs/devtools --program-id --uploader --mount-path examples/oft-solana --library-name oft -b solanafoundation/solana-verifiable-build:2.1.0 -- --config env.OFT_ID=\'\' ``` * Ensure your `solana-verify` version is at minimum `0.4.0` to be able to use `export-pda-tx` * You can also pass in `--commit-hash ` to pin the verification to a commit `export-pda-tx` will return a base58 string that represents the transaction data for uploading the verification PDA. Import this into Squads for approval and execution. ### (mainnet only) Submit to the OtterSec API This will provide your program with the `Verified` status on explorers. Note that currently the `Verified` status only exists on mainnet explorers. Verify against the code in the git repo and submit for verification status: ```bash wrap theme={null} solana-verify verify-from-repo --remote -um --program-id --mount-path examples/oft-solana https://github.com/LayerZero-Labs/devtools --library-name oft -b solanafoundation/solana-verifiable-build:2.1.0 -- --config env.OFT_ID=\'\' ``` You **must** run the above step using the same keypair as the program's upgrade authority. Learn more about the solana-verify CLI from the [official repo](https://github.com/Ellipsis-Labs/solana-verifiable-build). Program verification is tied to the program's Upgrade Authority. If you transfer a program's Upgrade Authority, you will need to redo the verification steps using the new Upgrade Authority address. ### Renouncing the OFT Program's Upgrade Authority If you intend on renouncing the Upgrade Authority of your OFT program, we recommend that you go through with program verification first. After renouncing, it will no longer be possible to verify your program as it requires submitting the verification PDA using the Upgrade Authority address. A program that has been verified will remain verified if its onchain hash does not change, even if its Upgrade Authority has been renounced. ## Token Supply Cap When transferring tokens across different blockchain VMs, each chain may have a different level of decimal precision for the smallest unit of a token. While EVM chains support `uint256` for token balances, Solana uses `uint64`. Because of this, the default OFT Standard has a max token supply `(2^64 - 1)/(10^6)`, or `18,446,744,073,709.551615`. If your token's supply needs to exceed this limit, you'll need to override the **shared decimals value**. ## Optional: Overriding `sharedDecimals` This shared decimal precision is essentially the maximum number of decimal places that can be reliably represented and handled across different blockchain VMs when transferring tokens. By default, an OFT has 6 `sharedDecimals`, which is optimal for most ERC20 use cases that use `18` decimals. ```typescript wrap theme={null} // @dev Sets an implicit cap on the amount of tokens, over uint64.max() will need some sort of outbound cap / totalSupply cap // Lowest common decimal denominator between chains. // Defaults to 6 decimal places to provide up to 18,446,744,073,709.551615 units (max uint64). // For tokens exceeding this totalSupply(), they will need to override the sharedDecimals function with something smaller. // ie. 4 sharedDecimals would be 1,844,674,407,370,955.1615 const OFT_DECIMALS = 6; ``` To modify this default, simply change the `OFT_DECIMALS` to another value during deployment. Shared decimals also control how token transfer precision is calculated. ## Troubleshooting ### DeclaredProgramIdMismatch Full error: `AnchorError occurred. Error Code: DeclaredProgramIdMismatch. Error Number: 4100. Error Message: The declared program id does not match the actual program id.` Fixing this error requires upgrading the deployed program. Upgrading your program will require that your keypair has sufficient SOL for the whole program's rent (approximately 3.9 SOL). This is due to how program upgrades in Solana works. Read further for the details. If you have access to additional SOL, we recommend you to continue with these steps. Alternatively, you can [close the existing program account](https://solana.com/docs/programs/deploying#close-program) (which will return the current program's SOL rent) and deploy from scratch. Note that after closing a program account, you cannot reuse the same program ID, which means you must use a [new program keypair](#generate-program-keypairs). This error occurs when the program is built with a `declare_id!` value that does not match its onchain program ID. The program ID onchain is determined by the original program keypair used when deploying (created by `solana-keygen new -o target/deploy/endpoint-keypair.json --force`). To debug, check the following: the following section in `Anchor.toml`: ```bash wrap theme={null} [programs.localnet] oft = "9obQfBnWMhxwYLtmarPWdjJgTc2mAYGRCoWbdvs9Wdm5" ``` the output of running `anchor keys list`: ```bash wrap theme={null} endpoint: Cfego9Noyr78LWyYjz2rYUiaUR4L2XymJ6su8EpRUviU oft: 9obQfBnWMhxwYLtmarPWdjJgTc2mAYGRCoWbdvs9Wdm5 ``` Ensure that in both, the `oft` values match your OFT program's onchain ID. If they already do, and you are still encountering `DeclaredProgramIdMismatch`, this means that you ran the build command with the wrong program ID, causing the declared program ID onchain to mismatch. To fix this, you can re-run the build command, ensuring you pass in the `OFT_ID` env var: ```bash wrap theme={null} anchor build -v -e OFT_ID= ``` Then, re-deploy (upgrade) your program. For this step, your keypair is required to have sufficient SOL at least equivalent to current program's rent. While the net difference in SOL will be zero if your program's size did not change, you will still need the same amount of SOL as required by the program's rent due to how Solana program upgrades work, which is as follows: * the existing program starts off as being unaffected * the updated program's bytecode is uploaded to a **buffer account (new account, hence SOL for rent is required)** which acts as a temporary staging area * the contents of the buffer account are then copied to the program data account * the buffer account is closed, and its rent SOL is returned Run the deploy command to upgrade the program. ```bash wrap theme={null} solana program deploy --program-id target/deploy/oft-keypair.json target/deploy/oft.so -u devnet --with-compute-unit-price 300000 ``` To deploy to Solana Mainnet, replace `-u devnet` with `-u mainnet-beta`. ### Retrying Failed Transactions If a transaction fails, it may be due to network congestion or other temporary issues. You can retry the transaction by resubmitting it. Ensure that you have enough SOL in your account to cover the transaction fees. ### Recovering Failed Rent ``` solana program close --buffer --keypair deployer-keypair.json -u mainnet-beta ``` For more troubleshooting help, refer to the Solana OFT [README](https://github.com/LayerZero-Labs/devtools/tree/main/examples/oft-solana). ### Building without Docker Our default instructions ask you to build in verifiable mode: ``` anchor build -v -e OFT_ID= ``` Where the `-v` flag instructs anchor to build in verifiable mode. We highly recommend you to build in verifiable mode, so that you can carry out [program verification](#optional-verify-the-oft-program). Verifiable mode requires Docker. If you cannot build using Docker, then the alternative is to build in regular mode, which results in slight differences in commands for two steps: build and deploy. For building: ```bash wrap theme={null} OFT_ID= anchor build ``` In verifiable mode, the output defaults to `target/verifiable/oft.so`. In regular mode, the output defaults to `target/deploy/oft.so`. For deploying: ```bash wrap theme={null} solana program deploy --program-id target/deploy/oft-keypair.json target/deploy/oft.so -u devnet --with-compute-unit-price ``` All other commands remain the same. ## Known Limitations ### Max number of DVNs Given Solana's transaction size limit of 1232 bytes, the current max number of DVNs for a pathway involving Solana is 5. ### Token Extensions Token-2022 ([Token Extensions](https://solana.com/solutions/token-extensions)) support for Solana OFT is limited. It is advised for you to conduct an end-to-end test if you require token extensions for your Solana OFT. As general guidance: * [Transfer Hook](https://solana.com/developers/guides/token-extensions/transfer-hook): supported for regular OFTs (non-Adapters) under the condition that OFT fees remain `0` (the default). * **Transfer Fee** (`TransferFeeConfig` / `TransferFeeAmount`): supported with caveats. Both points below assume that OFT Fees are set to 0. * For Solana-side OFT Adapters, locking and unlocking would incur the transfer fee as `transfer_checked` is used. * For regular OFTs, transfer fees would not be applied for cross-chain sends from and to Solana as those involve burn and mint, and not transfer. * Usage of the Transfer Fee extension alongside a non-zero OFT Fee requires further testing. * **Metadata & Metadata Pointer**: generally works in common OFT setups, but you should still validate initialization and both crosschain directions in your target environment. * **Pausable** (and related account behavior): requires `anchor-spl >= 0.32.1`. * **Other extensions**: behavior can vary by OFT mode (native vs adapter), fee configuration, and dependency versions. Test initialization, local transfers, and sends in both directions before confirming the usage of a particular token extension. ### Cross Program Invocation into the OFT Program (CPI Depth limitation) Solana has the max [CPI Depth](https://solana.com/docs/core/cpi) of 4. A Solana OFT send instruction has the following CPI trace: ``` OFT -> Endpoint -> ULN -> Worker -> Pricefeed ``` Which is already 4 CPI calls deep, relative to the OFT program. The above means it's not currently possible to CPI into the OFT program, as it would violate the current [Solana CPI Depth limit of 4](https://solana.com/docs/programs/limitations#cpi-call-depth---calldepth-error). If you require a certain action to be taken in tandem with an `OFT.send` call, it would not be possible to have it be done in the same instruction. However, since Solana allows for multiple instructions per transaction, you can instead have it be grouped into the same transaction as the `OFT.send` instruction. For example, if you have a project that involves staking OFTs crosschain, and when unstaking (let's refer to this instruction as `StakingProgram.unstake`), you want to allow for the OFT to be sent (via `OFT.send`) to another chain in the same transaction, then you can do the following: * prepare the `StakingProgram.unstake` instruction * prepare the `OFT.send` instruction * submit both instructions in one transaction It would not be possible for you to have call `OFT.send` inside the `StakingProgram`'s `unstake` instruction directly since this would result in the following CPI trace: `StakingProgram -> OFT -> Endpoint -> ULN -> Worker -> Pricefeed`, which has a CPI depth of 5, exceeding the limit of 4. # LayerZero V2 Solana OFT SDK Source: https://docs.layerzero.network/v2/developers/solana/oft/sdk Use the Solana OFT SDK to interact with LayerZero OFTs on Solana. Build crosschain token transfers with the oft-v2-solana-sdk. Build omnichain tokens with L... You can use the Solana OFT SDK - [@layerzerolabs/oft-v2-solana-sdk ](https://www.npmjs.com/package/@layerzerolabs/oft-v2-solana-sdk) library to interact with your Solana OFT. Setting up a project using the LayerZero CLI would have given you scripts under the [tasks/solana](https://github.com/LayerZero-Labs/devtools/tree/main/examples/oft-solana/tasks/solana) folder that utilizes the Solana OFT SDK. You can refer to [tasks/common/sendOFT.ts](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/common/sendOFT.ts) for the example usage. ## Using the Solana OFT SDK in the Frontend The Solana OFT SDK has been updated to be browser-compatible. Versions prior to `3.0.71` required more additional configurations via your bundling tool. For Next projects, no additional configurations are required to use the Solana OFT SDK. For Vite projects, the following are the minimal configurations required to work. `nodePolyfills` is required as `Buffer` is required by `@solana/web3.js` but Vite does not polyfill it by default. ```typescript wrap theme={null} // vite.config.ts import {defineConfig} from 'vite'; import react from '@vitejs/plugin-react'; import {nodePolyfills} from 'vite-plugin-node-polyfills'; // https://vite.dev/config/ export default defineConfig({ plugins: [react(), nodePolyfills()], }); ``` ### Requirements * `@layerzerolabs/oft-v2-solana-sdk@^3.0.86` * `@layerzerolabs/lz-v2-utilities@^3.0.86` * `@layerzerolabs/lz-definitions@^3.0.86` * `@metaplex-foundation/umi@^0.9.2` * `@metaplex-foundation/umi-bundle-defaults@^0.9.2` * `@metaplex-foundation/umi-signer-wallet-adapters@^0.9.2` * `@solana/web3.js@^1.95.8` #### Applying overrides for `@solana/web3.js` In `package.json` add the following `resolutions` / `overrides` to ensure a consistent version of `@solana/web3.js` is used: ```npm theme={null} "overrides": { "@solana/web3.js": "~1.95.8" } ``` ```pnpm theme={null} "pnpm": { "overrides": { "@solana/web3.js": "~1.95.8" } } ``` ```yarn theme={null} "resolutions": { "@solana/web3.js": "~1.95.8" } ``` ## Compatibility with `@solana/web3.js` Under the hood, uses `@metaplex-foundation/umi`, which is an alternative to `@solana/web3.js`. If your project is using `@solana/web3.js`, you can utilize [adapters for @solana/web3.js](https://developers.metaplex.com/umi/web3js-differences-and-adapters). ## Troubleshooting ### `Invalid Connection` This can occur when there are multiple incompatible versions of `@solana/web3.js`. We need to ensure a consistent version is used due to the usage of `@metaplex-foundation/umi@^0.9.2`. To verify that this is the issue, run `npm ls @solana/web3.js` and check whether there are multiple versions of `@solana/web3.js` in the output. To solve this issue, do the following: * delete your `node_modules` folder * delete your package manager's lockfile * in your package.json, specify `@solana/web3.js@^1.95.8` as the dependency and also [apply overrides](#applying-overrides-for-solanaweb3js) * rerun your package manager install command. # LayerZero V2 Solana Programs Source: https://docs.layerzero.network/v2/developers/solana/overview Overview of Solana Programs on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure crosschain messaging. The LayerZero Protocol consists of several programs built on the Solana blockchain designed to facilitate the secure movement of data, tokens, and digital assets between different blockchain environments. LayerZero provides **Solana Programs** that can communicate directly with the equivalent [Solidity Contract Libraries](/v2/developers/evm/overview) deployed on EVM-based chains. ## Solana Programs Learn how the LayerZero V2 Protocol operates on the Solana blockchain. Build the Endpoint instructions necessary for sending arbitrary data and external function calls crosschain. Create and send Omnichain Fungible Tokens (OFTs) on the Solana blockchain. ## Solana Protocol Configurations Configure which decentralized verifier networks (DVNs) secure your messages. Configure who executes your messages on the destination chain. Set the amount of gas to deliver to the destination chain.
You can find all [**LayerZero Solana Programs**](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/solana/programs) here. ### Tooling and Resources Solana development relies heavily on Rust and the Solana CLI. For more information, see an [Overview of Developing Solana Programs](https://solana.com/docs/programs/overview). LayerZero provides developer tooling to simplify the contract creation, testing, and deployment process: [LayerZero Scan](/v2/developers/layerzero-scan-explorer): a comprehensive crosschain explorer, search, API, and analytics platform for tracking and debugging your omnichain transactions. You can also ask for help or follow development in the [Discord](https://discord.com/invite/ktbvm8Nkcr). # LayerZero V2 Solana Protocol Overview Source: https://docs.layerzero.network/v2/developers/solana/technical-overview Step-by-step guide to layerzero v2 solana protocol overview using LayerZero V2. Build and deploy omnichain applications with crosschain messaging. Follow st... LayerZero V2 on Solana mirrors the design of the EVM version in that it coordinates crosschain messaging through multiple protocol smart contracts. However, instead of EVM contracts and events, Solana programs use CPIs (cross–program invocations), PDAs (program–derived addresses), and a series of instructions that are tightly validated by Anchor: * **Send Workflow:** How a crosschain message packet is created, fees calculated via the Message Library, and sent from the source chain. * **DVN Verification Workflow:** How an application's configured decentralized verifier networks (DVNs) initialize and later verify the message payload. * **Executor Workflow:** How the Executor program finally executes the message (invoking the receiving OApp via an `lzReceive` call). ### Send Overview When a user sends a crosschain message, the following high–level steps occur: #### Endpoint Program 1. **Send Instruction on the LayerZero Endpoint:**\ The `Send` instruction is called on the Endpoint program via a CPI call from another program: * Increments the outbound nonce. * Constructs a unique [packet](../../concepts/protocol/packet) (including a GUID computed via a hash of parameters). * Invokes the send library (e.g. ULN302) via CPI to calculate fee allocations and emit the corresponding events. ```rust wrap theme={null} impl Send<'_> { /// Applies the send function, which sends a LayerZero message packet. /// /// # Parameters /// - `ctx`: The execution context containing all required accounts. /// - `params`: The parameters for sending, which include destination, receiver, message payload, fee details, and options. /// /// # Returns /// - `MessagingReceipt`: Contains the unique GUID, the nonce, and the fee breakdown for the sent message. pub fn apply<'c: 'info, 'info>( ctx: &mut Context<'_, '_, 'c, 'info, Send<'info>>, params: &SendParams, ) -> Result { // 1. Increment the outbound nonce. // Each message sent increases the nonce to guarantee a gapless and unique message sequence. ctx.accounts.nonce.outbound_nonce += 1; // 2. Build and encode the packet: // - Retrieve the sender's address. // - Generate a globally unique identifier (GUID) for the message using the new nonce, // the source Endpoint's ID, sender address, destination endpoint, and receiver. let sender = ctx.accounts.sender.key(); let guid = get_guid( ctx.accounts.nonce.outbound_nonce, ctx.accounts.endpoint.eid, sender, params.dst_eid, params.receiver, ); // Create the packet structure with all the message details. let packet = Packet { nonce: ctx.accounts.nonce.outbound_nonce, src_eid: ctx.accounts.endpoint.eid, sender, dst_eid: params.dst_eid, receiver: params.receiver, guid, message: params.message.clone(), }; // 3. Validate the configured send library: // This ensures that the correct send library is in use for this application and destination. let send_library = assert_send_library( &ctx.accounts.send_library_info, &ctx.accounts.send_library_program.key, &ctx.accounts.send_library_config, &ctx.accounts.default_send_library_config, )?; // 4. Set up the CPI call: // Prepare the seeds needed to sign the CPI call to the send library. let seeds: &[&[&[u8]]] = &[&[MESSAGE_LIB_SEED, send_library.as_ref(), &[ctx.accounts.send_library_info.bump]]]; let cpi_ctx = CpiContext::new_with_signer( ctx.accounts.send_library_program.to_account_info(), messagelib_interface::cpi::accounts::Interface { endpoint: ctx.accounts.send_library_info.to_account_info(), }, seeds, ) .with_remaining_accounts(ctx.remaining_accounts.to_vec()); // 5. Call the send library via CPI: // The send library implements two interfaces: one for sending with native tokens, // and one for sending with LZ token fees. Here we decide which to call based on the fee provided. let (fee, encoded_packet) = if params.lz_token_fee == 0 { // When paying with native tokens: let send_params = messagelib_interface::SendParams { packet, options: params.options.clone(), native_fee: params.native_fee, }; messagelib_interface::cpi::send(cpi_ctx, send_params)?.get() } else { // When paying with LZ tokens: let lz_token_mint = ctx.accounts.endpoint.lz_token_mint .ok_or(LayerZeroError::LzTokenUnavailable)?; let send_params = messagelib_interface::SendWithLzTokenParams { packet, options: params.options.clone(), native_fee: params.native_fee, lz_token_fee: params.lz_token_fee, lz_token_mint, }; messagelib_interface::cpi::send_with_lz_token(cpi_ctx, send_params)?.get() }; // 6. Emit an event to signal that a packet has been sent. // This event notifies offchain infrastructure (like DVNs and executors) about the sent message. emit_cpi!(PacketSentEvent { encoded_packet, options: params.options.clone(), send_library, }); // 7. Return a MessagingReceipt containing the GUID, nonce, and fee details. Ok(MessagingReceipt { guid, nonce: ctx.accounts.nonce.outbound_nonce, fee }) } } ``` #### SendUln302 Program 2. **Fee Quotation and Payment via CPI:**\ The send library (ULN302) uses instructions like `QuoteExecutor` and `QuoteDvn` via a series of CPI calls to programs such as the Executor and DVN. ```rust wrap theme={null} impl Quote<'_> { /// Applies the quote function, which calculates the messaging fee required for sending a packet. /// /// # Parameters /// - `ctx`: The execution context containing all required accounts. /// - `params`: The parameters for quoting, including packet details and options. /// /// # Returns /// - `MessagingFee`: The fee breakdown (native fee and LZ token fee). pub fn apply(ctx: &Context, params: &QuoteParams) -> Result { // Retrieve the configuration for the ULN (send configuration) and the executor configuration. // This function merges the custom configuration from the OApp with the default configuration. let (uln_config, executor_config) = get_send_config(&ctx.accounts.send_config, &ctx.accounts.default_send_config)?; // Decode the options passed in the quote parameters. // The options might include specific settings for the executor and DVN fee calculations. let (executor_options, dvn_options) = decode_options(¶ms.options)?; // -------------------------- // CPI call to the Executor for fee quotation. // This call queries the executor configuration to estimate the fee based on: // - The ULN's key (which represents the OApp's messaging context) // - The destination endpoint ID // - The sender and the length of the message payload // - Specific executor options (e.g., gas or compute units) // - A slice of the remaining accounts expected to be used by the executor CPI call // -------------------------- let executor_fee = quote_executor( &ctx.accounts.uln.key(), &executor_config, params.packet.dst_eid, ¶ms.packet.sender, params.packet.message.len() as u64, executor_options, &ctx.remaining_accounts[0..4], )?; // -------------------------- // CPI call to the DVN(s) for fee quotation. // This call queries the configured DVNs to get their fee quotes based on: // - The ULN's key (providing the messaging context) // - The ULN configuration which includes DVN settings // - The destination endpoint ID and sender details // - The encoded packet header and the hashed payload (GUID + message) // - Specific DVN options (if any) // - A slice of the remaining accounts expected to be used for DVN CPI calls // -------------------------- let dvn_fees = quote_dvns( &ctx.accounts.uln.key(), &uln_config, params.packet.dst_eid, ¶ms.packet.sender, encode_packet_header(¶ms.packet), hash_payload(¶ms.packet.guid, ¶ms.packet.message), dvn_options, &ctx.remaining_accounts[4..], )?; // Sum up the fees from both the executor and DVNs. // Here, `worker_fee` is the total fee required to cover the processing by both workers. let worker_fee = executor_fee.fee + dvn_fees.iter().map(|f| f.fee).sum::(); // Calculate the final fee breakdown based on treasury settings. // If the ULN treasury is configured, determine the treasury fee and adjust the native fee or LZ token fee // depending on whether fees are being paid in LZ token. let (native_fee, lz_token_fee) = if let Some(treasury) = ctx.accounts.uln.treasury.as_ref() { let treasury_fee = quote_treasury(treasury, worker_fee, params.pay_in_lz_token)?; if params.pay_in_lz_token { // When paying with LZ token, the native fee remains as the worker fee, // and the treasury fee is taken from the LZ token fee. (worker_fee, treasury_fee) } else { // Otherwise, add the treasury fee to the worker fee and set LZ token fee to 0. (worker_fee + treasury_fee, 0) } } else { // If no treasury is configured, the fee is simply the worker fee. (worker_fee, 0) }; // Return the final messaging fee. Ok(MessagingFee { native_fee, lz_token_fee }) } } ``` 3. **Endpoint Packet Emission:**\ Finally, after fee calculations and transfers, the Endpoint program emits an event (e.g. `PacketSentEvent`) and the packet is recorded onchain. ```rust wrap theme={null} // packages/layerzero-v2/solana/programs/programs/endpoint/src/instructions/oapp/send.rs emit_cpi!(PacketSentEvent { encoded_packet, options: params.options.clone(), send_library, }); Ok(MessagingReceipt { guid, nonce: ctx.accounts.nonce.outbound_nonce, fee }) ``` ### Verification Workflow After the send operation, the DVNs must verify the message on the destination chain before message execution. On Solana, every account must be explicitly allocated with sufficient space. For DVN verification, this means a dedicated payload hash account is first created and initialized. This ensures that when a DVN writes its witness, the storage exists and is correctly sized. #### DVN Verification Each DVN individually performs the following steps: 1. **Initialization with `ReceiveULN.init_verify`:**\ The DVN calls `init_verify` on the ULN program to create and initialize a dedicated `Confirmations` account. The `init_verify` method initializes the Confirmations's account `value` field as `None`, and stores its [PDA bump](https://solana.stackexchange.com/questions/2271/what-is-the-bump-in-a-program-derived-address). ```rust wrap theme={null} // packages/layerzero-v2/solana/programs/programs/uln/src/instructions/dvn/init_verify.rs // This function initializes the confirmations account used for DVN verification. impl InitVerify<'_> { pub fn apply(ctx: &mut Context, _params: &InitVerifyParams) -> Result<()> { ctx.accounts.confirmations.value = None; ctx.accounts.confirmations.bump = ctx.bumps.confirmations; Ok(()) } } ``` 2. **Invocation with `invoke`:**\ After initialization, the DVN triggers its own verification logic via an `invoke` instruction. This CPI call executes internal checks (such as signature verification and configuration validation) and, in the process, calls into the ULN’s verification logic by triggering a CPI call to the `verify` instruction. ```rust wrap theme={null} // packages/layerzero-v2/solana/programs/programs/dvn/src/instructions/admin/invoke.rs impl Invoke<'_> { /// Applies the DVN verification logic by processing the execution digest. /// Ultimately, this invoke call triggers a CPI to the ULN's `verify` instruction. pub fn apply(ctx: &mut Context, params: &InvokeParams) -> Result<()> { // 1. Verify that the DVN configuration version (vid) matches the digest's version. require!(ctx.accounts.config.vid == params.digest.vid, DvnError::InvalidVid); // 2. Check that the transaction has not expired. require!(params.digest.expiration > Clock::get()?.unix_timestamp, DvnError::Expired); // 3. Compute the hash of the digest data; used for signature verification. let hash = keccak::hash(¶ms.digest.data()?).to_bytes(); // 4. Verify that the provided signatures are valid for the computed hash. ctx.accounts.config.multisig.verify_signatures(¶ms.signatures, &hash)?; // 5. Update the execute_hash account with the expiration and bump. ctx.accounts.execute_hash.expiration = params.digest.expiration; ctx.accounts.execute_hash.bump = ctx.bumps.execute_hash; // 6. Process the digest based on the target program ID. if params.digest.program_id == ID { // If the digest targets this DVN program: let mut data = params.digest.data.as_slice(); let config = MultisigConfig::deserialize(&mut data)?; let is_set_admin = matches!(config, MultisigConfig::Admins(_)); if !is_set_admin { require!( ctx.accounts.config.admins.contains(ctx.accounts.signer.key), DvnError::NotAdmin ); } config.apply(&mut ctx.accounts.config)?; emit_cpi!(MultisigConfigSetEvent { config }); } else { // If the digest targets a different program: require!( ctx.accounts.config.admins.contains(ctx.accounts.signer.key), DvnError::NotAdmin ); let mut accounts = Vec::with_capacity(params.digest.accounts.len()); let config_acc = ctx.accounts.config.key(); for acc in params.digest.accounts.iter() { let mut meta = AccountMeta::from(acc); if meta.pubkey == config_acc && acc.is_signer { meta.is_writable = false; } accounts.push(meta); } let ix = Instruction { program_id: params.digest.program_id, accounts, data: params.digest.data.clone(), }; invoke_signed( &ix, ctx.remaining_accounts, &[&[DVN_CONFIG_SEED, &[ctx.accounts.config.bump]]], )?; } Ok(()) } } ``` 3. **Final Verification via `ReceiveULN.verify`:**\ Once the DVN’s internal verification logic completes and the conditions are met, the ULN program finalizes the DVN verification by calling its own `verify` function. This function updates the DVN-specific payload hash and emits a `PayloadVerifiedEvent` to signal that the message has been verified by that DVN. ```rust wrap theme={null} // packages/layerzero-v2/solana/programs/programs/uln/src/instructions/dvn/verify.rs // This function finalizes the DVN verification process on the ULN side. impl Verify<'_> { pub fn apply(ctx: &mut Context, params: &VerifyParams) -> Result<()> { ctx.accounts.confirmations.value = Some(params.confirmations); emit_cpi!(PayloadVerifiedEvent { dvn: ctx.accounts.dvn.key(), header: params.packet_header, confirmations: params.confirmations, proof_hash: params.payload_hash, }); Ok(()) } } ``` **Summary of DVN Verification:** * **`ReceiveUln.init_verify()`:** Initializes a dedicated payload hash account with an empty hash. * **`DVN.invoke()`:** Executes the DVN’s internal verification logic and triggers the ULN’s `verify` instruction via a nested CPI. * **`ReceiveUln.verify()`:** The ULN finalizes the verification by updating the payload hash and emitting a `PayloadVerifiedEvent`. #### Commit Verification After all required verifications have been submitted (meeting the [X of Y of N](../../concepts/glossary#x-of-y-of-n) configuration), the payload hash can then be committed. The commit verification process ensures that the verified message is recorded in the Endpoint’s messaging channel. This process comprises two primary steps: 1. **Initialization via `Endpoint.init_verify` on the Endpoint:**\ Before committing the verification, the system calls `init_verify` on the Endpoint. This creates and initializes a dedicated payload hash account, reserving space for the verification data. The account is set up with an initial empty payload hash (`EMPTY_PAYLOAD_HASH`) and a bump value for PDA derivation. ```rust wrap theme={null} // packages/layerzero-v2/solana/programs/programs/endpoint/src/instructions/init_verify.rs impl InitVerify<'_> { pub fn apply(ctx: &mut Context, _params: &InitVerifyParams) -> Result<()> { // Initialize with an empty payload hash. ctx.accounts.payload_hash.hash = EMPTY_PAYLOAD_HASH; // Save the bump value for future PDA derivation. ctx.accounts.payload_hash.bump = ctx.bumps.payload_hash; Ok(()) } } ``` 2. **Committing Verification via `commitVerification` on ReceiveUln302:**\ Once the payload hash account is initialized and DVN confirmations have been collected, the `ReceiveUln302.commitVerification()` function is called to finalize the verification by: * **Validating the Packet Header:**\ It checks that the header version is correct and that the destination endpoint ID (EID) matches the ULN302’s configured EID. * **Verifying DVN Confirmations:**\ It calculates the number of DVN confirmation accounts (both required and optional) and uses helper functions (e.g., `check_verifiable` and `verified`) to ensure that every DVN has provided sufficient confirmation. * **CPI to the Endpoint’s `verify` Instruction:**\ If all checks pass, a CPI call is made to the Endpoint’s `verify` function. This call updates the payload hash stored in the dedicated account and emits a `PacketVerifiedEvent`, thereby recording the verified message on the Endpoint’s messaging channel. ```rust wrap theme={null} // packages/layerzero-v2/solana/programs/programs/uln/src/instructions/dvn/commit_verification.rs impl CommitVerification<'_> { pub fn apply( ctx: &mut Context, params: &CommitVerificationParams, ) -> Result<()> { // Retrieve the effective receive configuration (combining custom and default settings) let config = get_receive_config( &ctx.accounts.receive_config, &ctx.accounts.default_receive_config )?; // Validate the packet header: // 1. Ensure the header version matches the expected version. require!( packet_v1_codec::version(¶ms.packet_header) == PACKET_VERSION, UlnError::InvalidPacketVersion ); // 2. Ensure the destination EID matches the ULN302's configured EID. require!( packet_v1_codec::dst_eid(¶ms.packet_header) == ctx.accounts.uln.eid, UlnError::InvalidEid ); // Determine the number of DVN accounts (required and optional) let dvns_size = config.required_dvns.len() + config.optional_dvns.len(); // Verify that all DVN confirmation accounts provide a valid confirmation. let confirmation_accounts = &ctx.remaining_accounts[0..dvns_size]; require!( check_verifiable( &config, confirmation_accounts, &keccak256(¶ms.packet_header).to_bytes(), ¶ms.payload_hash )?, UlnError::Verifying ); // Commit the verification by calling the Endpoint's verify instruction via CPI. endpoint_verify::verify( ctx.accounts.uln.endpoint_program, ctx.accounts.uln.key(), ¶ms.packet_header, params.payload_hash, &[ULN_SEED, &[ctx.accounts.uln.bump]], &ctx.remaining_accounts[dvns_size..], ) } } ``` 3. **Insert Hash into the Endpoint's Message Channel via `verify`:**\ The Endpoint’s `verify` method is the final step in the commit verification process. Once invoked via CPI, it performs the following actions: * **Nonce Management:**\ It checks if the packet’s nonce is greater than the current inbound nonce and updates the pending inbound nonce if necessary. * **Updating the Payload Hash:**\ The verified payload hash is written into the payload hash account. * **Event Emission:**\ A `PacketVerifiedEvent` is emitted, signaling that the packet has been verified and recorded onchain. ```rust wrap theme={null} // packages/layerzero-v2/solana/programs/programs/endpoint/src/instructions/verify.rs use crate::*; use cpi_helper::CpiContext; use solana_program::clock::Slot; /// MESSAGING STEP 2 /// requires init_verify() #[event_cpi] #[derive(CpiContext, Accounts)] #[instruction(params: VerifyParams)] pub struct Verify<'info> { /// The PDA of the receive library. #[account( constraint = is_valid_receive_library( receive_library.key(), &receive_library_config, &default_receive_library_config, Clock::get()?.slot ) @LayerZeroError::InvalidReceiveLibrary )] pub receive_library: Signer<'info>, #[account( seeds = [RECEIVE_LIBRARY_CONFIG_SEED, ¶ms.receiver.to_bytes(), ¶ms.src_eid.to_be_bytes()], bump = receive_library_config.bump )] pub receive_library_config: Account<'info, ReceiveLibraryConfig>, #[account( seeds = [RECEIVE_LIBRARY_CONFIG_SEED, ¶ms.src_eid.to_be_bytes()], bump = default_receive_library_config.bump )] pub default_receive_library_config: Account<'info, ReceiveLibraryConfig>, #[account( mut, seeds = [ NONCE_SEED, ¶ms.receiver.to_bytes(), ¶ms.src_eid.to_be_bytes(), ¶ms.sender[..] ], bump = nonce.bump )] pub nonce: Account<'info, Nonce>, #[account( mut, seeds = [ PENDING_NONCE_SEED, ¶ms.receiver.to_bytes(), ¶ms.src_eid.to_be_bytes(), ¶ms.sender[..] ], bump = pending_inbound_nonce.bump )] pub pending_inbound_nonce: Account<'info, PendingInboundNonce>, #[account( mut, seeds = [ PAYLOAD_HASH_SEED, ¶ms.receiver.to_bytes(), ¶ms.src_eid.to_be_bytes(), ¶ms.sender[..], ¶ms.nonce.to_be_bytes() ], bump = payload_hash.bump, constraint = params.payload_hash != EMPTY_PAYLOAD_HASH @LayerZeroError::InvalidPayloadHash )] pub payload_hash: Account<'info, PayloadHash>, } impl Verify<'_> { pub fn apply(ctx: &mut Context, params: &VerifyParams) -> Result<()> { // No need for initializable() or verifiable() checks, as init_verify() already enforces the nonce requirement. // Update the pending inbound nonce if the message nonce is greater. if params.nonce > ctx.accounts.nonce.inbound_nonce { ctx.accounts .pending_inbound_nonce .insert_pending_inbound_nonce(params.nonce, &mut ctx.accounts.nonce)?; } // Write the verified payload hash into the payload hash account. ctx.accounts.payload_hash.hash = params.payload_hash; // Emit an event to signal that the packet has been verified. emit_cpi!(PacketVerifiedEvent { src_eid: params.src_eid, sender: params.sender, receiver: params.receiver, nonce: params.nonce, payload_hash: params.payload_hash, }); Ok(()) } } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct VerifyParams { pub src_eid: u32, pub sender: [u8; 32], pub receiver: Pubkey, pub nonce: u64, pub payload_hash: [u8; 32], } ``` **Summary of Commit Verification:** * **`Endpoint.init_verify()`:** Creates and initializes a dedicated payload hash account with an empty hash. * **`ReceiveUln302.commitVerification()`:** Validates the packet header and DVN confirmations, then commits the verification by calling the Endpoint's `verify` via CPI. * **`Endpoint.verify()`:** Inserts the verified payload hash into the messaging channel, updates nonce management, and emits a `PacketVerifiedEvent`. Together, these steps ensure that only messages with sufficient DVN confirmations are recorded onchain in the Endpoint's messaging channel, thereby maintaining the integrity and security of the crosschain message. ### Receive Workflow The Solana receive flow is divided into three primary stages: 1. **Execute:**\ The Executor program initiates the message execution process by calling its `execute` instruction. In this step, the Executor: * Gathers all required accounts. * Invokes downstream instructions via CPI to eventually call `lzReceive`. * Checks that its lamport balance does not drop unexpectedly. * If the CPI call fails, an alert is triggered via `lzReceiveAlert`. ```rust wrap theme={null} // packages/layerzero-v2/solana/programs/programs/executor/src/instructions/execute.rs #[event_cpi] #[derive(Accounts)] pub struct Execute<'info> { #[account(mut)] pub executor: Signer<'info>, #[account( seeds = [EXECUTOR_CONFIG_SEED], bump = config.bump, constraint = config.executors.contains(executor.key) @ExecutorError::NotExecutor )] pub config: Account<'info, ExecutorConfig>, pub endpoint_program: Program<'info, Endpoint>, /// The authority for the endpoint program to emit events pub endpoint_event_authority: UncheckedAccount<'info>, } impl Execute<'_> { pub fn apply(ctx: &mut Context, params: &ExecuteParams) -> Result<()> { let balance_before = ctx.accounts.executor.lamports(); let program_id = ctx.remaining_accounts[0].key(); let accounts = ctx .remaining_accounts .iter() .skip(1) .map(|acc| acc.to_account_metas(None)[0].clone()) .collect::>(); let data = get_lz_receive_ix_data(¶ms.lz_receive)?; let result = invoke(&Instruction { program_id, accounts, data }, ctx.remaining_accounts); if let Err(e) = result { // If execution fails, trigger an alert. let params = LzReceiveAlertParams { /* omitted for brevity */ }; let cpi_ctx = LzReceiveAlert::construct_context( ctx.accounts.endpoint_program.key(), &[ ctx.accounts.config.to_account_info(), // executor config as signer ctx.accounts.endpoint_event_authority.to_account_info(), ctx.accounts.endpoint_program.to_account_info(), ], )?; endpoint::cpi::lz_receive_alert( cpi_ctx.with_signer(&[&[EXECUTOR_CONFIG_SEED, &[ctx.accounts.config.bump]]]), params, )?; } else { // Ensure the executor did not lose more lamports than expected. let balance_after = ctx.accounts.executor.lamports(); require!( balance_before <= balance_after + params.value, ExecutorError::InsufficientBalance ); } require!( ctx.accounts.executor.owner.key() == system_program::ID, ExecutorError::InvalidOwner ); require!(ctx.accounts.executor.data_is_empty(), ExecutorError::InvalidSize); Ok(()) } } ``` 2. **LzReceiveTypes – Account Assembly:**\ The `lzReceiveTypes` instruction gathers all the accounts required by the final message execution. This step constructs the list of accounts—including PDAs for the peer, configuration accounts, token escrow (if needed), token destination, mint, and various system accounts—based on the parameters of the received message. ```rust wrap theme={null} // packages/solana/programs/counter/src/instructions/lz_receive_types.rs use crate::*; use oapp::endpoint_cpi::{get_accounts_for_clear, get_accounts_for_send_compose, LzAccount}; use oapp::{endpoint::ID as ENDPOINT_ID, LzReceiveParams}; /// LzReceiveTypes provides the list of accounts required in the subsequent LzReceive instruction. #[derive(Accounts)] pub struct LzReceiveTypes<'info> { #[account(seeds = [COUNT_SEED, &count.id.to_be_bytes()], bump = count.bump)] pub count: Account<'info, Count>, } impl LzReceiveTypes<'_> { pub fn apply( ctx: &Context, params: &LzReceiveParams, ) -> Result> { // Determine the fixed count account. let count = ctx.accounts.count.key(); // Derive the remote PDA using the source endpoint id. let seeds = [REMOTE_SEED, &count.to_bytes(), ¶ms.src_eid.to_be_bytes()]; let (remote, _) = Pubkey::find_program_address(&seeds, ctx.program_id); // Start with the count and remote accounts. let mut accounts = vec![ LzAccount { pubkey: count, is_signer: false, is_writable: true }, LzAccount { pubkey: remote, is_signer: false, is_writable: false }, ]; // Append accounts required by the clear instruction (from the Endpoint). let accounts_for_clear = get_accounts_for_clear( ENDPOINT_ID, &count, params.src_eid, ¶ms.sender, params.nonce, ); accounts.extend(accounts_for_clear); // If the message type is composed, append accounts for the compose instruction. let is_composed = msg_codec::msg_type(¶ms.message) == msg_codec::COMPOSED_TYPE; if is_composed { let accounts_for_composing = get_accounts_for_send_compose( ENDPOINT_ID, &count, &count, // self, for example ¶ms.guid, 0, ¶ms.message, ); accounts.extend(accounts_for_composing); } Ok(accounts) } } ``` 3. **LzReceive – Final Message Execution:**\ Finally, the `lzReceive` instruction executes the received message. This is where the actual processing occurs. In this step, the program must implement safety checks that clear the payload to prevent reentrancy and double execution. Specifically, it: * **Clears the Payload:**\ Updates nonces, verifies that the payload hash matches the verified data, and deletes the message from storage. * **Performs Token Operations (if applicable):**\ Depending on the message type, it may mint tokens or perform transfers. * **Emits an Event:**\ Signals that the message has been successfully received and processed. ```rust wrap theme={null} // packages/solana/programs/counter/src/instructions/lz_receive.rs use crate::*; use anchor_lang::prelude::*; use oapp::{ endpoint::{ cpi::accounts::Clear, instructions::{ClearParams, SendComposeParams}, ConstructCPIContext, ID as ENDPOINT_ID, }, LzReceiveParams, }; #[derive(Accounts)] #[instruction(params: LzReceiveParams)] pub struct LzReceive<'info> { #[account(mut, seeds = [COUNT_SEED, &count.id.to_be_bytes()], bump = count.bump)] pub count: Account<'info, Count>, #[account( seeds = [REMOTE_SEED, &count.key().to_bytes(), ¶ms.src_eid.to_be_bytes()], bump = remote.bump, constraint = params.sender == remote.address )] pub remote: Account<'info, Remote>, } impl LzReceive<'_> { pub fn apply(ctx: &mut Context, params: &LzReceiveParams) -> Result<()> { let seeds: &[&[u8]] = &[COUNT_SEED, &ctx.accounts.count.id.to_be_bytes(), &[ctx.accounts.count.bump]]; // **Clear the payload.** // This step updates nonces, verifies the payload hash, and deletes the message to prevent reentrancy. let accounts_for_clear = &ctx.remaining_accounts[0..Clear::MIN_ACCOUNTS_LEN]; let _ = oapp::endpoint_cpi::clear( ENDPOINT_ID, ctx.accounts.count.key(), accounts_for_clear, seeds, ClearParams { receiver: ctx.accounts.count.key(), src_eid: params.src_eid, sender: params.sender, nonce: params.nonce, guid: params.guid, message: params.message.clone(), }, )?; // Execute token operations or minting if applicable. // For a composed message, trigger the compose logic. let msg_type = msg_codec::msg_type(¶ms.message); match msg_type { msg_codec::VANILLA_TYPE => ctx.accounts.count.count += 1, msg_codec::COMPOSED_TYPE => { ctx.accounts.count.count += 1; oapp::endpoint_cpi::send_compose( ENDPOINT_ID, ctx.accounts.count.key(), &ctx.remaining_accounts[Clear::MIN_ACCOUNTS_LEN..], seeds, SendComposeParams { to: ctx.accounts.count.key(), // For example, self guid: params.guid, index: 0, message: params.message.clone(), }, )?; }, _ => return Err(CounterError::InvalidMessageType.into()), } Ok(()) } } ``` ### Key Solana-Specific Considerations * **Explicit Safety Checks:**\ Unlike the EVM, where safety checks such as payload clearing are handled by a provided inheritance pattern, the Solana OApp must explicitly implement these checks within its `lzReceive` logic. This includes updating nonces, verifying payload integrity, and deleting processed messages to prevent reentrancy or double execution. * **CPI and Account Assembly:**\ The flow (`execute` → `lzReceiveTypes` → `lzReceive`) relies on explicit CPI calls, with each instruction receiving a full list of pre-allocated accounts. There is no runtime dispatch or inheritance; all required accounts must be passed along manually. * **Token Operations:**\ When the message carries token transfers (as in OFT), token operations (transfer or mint) are executed within `lzReceive` via CPI calls to the Token Program. This documentation outlines the full receive workflow on Solana, detailing the flow from message execution to final processing while emphasizing the responsibility of the OApp to implement its own safety measures within `lzReceive`. # Solana Guidance Source: https://docs.layerzero.network/v2/developers/solana/technical-reference/solana-guidance Technical reference for Solana Guidance. Complete API documentation with functions, parameters, and usage examples. LayerZero enables secure crosschain... This page provides development guidance for building on Solana. While some entries are LayerZero-specific, others cover general topics and tooling relevant to the Solana ecosystem. ## Deploying Solana programs with a priority fee This section applies if you are unable to land your deployment transaction due to network congestion. [Priority Fees](https://solana.com/developers/guides/advanced/how-to-use-priority-fees) are Solana's mechanism to allow transactions to be prioritized during periods of network congestion. When the network is busy, transactions without priority fees might never be processed. It is then necessary to include priority fees, or wait until the network is less congested. Priority fees are calculated as follows: `priorityFee = compute budget * compute unit price`. We can make use of priority fees by attaching the `--with-compute-unit-price` flag to our `solana program deploy` command. Note that the flag takes in a value in micro lamports, where 1 micro lamport = 0.000001 lamport. For example: ```bash wrap theme={null} solana program deploy --program-id target/deploy/oft-keypair.json target/verifiable/oft.so -u devnet --with-compute-unit-price ``` You can refer QuickNode's [Solana Priority Fee Tracker](https://www.quicknode.com/gas-tracker/solana) to know what value you'd need to pass into the `--with-compute-unit-price` flag. ## Previewing Solana rent costs Most of the SOL you spend during a program deployment goes toward [rent](https://solana.com/docs/core/fees#rent) so the program account can remain rent-exempt. This cost scales with the size (in bytes) of the compiled `.so` artifact, while the additional accounts you create (for example, PDAs or configuration accounts) typically contribute only a small fraction of the total. You can preview the rent-exempt minimum required for your compiled program with the Solana CLI: ```bash wrap theme={null} solana rent $(wc -c < target/verifiable/) ``` Expect the total SOL needed for deployment to be slightly higher than the returned rent figure so you can cover the other accounts that get created as part of your setup. ## Transferring OFT ownership on Solana There are six roles regarding OFT ownership/authority on Solana: * **owner** * **delegate** * **upgrade authority** * **token metadata update authority** * (if applicable) **mint authority** * (if applicable) **anchor IDL authority** * (if applicable) **freeze authority** When you deploy a Solana OFT, the deployer wallet is automatically set as the **owner** and **delegate**. The deployer wallet would also have been made as the [upgrade authority](https://solana.com/docs/core/programs#updating-solana-programs) of your OFT program. **Owner** and **delegate** are specific to the LayerZero OFT context whereas **upgrade authority** is generic to Solana. The transfer of **owner** and **delegate** are separate from the transfer of **upgrade authority**. The steps below are identical regardless if the new owner and delegate are Multisig accounts. ### Transferring Owner and Delegate The transfer of both require modifying your [LZ Config](../../../concepts/glossary#lz-config) file and running helper tasks. Overall, you should carry out these steps: 1. Modify LZ Config to include **only** the [new delegate address](../../../get-started/create-lz-oapp/configuring-pathways#adding-delegate) 2. Run `pnpm hardhat lz:oapp:wire --oapp-config layerzero.config.ts` 3. Modify LZ Config to include the [new owner address](../../../get-started/create-lz-oapp/configuring-pathways#adding-owner) 4. Run `pnpm hardhat lz:ownable:transfer-ownership --oapp-config layerzero.config.ts` You have now transferred both owner and delegate of your Solana OFT. ### Transferring the OFT Program Upgrade Authority The steps vary based on whether the current Upgrade Authority is your [local keypair](#when-the-current-upgrade-authority-is-your-local-keypair) or a [Squads Multisig](#when-the-current-upgrade-authority-is-a-squads-multisig). #### When the current Upgrade Authority is your local keypair The steps for when the current Upgrade Authority is your local keypair then also differ depending on whether (1) **the new upgrade authority is a regular account that you control** or whether (2) **the new upgrade authority is a Multisig or an account that you do not control**. They differ in whether the new upgrade authority's keypair is included or whether you use the `--skip-new-upgrade-authority-signer-check` param. ##### The new upgrade authority is an account that you control Run the follwowing: ```bash wrap theme={null} solana program set-upgrade-authority --keypair --new-upgrade-authority ``` ##### The new upgrade authority is a Squads Multisig or an account that you do not control The steps below are identical whether your new upgrade authority is a Squads Multisig or whether it's an account that you do no control. If it is a Squads Multisig, note that the address you want to pass in is the [Vault Account](https://docs.squads.so/main/navigating-your-squad/settings#vault-and-multisig-address) address. > This differs from the current `--multisig` param required by LayerZero helpers which requires the Multisig account address With the correct new upgrade authority address prepared, run the following: ```bash wrap theme={null} solana program set-upgrade-authority --skip-new-upgrade-authority-signer-check --new-upgrade-authority ``` #### When the current Upgrade Authority is a Squads Multisig Refer to the [Squads documentation on updating the Upgrade Authority](https://docs.squads.so/main/navigating-your-squad/developers-assets/programs#withdraw-program-upgrade-authority). ### Transferring the Token Metadata Update Authority The [Token Metadata](https://developers.metaplex.com/token-metadata) Update Authority is able to update the Solana token's metadata such as name, symbol, uri and creators information. To transfer the Update Authority, you can use the [setUpdateAuthority helper script](https://developers.metaplex.com/token-metadata/update): ```bash wrap theme={null} pnpm hardhat lz:oft:solana:set-update-authority --eid --mint --new-update-authority ``` * `` - `30168` for Solana Mainnet, `40168` for Solana Devnet > Read more on what the Update Authority can do here: [https://developers.metaplex.com/token-metadata/update](https://developers.metaplex.com/token-metadata/update) ### (if applicable) Transfer the Mint Authority This section only applies if the Solana OFT was created with Additional Minters. You can verify this by viewing the Solana OFT's Mint Authority. If it is an SPL Multisig with more than 1 address, then there are additional minters. If the Mint Authority is a single PDA and not an SPL Multisig, then the Mint Authority is the OFT Store and no transfer steps are necessary for the Mint Authority. SPL Multisigs can only be created and not be edited. If your current Solana OFT has an additional minter and you need to change the additional minter address, then a new 1 of N SPL Multisig needs to be created. For this, you can simply run the [setAuthority helper](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/solana/setAuthority.ts): #### If you no longer need additional minters: ```bash wrap theme={null} pnpm hardhat lz:oft:solana:setauthority --eid 30168 --mint --program-id --escrow --only-oft-store true ``` * `` - `30168` for Solana Mainnet, `40168` for Solana Devnet * `--only-oft-store true` - This is irreversible and will set the Mint Authority to the OFT Store directly. This will update the Mint Authority to be the OFT Store PDA. No SPL Multisig will be created. #### If there should be new additional minter(s) ```bash wrap theme={null} pnpm hardhat lz:oft:solana:setauthority --eid 30168 --mint --program-id --escrow --additional-minters ``` * `` - `30168` for Solana Mainnet, `40168` for Solana Devnet * `--additional-minters` comma-separated list of additional minters This will create a new 1 of N SPL Multisig with the OFT Store and the additional minter(s) as signers. ### (if applicable) Transfer the Anchor IDL Authority This only applies if you had previously uploade the Anchor IDL onchain. A quick way to test this is by running ```bash wrap theme={null} anchor idl authority --provider.cluster mainnet ``` If it returns with `Error: AccountNotFound: pubkey=`, this means no Anchor IDL PDA had been created and you can ignore this step. To transfer the Anchor IDL Authority, run: ```bash wrap theme={null} anchor idl set-authority --program-id --new-authority ``` ### (if applicable) Transfer the Freeze Authority While the OFT Program does not require the Freeze Authority at all, your Solana Token might have its Freeze Authority set. To transfer the Freeze Authority to a new address: ```bash wrap theme={null} spl-token authorize freeze ``` To renounce the Freeze Authority (irreversible): ```bash wrap theme={null} spl-token authorize freeze --disable ``` ## Deciding the number of local decimals for your Solana OFT As OFTs can span across VMs, with each VM potentially using a different data type for token amounts, it's important to understand the concept of decimals in the context of OFTs. Make sure you understand [shared decimals](../../../concepts/glossary#shared-decimals) and [local decimals](../../../concepts/glossary#local-decimals) before proceeding. Before running the `pnpm hardhat lz:oft:solana:create` command, you should have decided the number of values to pass in for both the `--shared-decimals` and `--local-decimals` params. For `--shared-decimals`, it should be the same across all your OFTs regardless of VM. Inconsistent values (i.e. one chain having a share decimals value of `4` while another has it as `6`) can result in value loss. For more detail, read [Token Transfer Precision](../oft/overview#token-transfer-precision). On EVM chains, the data type that represents token amounts is `uint256` and the common number of (local) decimals is `18`. This results in an astronomically high possible max supply value. ``` (2^256 - 1) / 10^18 ≈ 1.1579 × 10^59 // (1.1579 million trillion trillion trillion trillion trillion) ``` In practice, tokens are typically created with a manually set max supply, for example: 1 billion (1 × 10⁹), 50 trillion (5 × 10¹³) or 1 quadrillion ( 1 × 10¹⁵). Solana uses the `u64` type to represent token amounts, with the decimals value defaulting to `9`, although many tokens choose to go with `6` decimals. The possible max value by default (\~18 billion) is a lot lower, so it's important to select a local decimals value on Solana that can fit your token's max supply. Refer to the table below for a comparison between a Solana token's (local) decimals and the possible max supply value. **Max Supply in Solana for a Given Decimals Value (Decimals 9 to 4)** | **Decimals** | **Max Supply (in whole tokens)** | | :----------: | :---------------------------------: | | 9 | \~1.84 × 10¹⁰ ( \~18 billion ) | | 8 | \~1.84 × 10¹¹ ( \~184 billion ) | | 7 | \~1.84 × 10¹² ( \~1.8 trillion ) | | 6 | \~1.84 × 10¹³ ( \~18 trillion ) | | 5 | \~1.84 × 10¹⁴ ( \~184 trillion ) | | 4 | \~1.84 × 10¹⁵ ( \~1.8 quadrillion ) | If you create a Solana token with 18 decimals (common on EVM chains), the maximum supply on Solana will be only \~18 tokens due to the `u64` limit. Choose a lower decimals value that can accommodate your intended max supply. ## Squads Multisig – Multisig Account vs Vault In Squads, there are two distinct address types: * **Multisig Account** – the primary account that manages the Squad. * **Vault** – a derived account (at a specific index) where assets and program interactions occur. For a deeper explanation, refer to the official Squads documentation: [Vault and Multisig Address](https://docs.squads.so/main/navigating-your-squad/settings#vault-and-multisig-address). When using LayerZero Hardhat Helpers with the `--multisig-key` flag: * **Provide the Multisig Account address**, **not** the Vault address. * The helper internally derives the Vault address at **index 0** to propose transactions to. ## Creating a Squads Multisig on Solana Devnet [Squads](https://squads.xyz/) is the most widely used multisig on Solana. The current version of Squads is v4. The OFT tasks support the usage of a Squads via the `--multisig-key` param. On mainnet, you can create a v4 Multisig using the [Mainnet UI](https://app.squads.so/squads). On the [devnet UI](https://backup.app.squads.so/), you are currently not able to create a multisig. However, you can still perform operations such as voting on transactions and executing them. In order to create a Squads v4 Multisig for Solana Devnet, you have two options: CLI and Typescript SDK. ### Creating using the CLI With the [Squads CLI](https://docs.squads.so/main/development/cli/installation) installed, you can run: ```bash wrap theme={null} multisig-create --rpc-url --keypair --members ... --threshold ``` For full context and instructions, refer to [https://docs.squads.so/main/development/cli/commands#multisig-create](https://docs.squads.so/main/development/cli/commands#multisig-create) ### Creating using the Typescript SDK Dependencies: ``` "@solana-developers/helpers": "^2.5.6", "@solana/web3.js": "^1.98.0", "@sqds/multisig": "^2.1.3", ``` Here is a minimal script for creating a multisig via the Typescript SDK: ```typescript wrap theme={null} import * as multisig from '@sqds/multisig'; import {Connection, Keypair, clusterApiUrl} from '@solana/web3.js'; import {getKeypairFromFile} from '@solana-developers/helpers'; const {Permission, Permissions} = multisig.types; (async () => { const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); // or "mainnet-beta" for mainnet // Signers const creator = await getKeypairFromFile(); // first member + fee-payer // const secondMember = Keypair.generate(); // second member // if you add another member, remember to update the threshold if not going for 1 of N const createKey = Keypair.generate(); // seed for the PDA (must sign) // Derive PDA for the multisig. This will be the multisig account address. const [multisigPda] = multisig.getMultisigPda({ createKey: createKey.publicKey, }); const programConfigPda = multisig.getProgramConfigPda({})[0]; const programConfig = await multisig.accounts.ProgramConfig.fromAccountAddress( connection, programConfigPda, ); const configTreasury = programConfig.treasury; const sig = await multisig.rpc.multisigCreateV2({ connection, createKey, // must sign creator, // must sign & pays fees multisigPda, threshold: 1, // timeLock: 0, // no timelock configAuthority: null, rentCollector: null, treasury: configTreasury, members: [ {key: creator.publicKey, permissions: Permissions.all()}, // { key: secondMember.publicKey, permissions: Permissions.fromPermissions([Permission.Vote]) }, ], }); const latestBlockhashInfo = await connection.getLatestBlockhash(); await connection.confirmTransaction({ signature: sig, blockhash: latestBlockhashInfo.blockhash, lastValidBlockHeight: latestBlockhashInfo.lastValidBlockHeight, }); console.log(`Multisig account: ${multisigPda.toBase58()}`); console.log(`Multisig creation txn link: https://solscan.io/tx/${sig}?cluster=devnet`); })(); ``` ### Using the created Multisig Account in the Squads Devnet UI In the Squads v4 Devnet UI, on the initial page load you'll be asked to fill up the value for the **Multisig Config Address**. Input the address of the **Multisig Account** you had just created. If the page is not loading, try updating the Settings to use a private RPC URL. Also ensure that the RPC in use is for Solana Devnet and not Mainnet Beta. ## Implementing Time-locks for Solana OFT Mints The Solana OFT Program's mint function cannot be altered without breaking crosschain transfers. If you require the ability to implement time-locks for minting operations, the timelock must be configured via an additional minter and NOT on the program's mint function itself. To implement time-locks for mints: * Specify additional minters when creating the OFT * Configure the timelock on the additional minter authority * Ensure the timelock is NOT applied directly to the program's mint function ### Using Squads Multisig for Time-locked Mints The additional minter can be a Squads Multisig, which you can configure to have a timelock for minting transactions. This approach allows you to implement secure time-delayed minting while preserving crosschain transfer functionality. When configuring a Squads Multisig as an additional minter: 1. Set up the Squads Multisig as described in the previous section 2. Configure the desired timelock duration for the multisig transactions 3. Specify the multisig address as an additional minter when deploying your OFT For more information on configuring time-locks with Squads, refer to the [Squads Time-locks documentation](https://docs.squads.so/main/development/reference/time-locks). # Common Errors Source: https://docs.layerzero.network/v2/developers/solana/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 errors that are commonly faced during deployment of Solana OFTs. ### `signatureSubscribe` error ``` Received JSON-RPC error calling `signatureSubscribe` { args: [ 'VbzmoNsDHw4z2zmCA12xxGX2pNYtxLTxkYSZsYZdTgxUoMR54w4gA2TvFh3pnd1gFzstGDDqAKDxfu3DjD1qPBj', { commitment: 'confirmed' } ], error: { code: -32601, message: 'Subscriptions unsupported for this network' } } ``` Some third-party providers (e.g., Alchemy, Quicknode) may restrict the access to the `signatureSubscribe` method on lower-tier plans. To resolve this error, use public RPCs like [https://api.mainnet-beta.solana.com](https://api.mainnet-beta.solana.com) (or [https://api.devnet.solana.com](https://api.devnet.solana.com) ) or, Solana-dedicated RPC providers such as Helius. ### `DeclaredProgramIdMismatch` ``` AnchorError occurred. Error Code: DeclaredProgramIdMismatch. Error Number: 4100. Error Message: The declared program id does not match the actual program id. ``` This is caused by building the program with the wrong `OFT_ID` value in the OFT Programs `lib.rs`. Ensure you are passing in `OFT_ID` as an environment variable. ``` anchor build -v -e OFT_ID= ``` ### `anchor build -v` fails There are known issues with downloading rust crates in older versions of docker. Please ensure you are using the most up-to-date docker version. The issue manifests similar to: ```bash wrap theme={null} anchor build -v Using image "backpackapp/build:v0.29.0" Run docker image WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested 417a5b38e427cbc75ba2440fedcfb124bbbfe704ab73717382e7d644d8c021b1 Building endpoint manifest: "programs/endpoint-mock/Cargo.toml" info: syncing channel updates for '1.75.0-x86_64-unknown-linux-gnu' info: latest update on 2023-12-28, rust version 1.75.0 (82e1608df 2023-12-21) info: downloading component 'cargo' info: downloading component 'clippy' info: downloading component 'rust-docs' info: downloading component 'rust-std' info: downloading component 'rustc' info: downloading component 'rustfmt' info: installing component 'cargo' info: installing component 'clippy' info: installing component 'rust-docs' info: installing component 'rust-std' info: installing component 'rustc' info: installing component 'rustfmt' Updating crates.io index Cleaning up the docker target directory Removing the docker container anchor-program Error during Docker build: Failed to build program Error: Failed to build program ``` Note: The error occurs after attempting to update crates.io index. ### `The value of "offset" is out of range. It must be >= 0 and <= 32. Received 41` This error may occur when sending tokens from Solana. If you receive this error, it may be caused by an improperly configured executor address in your `layerzero.config.ts` configuration file. The value for this address is not the programId from listed as `LZ Executor` in the [deployed endpoints page](/v2/developers/evm/technical-reference/deployed-contracts). Instead, this address is the Executor Config PDA. It can be derived using the following: ```typescript wrap theme={null} const executorProgramId = '6doghB248px58JSSwG4qejQ46kFMW4AMj7vzJnWZHNZn'; console.log(new ExecutorPDADeriver('executorProgramId').config()); ``` The result is: ```text wrap theme={null} AwrbHeCyniXaQhiJZkLhgWdUCteeWSGaSN1sTfLiY7xK ``` The full error message looks similar to below: ```text wrap theme={null} RangeError [ERR_OUT_OF_RANGE]: The value of "offset" is out of range. It must be >= 0 and <= 32. Received 41 at new NodeError (node:internal/errors:405:5) at boundsError (node:internal/buffer:88:9) at Buffer.readUInt32LE (node:internal/buffer:222:5) at Object.read (/Users/user/go/src/github.com/paxosglobal/solana-programs-internal/paxos-lz-oft/node_modules/@metaplex-foundation/beet/src/beets/numbers.ts:51:16) at Object.toFixedFromData (/Users/user/go/src/github.com/paxosglobal/solana-programs-internal/paxos-lz-oft/node_modules/@metaplex-foundation/beet/src/beets/collections.ts:142:23) at fixBeetFromData (/Users/user/go/src/github.com/paxosglobal/solana-programs-internal/paxos-lz-oft/node_modules/@metaplex-foundation/beet/src/beet.fixable.ts:23:17) at FixableBeetArgsStruct.toFixedFromData (/Users/user/go/src/github.com/paxosglobal/solana-programs-internal/paxos-lz-oft/node_modules/@metaplex-foundation/beet/src/struct.fixable.ts:85:40) at fixBeetFromData (/Users/user/go/src/github.com/paxosglobal/solana-programs-internal/paxos-lz-oft/node_modules/@metaplex-foundation/beet/src/beet.fixable.ts:23:17) at FixableBeetStruct.toFixedFromData (/Users/user/go/src/github.com/paxosglobal/solana-programs-internal/paxos-lz-oft/node_modules/@metaplex-foundation/beet/src/struct.fixable.ts:85:40) at FixableBeetStruct.deserialize (/Users/user/go/src/github.com/paxosglobal/solana-programs-internal/paxos-lz-oft/node_modules/@metaplex-foundation/beet/src/struct.fixable.ts:59:17) { code: 'ERR_OUT_OF_RANGE' ``` ### `Error: Account allocation failed: unable to confirm transaction.` This error can occur while deploying the Solana OFT. The full error message: `Error: Account allocation failed: unable to confirm transaction. This can happen in situations such as transaction expiration and insufficient fee-payer funds` This error is caused by the inability to confirm the transaction in time, or by running out of funds. This is not specific to OFT deployment, but Solana programs in general. Fortunately, you can retry by recovering the program key and re-running with `--buffer` flag similar to the following: ```bash wrap theme={null} solana-keygen recover -o recover.json solana program deploy --buffer recover.json --upgrade-authority --program-id target/verifiable/oft.so -u mainnet-beta ``` ### `Instruction passed to inner instruction is too large (1388 > 1280)` This error can occur when sending tokens from Solana. The outbound OApp DVN configuration violates a hard CPI size restriction, as you have included too many DVNs in the configuration (more than 3 for Solana outbound). As such, you will need to adjust the DVNs to comply with the CPI size restriction. The current CPI size restriction is 1280 bytes. The error message looks similar to the following: ```text wrap theme={null} SendTransactionError: Simulation failed. Message: Transaction simulation failed: Error processing Instruction 0: Program failed to complete. Logs: [ "Program 2gFsaXeN9jngaKbQvZsLwxqfUrT2n4WRMraMpeL8NwZM invoke [1]", "Program log: Instruction: Send", "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", "Program log: Instruction: Burn", "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1143 of 472804 compute units", "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", "Program 2gFsaXeN9jngaKbQvZsLwxqfUrT2n4WRMraMpeL8NwZM consumed 67401 of 500000 compute units", "Program 2gFsaXeN9jngaKbQvZsLwxqfUrT2n4WRMraMpeL8NwZM failed: Instruction passed to inner instruction is too large (1388 > 1280)" ]. ``` [`loosen_cpi_size_restriction`](https://github.com/solana-labs/solana/blob/v1.18.26/programs/bpf_loader/src/syscalls/cpi.rs#L958-L994), which allows more lenient CPI size restrictions, is not yet enabled in the current version of Solana devnet or mainnet. ```text wrap theme={null} solana feature status -u devnet --display-all ``` ### `base64 encoded solana_sdk::transaction::versioned::VersionedTransaction too large: 1728 bytes (max: encoded/raw 1644/1232).` This error can occur when sending tokens from Solana. This error happens when sending for Solana outbound due to the transaction size exceeds the maximum hard limit. To alleviate this issue, consider using an Address Lookup Table (ALT) instruction in your transaction. Example ALTs for mainnet and testnet (devnet): | Stage | Address | | ------------ | ---------------------------------------------- | | mainnet-beta | `AokBxha6VMLLgf97B5VYHEtqztamWmYERBmmFvjuTzJB` | | devnet | `9thqPdbR27A1yLWw2spwJLySemiGMXxPnEvfmXVk4KuK` | More info can be found in the [Solana documentation](https://solana.com/docs/advanced/lookup-tables). # Frequently Asked Questions (FAQ) Source: https://docs.layerzero.network/v2/developers/solana/troubleshooting/faq Common issues and solutions for Frequently Asked Questions (FAQ). Troubleshoot problems and find answers to frequently asked questions. LayerZero enables... The Freeze Authority is managed directly via the regular Solana token's (SPL/Token2022) interface and not through the OFT program or any LayerZero-specific tooling. The default OFT program does not utilize the Freeze Authority and renouncing it will not affect anything given an unmodified OFT program. Note that for Solana OFTs [created](https://github.com/LayerZero-Labs/devtools/tree/main/examples/oft-solana#for-oft) with `--only-oft-store true`, meaning there are no additional minters, then the Freeze Authority has been renounced automatically at the start. It's only if you had specified additional minters, that the Freeze Authority would have been set to the 1 of N SPL multisig which would have the OFT Store and additional minter(s) as signers. To renounce the Freeze Authority, any one of the additional minters can be used, since the SPL Multisig is a 1 of N. If the additional minter address is a regular address, then the CLI can be used to renounce the Freeze Authority. Assuming the local keypair belongs to the additional minter's address, you can run: ``` spl-token authorize freeze --disable ``` If the additional minter address is a Squads multisig, you may utilize the [Token Manager](https://docs.squads.so/main/navigating-your-squad/developers-assets/token-manager#burning-the-freeze-authority-of-a-token) if you are on the Squads Business or Enterprise Plan. Solana has the concept of 'rent' which now actually refers to the amount needed to satisfy the minimum balance required to be [rent-exempt](https://solana.com/docs/references/terminology#rent-exempt). Any account created on Solana requires this 'rent' amount. The majority of the message 'fee' when sending an OFT to Solana is to pay for this 'rent'. This is specified either via [enforced options](/v2/concepts/message-options#enforcing-options) or in [extra options](/v2/concepts/message-options#extra-options) as the message `value`. More specifically, the 'rent' applies to [token accounts](https://solana.com/docs/tokens#token-account) that need to be created when an address receives any token on Solana. The 'rent' amount varies according to the size in bytes of the account that needs to be created. Solana has two token account standards: [SPL](https://www.solana-program.com/docs/token) and [Token-2022](https://www.solana-program.com/docs/token-2022). SPL token accounts have a fixed size of 165 bytes, this results in a required rent amount of `0.00203928 SOL`. For Token-2022, token accounts can vary in size depending on which [token extensions](https://solana.com/developers/guides/token-extensions/getting-started#how-do-i-create-a-token-with-token-extensions) are enabled. Given a crosschain transfer from a chain to Solana (that sets the CU limit (`message.gas`) to `200_000`), the following is a breakdown of how much SOL is needed to execute `lzReceive` on Solana: ``` 0.00203928 SOL (rent) + 0.000015 SOL (base fee) + 0.0002 SOL (priority fee) = 0.00225428 SOL ``` Breakdown: * Base fee = `3 signatures × 5,000 lamports = 15,000 lamports = 0.000015 SOL` * [Priority fee](https://solana.com/developers/guides/advanced/how-to-use-priority-fees#what-are-priority-fees) = `200_000 CU × 1 lamport per CU = 200,000 lamports = 0.0002 SOL` Given the above, the **rent accounts for ≈ 90.43% of the total SOL** needed for an `lzReceive` execution for a Solana OFT. Note that the rent is not needed when the receiving address already has a token account. # DVN and Executor Configuration on Starknet Source: https://docs.layerzero.network/v2/developers/starknet/configuration/dvn-executor-config Configure Decentralized Verifier Networks (DVNs), Executors, and message libraries for LayerZero on Starknet. This guide explains how to configure Decentralized Verifier Networks (DVNs), Executors, and message libraries for your Starknet OApp or OFT using the SDK. **Production deployments should use multiple required DVNs from independent operators.** A single-DVN configuration means a compromise of that one verifier results in unrestricted forged messages on the pathway. See the [Integration Checklist](/v2/tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway) for production DVN guidance. ## Overview Configuration is done through the SDK provided by `@layerzerolabs/lz-v2-protocol-starknet`. All examples use SDK methods to interact with the Endpoint and message library contracts. **Configuration flow**: 1. Set delegate (required if configuring via external account) 2. Set message libraries (optional) 3. Configure DVNs for send/receive (optional but recommended) 4. Set enforced options (optional) 5. Set peer addresses (required - opens pathway, call last!) **Critical Order** Always configure in this order. Setting peers last ensures the pathway isn't opened until all security settings are in place. ## SDK Setup ```typescript wrap theme={null} import {RpcProvider, Account} from 'starknet'; import { getEndpointV2Contract, getOAppContract, getUltraLightNodeContractWithAddress, encodeUlnConfig, encodeExecutorConfig, MessageLibConfigType, } from '@layerzerolabs/lz-v2-protocol-starknet'; import {EndpointId, ChainName, Environment} from '@layerzerolabs/lz-definitions'; // Setup provider and account const provider = new RpcProvider({nodeUrl: 'YOUR_RPC_URL'}); const account = new Account({provider, address: accountAddress, signer: privateKey}); // Get contract instances const oappContract = await getOAppContract(oappAddress, provider); const endpointContract = await getEndpointV2Contract( ChainName.STARKNET, Environment.TESTNET, provider, ); // If you already have the Endpoint address, you can use: // const endpointContract = await getEndpointV2ContractWithAddress(ENDPOINT_ADDRESS, provider); // This is recommended when running outside the monorepo. ``` ## Prerequisite: Set Delegate Endpoint configuration calls (`set_send_library`, `set_receive_library`, `set_send_configs`, `set_receive_configs`) require the caller to be the OApp itself or an authorized delegate. If you're configuring from an external account, set a delegate first (owner-only): ```typescript wrap theme={null} const setDelegateCall = oappContract.populateTransaction.set_delegate(accountAddress); await account.execute([setDelegateCall]); ``` Use the address of the account that will submit the endpoint configuration transactions. ## Default Configuration LayerZero provides sensible defaults. If you don't configure custom settings: | Setting | Default | | --------------- | ----------------------- | | DVN | LayerZero Labs DVN | | Executor | LayerZero Labs Executor | | Send Library | ULN302 | | Receive Library | ULN302 | You can query the default configuration via the Endpoint or check [LayerZero Deployments](/v2/deployments/deployed-contracts). *** ## Configuration Methods ### Set Peer ```typescript wrap theme={null} import {getOAppContract} from '@layerzerolabs/lz-v2-protocol-starknet'; import {EndpointId} from '@layerzerolabs/lz-definitions'; const oapp = await getOAppContract(oappAddress, provider); // Peer address as Bytes32 (left-padded for EVM addresses) const peerBytes32 = {value: BigInt('0x000000000000000000000000' + evmAddress.slice(2))}; const call = oapp.populateTransaction.set_peer( EndpointId.ETHEREUM_V2_MAINNET, // Remote chain endpoint ID peerBytes32, ); await account.execute([call]); ``` **Address format**: Use Bytes32 for all peers. EVM addresses (20 bytes) must be left-padded with zeros to 32 bytes. ### Set Message Libraries ```typescript wrap theme={null} import {getEndpointV2Contract} from '@layerzerolabs/lz-v2-protocol-starknet'; import {EndpointId, ChainName, Environment} from '@layerzerolabs/lz-definitions'; const endpoint = await getEndpointV2Contract(ChainName.STARKNET, Environment.TESTNET, provider); // Set custom send library const setSendLibCall = endpoint.populateTransaction.set_send_library( oappAddress, EndpointId.ETHEREUM_V2_MAINNET, messageLibAddress, ); // Set custom receive library (with grace period) const setReceiveLibCall = endpoint.populateTransaction.set_receive_library( oappAddress, EndpointId.ETHEREUM_V2_MAINNET, messageLibAddress, 0, // Grace period in blocks (0 = immediate) ); await account.execute([setSendLibCall, setReceiveLibCall]); ``` **Default**: Uses Endpoint defaults if not configured. If you see `DEFAULT_SEND_LIB_UNAVAILABLE` or `UNSUPPORTED_EID`, set the send/receive libraries explicitly and ensure the EID is supported by the ULN. ```typescript wrap theme={null} import {getUltraLightNodeContractWithAddress} from '@layerzerolabs/lz-v2-protocol-starknet'; const uln = await getUltraLightNodeContractWithAddress(ulnAddress, provider); const canSend = await uln.is_supported_send_eid(remoteEid); const canReceive = await uln.is_supported_receive_eid(remoteEid); ``` *** ## DVN Configuration ### Configure Send DVN (Outbound) ```typescript wrap theme={null} import { getEndpointV2Contract, encodeUlnConfig, MessageLibConfigType, } from '@layerzerolabs/lz-v2-protocol-starknet'; import {EndpointId, ChainName, Environment} from '@layerzerolabs/lz-definitions'; const endpoint = await getEndpointV2Contract(ChainName.STARKNET, Environment.TESTNET, provider); const remoteEid = EndpointId.ETHEREUM_V2_MAINNET; // Get the current send library address const sendLibResponse = await endpoint.get_send_library(oappAddress, remoteEid); const sendLibAddress = sendLibResponse.lib; // Encode ULN configuration const ulnConfig = encodeUlnConfig({ confirmations: 15, has_confirmations: true, required_dvns: [LAYERZERO_DVN_ADDRESS, PARTNER_DVN_ADDRESS], // Must be sorted ascending has_required_dvns: true, optional_dvns: [], optional_dvn_threshold: 0, has_optional_dvns: false, }); // Set send config const call = endpoint.populateTransaction.set_send_configs(oappAddress, sendLibAddress, [ { eid: remoteEid, config_type: MessageLibConfigType.ULN, // 2 config: ulnConfig, }, ]); await account.execute([call]); ``` ### Configure Receive DVN (Inbound) ```typescript wrap theme={null} // Get the current receive library address const receiveLibResponse = await endpoint.get_receive_library(oappAddress, remoteEid); const receiveLibAddress = receiveLibResponse.lib; // Encode ULN configuration for receive const ulnConfig = encodeUlnConfig({ confirmations: 15, has_confirmations: true, required_dvns: [LAYERZERO_DVN_ADDRESS], has_required_dvns: true, optional_dvns: [], optional_dvn_threshold: 0, has_optional_dvns: false, }); // Set receive config const call = endpoint.populateTransaction.set_receive_configs(oappAddress, receiveLibAddress, [ { eid: remoteEid, config_type: MessageLibConfigType.ULN, // 2 config: ulnConfig, }, ]); await account.execute([call]); ``` ### ULN Configuration Structure | Field | Type | Description | | ------------------------ | ---------- | -------------------------------------------------- | | `confirmations` | `number` | Block confirmations required before verification | | `has_confirmations` | `boolean` | Set `true` to use custom value | | `required_dvns` | `string[]` | DVN addresses (all must verify) - sorted ascending | | `has_required_dvns` | `boolean` | Set `true` to use custom DVNs | | `optional_dvns` | `string[]` | Optional DVN addresses - sorted ascending | | `optional_dvn_threshold` | `number` | How many optional DVNs must verify | | `has_optional_dvns` | `boolean` | Set `true` to use custom optional DVNs | **has\_* Fields*\* The `has_*` fields indicate whether the corresponding value should override the default configuration. Set them to `true` when you want to use custom values, otherwise the protocol defaults will be used. **DVN Ordering** DVN addresses in `required_dvns` and `optional_dvns` **must be sorted in ascending order**. The contract will revert if unsorted or duplicate DVNs are provided. **Config types**: `MessageLibConfigType.EXECUTOR` = 1, `MessageLibConfigType.ULN` = 2 *** ## Configuring Executor The Executor delivers messages on the destination chain. ```typescript wrap theme={null} import { getEndpointV2Contract, encodeExecutorConfig, MessageLibConfigType, } from '@layerzerolabs/lz-v2-protocol-starknet'; import {ChainName, Environment} from '@layerzerolabs/lz-definitions'; const endpoint = await getEndpointV2Contract(ChainName.STARKNET, Environment.TESTNET, provider); // Encode executor configuration const executorConfig = encodeExecutorConfig({ max_message_size: 10000, executor: EXECUTOR_ADDRESS, }); // Set executor config (only for send direction) const call = endpoint.populateTransaction.set_send_configs(oappAddress, sendLibAddress, [ { eid: remoteEid, config_type: MessageLibConfigType.EXECUTOR, // 1 config: executorConfig, }, ]); await account.execute([call]); ``` ### Executor Config Structure | Field | Type | Description | | ------------------ | -------- | ----------------------------- | | `max_message_size` | `number` | Maximum message size in bytes | | `executor` | `string` | Executor contract address | *** ## Setting Enforced Options Enforced options set **minimum** execution parameters that users cannot override. This requires the OAppOptionsType3 component (included in OFT contracts). If your ABI doesn't expose `set_enforced_options`, load your compiled contract artifact and call the entrypoint directly. ```typescript wrap theme={null} import {Contract} from 'starknet'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import compiledArtifact from './path/to/contract_class.json'; // Use your compiled ABI for OFT/OAppOptionsType3 since getOAppContract // only exposes the base OApp interface. const oappAbi = compiledArtifact.abi; // Load from target/dev/*.contract_class.json const oapp = new Contract({abi: oappAbi, address: oappAddress, provider}).typedv2(oappAbi); // Build options with minimum gas requirements const options = Options.newOptions() .addExecutorLzReceiveOption(200000, 0) // 200k gas for lz_receive .toBytes(); // Set enforced options for SEND message type (1) const call = oapp.populateTransaction.set_enforced_options([ { eid: remoteEid, msg_type: 1, options, }, ]); await account.execute([call]); ``` If you prefer `sncast`, call the entrypoint directly: ```bash theme={null} # lzReceive gas = 120000, value = 0 (no compose) sncast invoke \ --contract-address \ --function set_enforced_options \ --network sepolia \ --arguments 'array![layerzero::oapps::common::oapp_options_type_3::structs::EnforcedOptionParam { eid: , msg_type: 1, options: core::byte_array::ByteArray { data: array![], pending_word: 0x0003010011010000000000000000000000000001d4c0, pending_word_len: 22 } }]' ``` ### Message Types | Type | Value | Description | | ------ | ----- | --------------------- | | `SEND` | 1 | Standard OFT transfer | *** ## Complete Configuration Example ```typescript wrap theme={null} import {RpcProvider, Account, Contract} from 'starknet'; import { getEndpointV2Contract, getOAppContract, encodeUlnConfig, encodeExecutorConfig, MessageLibConfigType, } from '@layerzerolabs/lz-v2-protocol-starknet'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {EndpointId, ChainName, Environment} from '@layerzerolabs/lz-definitions'; import compiledArtifact from './path/to/contract_class.json'; async function configureOApp() { const provider = new RpcProvider({nodeUrl: RPC_URL}); const account = new Account({provider, address: ACCOUNT_ADDRESS, signer: PRIVATE_KEY}); const endpoint = await getEndpointV2Contract(ChainName.STARKNET, Environment.TESTNET, provider); const oapp = await getOAppContract(OFT_ADDRESS, provider); const oappOptions = new Contract({ abi: compiledArtifact.abi, address: OFT_ADDRESS, provider, }).typedv2(compiledArtifact.abi); const remoteEid = EndpointId.ETHEREUM_V2_MAINNET; const ULN_ADDRESS = '0x...'; // ULN302 send library address // Authorize the account to configure endpoint settings (owner-only) const setDelegateCall = oapp.populateTransaction.set_delegate(ACCOUNT_ADDRESS); // Set libraries (required if defaults aren't configured for the EID) const setSendLibCall = endpoint.populateTransaction.set_send_library( OFT_ADDRESS, remoteEid, ULN_ADDRESS, ); const setReceiveLibCall = endpoint.populateTransaction.set_receive_library( OFT_ADDRESS, remoteEid, ULN_ADDRESS, 0, ); // 1. Configure send DVN + Executor const sendConfigCall = endpoint.populateTransaction.set_send_configs(OFT_ADDRESS, ULN_ADDRESS, [ { eid: remoteEid, config_type: MessageLibConfigType.ULN, config: encodeUlnConfig({ confirmations: 15, has_confirmations: true, required_dvns: [LAYERZERO_DVN, PARTNER_DVN], has_required_dvns: true, optional_dvns: [], optional_dvn_threshold: 0, has_optional_dvns: false, }), }, { eid: remoteEid, config_type: MessageLibConfigType.EXECUTOR, config: encodeExecutorConfig({ max_message_size: 10000, executor: EXECUTOR_ADDRESS, }), }, ]); // 2. Configure receive DVN const receiveConfigCall = endpoint.populateTransaction.set_receive_configs( OFT_ADDRESS, ULN_ADDRESS, [ { eid: remoteEid, config_type: MessageLibConfigType.ULN, config: encodeUlnConfig({ confirmations: 15, has_confirmations: true, required_dvns: [LAYERZERO_DVN], has_required_dvns: true, optional_dvns: [], optional_dvn_threshold: 0, has_optional_dvns: false, }), }, ], ); // 3. Set enforced options const enforcedOptionsCall = oappOptions.populateTransaction.set_enforced_options([ { eid: remoteEid, msg_type: 1, // SEND message type options: Options.newOptions().addExecutorLzReceiveOption(200000, 0).toBytes(), }, ]); // 4. Set peer (LAST!) const setPeerCall = oapp.populateTransaction.set_peer(remoteEid, { value: BigInt('0x000000000000000000000000' + EVM_OFT_ADDRESS.slice(2)), }); // Execute all in atomic transaction await account.execute([ setDelegateCall, setSendLibCall, setReceiveLibCall, sendConfigCall, receiveConfigCall, enforcedOptionsCall, setPeerCall, ]); console.log('Configuration complete!'); } ``` *** ## Reading Configuration ### Get Current Send Config ```typescript wrap theme={null} import {getUltraLightNodeContractWithAddress} from '@layerzerolabs/lz-v2-protocol-starknet'; const ulnContract = await getUltraLightNodeContractWithAddress(sendLibAddress, provider); // Get executor config const executorConfig = await ulnContract.get_raw_oapp_executor_config(oappAddress, remoteEid); console.log('Max message size:', executorConfig.max_message_size); console.log('Executor:', executorConfig.executor); // Get ULN config const ulnConfig = await ulnContract.get_raw_oapp_uln_send_config(oappAddress, remoteEid); console.log('Confirmations:', ulnConfig.confirmations); console.log('Required DVNs:', ulnConfig.required_dvns); ``` ### Get Current Receive Config ```typescript wrap theme={null} const ulnConfig = await ulnContract.get_raw_oapp_uln_receive_config(oappAddress, remoteEid); console.log('Confirmations:', ulnConfig.confirmations); console.log('Required DVNs:', ulnConfig.required_dvns); ``` ### Get Peer ```typescript wrap theme={null} const oapp = await getOAppContract(oappAddress, provider); const peer = await oapp.get_peer(remoteEid); console.log('Peer:', peer.value.toString(16)); ``` *** ## Gas Recommendations | Operation | Recommended Gas | Notes | | ------------------- | --------------- | ---------------------- | | `lz_receive` (OApp) | 200,000 | Basic message handling | | `lz_receive` (OFT) | 200,000 | Token credit operation | Always test your specific use case on testnet to determine accurate gas requirements. *** ## Next Steps * [Protocol Overview](/v2/developers/starknet/protocol-overview) - Message lifecycle * [Technical Reference](/v2/developers/starknet/technical-reference/starknet-guidance) - Deployment guide * [Troubleshooting](/v2/developers/starknet/troubleshooting/common-errors) - Configuration errors # Getting Started with LayerZero V2 on Starknet Source: https://docs.layerzero.network/v2/developers/starknet/getting-started Learn how to build crosschain applications on Starknet with LayerZero V2. Covers Cairo fundamentals, account abstraction, and development setup. Any data, whether it's a fungible token transfer, an NFT, or some other smart contract input can be encoded on-chain as bytes and delivered to a destination chain to trigger some action using LayerZero. Because of this, any blockchain that broadly supports state propagation and events can be connected to LayerZero, including **Starknet**. If you're new to LayerZero, we recommend reviewing [**"What is LayerZero?"**](/v2/concepts/getting-started/what-is-layerzero) before continuing.
LayerZero provides **Starknet Cairo Contracts** that can communicate with the equivalent [Solidity Contract Libraries](/v2/developers/evm/overview) and [Solana Programs](/v2/developers/solana/overview) deployed on other chains. These contracts, like their Solidity and Rust counterparts, simplify calling the [LayerZero Endpoint](/v2/concepts/protocol/layerzero-endpoint), provide message handling, interfaces for protocol configurations, and other utilities for interoperability: * **Omnichain Fungible Token (OFT)**: extends OApp with functionality for handling omnichain token transfers using Starknet's ERC20 standard. * **Omnichain Application (OApp)**: the base contract utilities for omnichain messaging and configuration. Each of these contract standards implements common functions for **sending** and **receiving** omnichain messages. ## Differences from the Ethereum Virtual Machine The full differences between Solidity/EVM and Cairo/Starknet are significant. For comprehensive guides, see: * [Starknet Documentation](https://docs.starknet.io/) * [The Cairo Book](https://book.cairo-lang.org/) * [Cairo by Example](https://cairo-by-example.com/) Skip this section if you already feel comfortable working with Starknet and its account abstraction model. ### Comparison Table | Aspect | EVM | Starknet | | ------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------ | | **State Model** | Storage slots | Felt-based contract storage | | **Language** | Solidity | Cairo | | **Authorization** | `msg.sender` | `get_caller_address()` | | **Cross-Contract** | External calls | [Dispatcher pattern](https://book.cairo-lang.org/ch102-02-interacting-with-another-contract.html#the-dispatcher-pattern) | | **OApp Identity** | Contract address | Contract address | | **Balance Type** | `uint256` | `u256` (struct: two `u128`) | | **Fee Model** | Gas (ETH) | Resource bounds (STRK/ETH) | | **Account Model** | EOA + Contracts | Account Abstraction (all accounts are contracts) | | **Deployment** | Single transaction | Declare + Deploy (two steps) | ### Account Abstraction (No EOAs) The most fundamental difference is Starknet's **native account abstraction**: **EVM**: ```solidity wrap theme={null} // EOA (Externally Owned Account) signs and sends transactions directly // msg.sender is the account that signed the transaction function transfer() external { require(msg.sender == owner, "not owner"); } ``` **Starknet**: ```rust wrap theme={null} // All accounts are smart contracts // Signatures are produced off-chain and validated by the account contract // get_caller_address() returns the account contract address fn transfer(ref self: ContractState) { let caller = get_caller_address(); assert(caller == self.owner.read(), 'not owner'); } ``` **Key Implications**: * **Before deploying any contract**, you must have a deployed and funded account contract (wallet) * Signatures are produced off-chain by the account owner and validated by the account contract * Account contracts handle fee payment and transaction execution for that account * Common account implementations include Ready Wallet (formerly Argent), Braavos, and OpenZeppelin Account ### Declare Then Deploy Lifecycle Unlike EVM where you deploy bytecode in a single transaction, Starknet separates code publication from instantiation: ```mermaid theme={null} flowchart LR A[Build Cairo] --> B[Declare Class] B --> C[Get class_hash] C --> D[Deploy Instance] D --> E[Contract Address] ``` **Step 1: Declare** - Publish your contract code to the network: ```bash theme={null} sncast declare --contract-name MyOFT # Returns: class_hash = 0x123abc... ``` **Step 2: Deploy** - Create an instance of the declared class: ```bash theme={null} sncast deploy --class-hash 0x123abc... --network sepolia --arguments '' # Returns: contract_address = 0x456def... ``` **Key Concepts**: * **class\_hash**: Unique identifier for your contract's code (like a "template") * **contract\_address**: Specific instance of that code with its own state * **Multiple contracts** can share the same class\_hash (reusable code) ### Dispatcher Pattern vs Inheritance Starknet uses the [dispatcher pattern](https://book.cairo-lang.org/ch102-02-interacting-with-another-contract.html#the-dispatcher-pattern) instead of Solidity's inheritance model. **EVM uses inheritance** for contract composition: ```solidity wrap theme={null} // Solidity: Inherit and override contract MyOFT is OFT { constructor() OFT("Token", "TKN", endpoint, owner) {} } ``` **Starknet uses components and dispatchers**: ```rust wrap theme={null} // Cairo: Compose with components #[starknet::contract] mod MyOFT { // Import components use layerzero::oapps::oapp::oapp_core::OAppCoreComponent; use openzeppelin::token::erc20::ERC20Component; // Declare components component!(path: OAppCoreComponent, storage: oapp_core, event: OAppCoreEvent); component!(path: ERC20Component, storage: erc20, event: ERC20Event); // Embed implementations #[abi(embed_v0)] impl OAppCoreImpl = OAppCoreComponent::OAppCoreImpl; } ``` **Cross-contract calls use dispatchers**: ```rust wrap theme={null} // Cairo: Typed dispatcher for cross-contract calls use layerzero::endpoint::interfaces::endpoint_v2::{IEndpointV2Dispatcher, IEndpointV2DispatcherTrait}; fn call_endpoint(endpoint_address: ContractAddress) { let endpoint = IEndpointV2Dispatcher { contract_address: endpoint_address }; let fee = endpoint.quote(params, sender); // Type-safe call } ``` ### Constructor Caller Footgun When deploying via the Universal Deployer Contract (UDC), `get_caller_address()` in the constructor returns the **UDC address**, not your account address! **Problem**: ```rust wrap theme={null} #[constructor] fn constructor(ref self: ContractState, endpoint: ContractAddress) { // BUG: If deployed via UDC, this sets UDC as owner! let caller = get_caller_address(); self.ownable.initializer(caller); } ``` **Solution** - Always pass the owner explicitly: ```rust wrap theme={null} #[constructor] fn constructor( ref self: ContractState, endpoint: ContractAddress, owner: ContractAddress, // Explicitly pass the intended owner ) { self.ownable.initializer(owner); } ``` ### Cairo Integer Types Cairo 1.0 provides native unsigned integer types. For **token amounts** (ERC20 balances, transfers, allowances), always use `u256`—matching Solidity's `uint256` for crosschain compatibility. | Type | Size | Use Case | | ------ | ------- | --------------------------------------------- | | `u64` | 64-bit | Timestamps, small counters | | `u128` | 128-bit | Medium-sized values | | `u256` | 256-bit | **Token amounts** (ERC20 balances, transfers) | OpenZeppelin's Cairo ERC20 interface uses `u256` for `balance_of`, `total_supply`, `allowance`, and all transfer/approve amounts. ```rust wrap theme={null} // u256 for token amounts (matches Solidity uint256) let amount: u256 = 1000000000000000000_u256; // 1 token (18 decimals) fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { self.erc20.balance_of(account) } ``` You'll also encounter `felt252` in Cairo—it's Starknet's base field element type used internally (e.g., `ContractAddress` wraps a `felt252`). However, don't use it for token amounts; its modular arithmetic can cause unexpected behavior. **Crosschain encoding**: LayerZero messages encode addresses as `Bytes32` for compatibility across chains with different address sizes. ### Resource Bounds (Gas Model) Starknet uses **resource bounds** instead of simple gas limits: ```rust wrap theme={null} // INVOKE v3 transactions include resource bounds: // - l1_gas: Max L1 gas willing to pay // - l2_gas: Max L2 gas (compute) willing to pay // - l1_data_gas: Max L1 data availability gas ``` **Common error**: "Insufficient max fee" - increase your resource bounds (or fee cap in tooling): ```bash theme={null} sncast deploy --class-hash 0x... ``` ## Prerequisites Before you start building, you'll need to set up your development environment. ### 1. Deploy and Fund an Account Contract Unlike EVM, you need an account contract before you can deploy other contracts: ##### Create a new account (generates keys and computes address) ```bash theme={null} sncast account create \ --name my_account \ --type oz \ --url ``` ##### Fund the computed address with STRK/ETH before deploying. The `account create` command outputs the computed address. Fund this address using a [faucet](https://starknet-faucet.vercel.app/) before running `account deploy`. ##### Deploy the account contract ```bash theme={null} sncast account deploy \ --name my_account \ --url ``` ### 2. Account File Location Accounts are stored in `~/.starknet_accounts/starknet_open_zeppelin_accounts.json`: ```json theme={null} { "alpha-sepolia": { "my_account": { "address": "0x...", "deployed": true, "legacy": false, "private_key": "0x...", "public_key": "0x...", "salt": "0x..." } } } ``` ### 3. Install Scarb (Cairo Build Tool) [Scarb](https://docs.swmansion.com/scarb/) is the official Cairo package manager and build tool: ```bash theme={null} # Install via asdf asdf plugin add scarb asdf install scarb 2.14.0 asdf set scarb 2.14.0 ``` Verify installation: ```bash theme={null} scarb --version # scarb 2.14.0 or later ``` ### 4. Install Starknet Foundry (sncast + snforge) [Starknet Foundry](https://foundry-rs.github.io/starknet-foundry/) provides `sncast` (deployment) and `snforge` (testing): ```bash theme={null} # Install via asdf asdf plugin add starknet-foundry asdf install starknet-foundry 0.53.0 asdf set starknet-foundry 0.53.0 ``` Verify installation: ```bash theme={null} sncast --version snforge --version ``` **RPC Version Compatibility** sncast requires a compatible RPC version: * **Starknet Foundry 0.53.0+** expects RPC v0.9.0 or v0.10.0 * **Starknet Foundry 0.49.0** expects RPC v0.9.0 If you see `RPC node uses incompatible version` warnings, update your RPC URL to use a compatible version: ``` # For v0.9.0 (Alchemy) https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_9/ # For v0.10.0 (Alchemy) https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/ ``` ### 5. Configure snfoundry.toml Create a `snfoundry.toml` in your project root to configure `sncast` defaults: ```toml wrap theme={null} [sncast.default] account = "my_account" url = "https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_9/" wait-params = { timeout = 300, retry-interval = 10 } block-explorer = "StarkScan" show-explorer-links = true ``` | Field | Description | | --------------------- | --------------------------------------------------------------------- | | `account` | Account name from your accounts file | | `url` | RPC endpoint URL (must match sncast version; see RPC note above) | | `wait-params` | Transaction wait timeout and retry settings | | `block-explorer` | Explorer for transaction links (`StarkScan`, `Blockchain`, `Voyager`) | | `show-explorer-links` | Show explorer links after transactions | Use `sncast account list` to see available account names. ### 6. Install Node.js For any TypeScript tooling or SDK usage: ```bash theme={null} # Using nvm (recommended) nvm install 20 nvm use 20 # Verify node --version # v20.x.x ``` ### 7. Get Testnet STRK/ETH For testing on Starknet Sepolia testnet: * [Starknet Faucet](https://starknet-faucet.vercel.app/) - Get testnet STRK * [Starkgate Bridge](https://sepolia.starkgate.starknet.io/) - Bridge ETH from Ethereum Sepolia ## Project Structure A typical LayerZero Starknet project structure: ``` my-oft-project/ ├── Scarb.toml # Package manifest (dependencies) ├── snfoundry.toml # Starknet Foundry config ├── src/ │ ├── lib.cairo # Module declarations │ └── my_oft.cairo # Your OFT contract └── tests/ └── test_my_oft.cairo # Contract tests ``` **Example `Scarb.toml`**: ```toml wrap theme={null} [package] name = "my_oft" version = "0.1.0" edition = "2024_07" [dependencies] starknet = "2.14.0" openzeppelin = "2.0.0" lz_utils = { path = "./node_modules/@layerzerolabs/protocol-starknet-v2/libs/lz_utils" } layerzero = { path = "./node_modules/@layerzerolabs/protocol-starknet-v2/layerzero" } [dev-dependencies] snforge_std = "0.53.0" [[target.starknet-contract]] sierra = true casm = true ``` **Installing LayerZero Packages** Before building, install the LayerZero Cairo contracts: ```bash theme={null} npm init -y npm install @layerzerolabs/protocol-starknet-v2 ``` ## Network Configuration | Network | Endpoint ID | Chain ID | | ---------------- | ----------- | ------------ | | Starknet Mainnet | `30500` | `SN_MAIN` | | Starknet Sepolia | `40500` | `SN_SEPOLIA` | > `SN_MAIN` and `SN_SEPOLIA` are [Starknet's native chain-id identifiers](https://docs.starknet.io/chain-info/), defined by the Starknet protocol to identify the target network for transactions. **RPC Endpoints**: * Mainnet: `` * Sepolia: `` **RPC providers** Blast public endpoints are deprecated; use Alchemy, Infura, or another provider that supports Starknet JSON-RPC v0.9+. ## Next Steps Choose your path: ### Build an OApp For custom crosschain logic: * [OApp Overview](/v2/developers/starknet/oapp/overview) - Architecture and patterns * [Protocol Overview](/v2/developers/starknet/protocol-overview) - Deep technical dive * [Technical Overview](/v2/developers/starknet/technical-overview) - Starknet fundamentals ### Build an OFT For crosschain tokens: * [OFT Overview](/v2/developers/starknet/oft/overview) - Token architecture * [Configuration Guide](/v2/developers/starknet/configuration/dvn-executor-config) - Security and DVN setup ### Understand the Protocol For protocol-level understanding: * [Technical Overview](/v2/developers/starknet/technical-overview) - Cairo architecture and patterns * [Protocol Overview](/v2/developers/starknet/protocol-overview) - Complete message workflows ### Get Help * [Troubleshooting](/v2/developers/starknet/troubleshooting/common-errors) - Common issues * [FAQ](/v2/developers/starknet/troubleshooting/faq) - Frequently asked questions * [Discord](https://discord.com/invite/ktbvm8Nkcr) - Community support # LayerZero V2 OApp on Starknet Source: https://docs.layerzero.network/v2/developers/starknet/oapp/overview Build crosschain applications on Starknet with LayerZero V2 OApp standard. Learn component integration, message sending and receiving. The **Omnichain Application (OApp)** standard provides the foundational building blocks for crosschain messaging on Starknet. OApps can send arbitrary data to any supported chain and receive messages from other chains. ## What is an OApp on Starknet? An OApp on Starknet is a Cairo contract that: 1. **Integrates with LayerZero** via the OAppCoreComponent 2. **Sends messages** through the Endpoint's `send` function 3. **Receives messages** by implementing the `OAppHooks` trait 4. **Manages peers** (trusted remote OApps on other chains) ## Differences from EVM OApps | Aspect | EVM (Solidity) | Starknet (Cairo) | | --------------- | ---------------------------- | --------------------------------- | | Base Contract | `OApp` inheritance | `OAppCoreComponent` composition | | Send Message | `_lzSend()` | `_lz_send()` via OAppSenderImpl | | Receive Message | `_lzReceive()` override | `_lz_receive` via OAppHooks trait | | Peer Storage | `mapping(uint32 => bytes32)` | `Map` | | Authorization | `onlyOwner` modifier | `assert_only_owner()` | | Options Builder | `OptionsBuilder` library | ByteArray encoding | ## Installation ### Step 1: Install LayerZero Cairo Contracts ```bash theme={null} # Create your project directory mkdir my-oapp-project && cd my-oapp-project # Initialize npm and install LayerZero Starknet packages npm init -y npm install @layerzerolabs/protocol-starknet-v2 ``` ### Step 2: Configure Scarb.toml ```toml wrap theme={null} [package] name = "my_oapp" version = "0.1.0" edition = "2024_07" [dependencies] starknet = "2.14.0" openzeppelin = "2.0.0" lz_utils = { path = "./node_modules/@layerzerolabs/protocol-starknet-v2/libs/lz_utils" } layerzero = { path = "./node_modules/@layerzerolabs/protocol-starknet-v2/layerzero" } [dev-dependencies] snforge_std = "0.53.0" [[target.starknet-contract]] sierra = true casm = true ``` **Tool versions** Use Scarb 2.14.0 and Starknet Foundry 0.53.0. Mismatched versions can cause class hash mismatch errors during `sncast declare`. ### Step 3: Create Project Structure ```bash theme={null} mkdir -p src tests echo 'pub mod my_oapp;' > src/lib.cairo ``` ``` my-oapp/ ├── package.json ├── node_modules/@layerzerolabs/protocol-starknet-v2/ ├── Scarb.toml ├── src/ │ ├── lib.cairo │ └── my_oapp.cairo └── tests/ └── test_my_oapp.cairo ``` ### lib.cairo ```rust wrap theme={null} pub mod my_oapp; ``` *** ### Step 4: Configure snfoundry.toml Create a `snfoundry.toml` with your account name and RPC URL. See [Starknet Guidance](/v2/developers/starknet/technical-reference/starknet-guidance) for the full configuration reference and RPC version compatibility notes. ## Deployment ### Step 1: Build ```bash theme={null} scarb build ``` Build artifacts are generated in `target/dev/` by default. ### Step 2: Declare ```bash theme={null} sncast --account declare \ --contract-name MyOApp \ --url # Returns: class_hash = ``` ### Step 3: Deploy ```bash theme={null} sncast --account deploy \ --class-hash \ --url \ --arguments ', , ' # Constructor parameters: # - endpoint: LayerZero Endpoint address # - owner: Contract owner address # - native_token: Fee payment token (STRK address) ``` **Network flag** If you set `url` in `snfoundry.toml`, omit `--network` (sncast will reject it). **Using --arguments** The `--arguments` flag allows passing constructor arguments in a human-readable format. sncast automatically serializes them based on the contract's ABI. For more details, see [Calldata Transformation](https://foundry-rs.github.io/starknet-foundry/starknet/calldata-transformation.html). ### Step 4: Verify ```bash theme={null} sncast verify \ --class-hash \ --contract-name MyOApp \ --verifier voyager \ --network sepolia \ --confirm-verification ``` For more verification options, see the [Starknet Foundry verification guide](https://foundry-rs.github.io/starknet-foundry/starknet/verify.html). ### Step 5: Configure ```bash theme={null} # Set peer for destination chain (e.g., Ethereum Mainnet eid=30101) # For EVM address 0x1234567890abcdef1234567890abcdef12345678: # - Pad to 32 bytes: 0x0000000000000000000000001234567890abcdef1234567890abcdef12345678 # - Split into u256 (low, high): low=0x90abcdef1234567890abcdef12345678, high=0x12345678 sncast --account invoke \ --contract-address \ --function set_peer \ --url \ --calldata 0x7595 0x90abcdef1234567890abcdef12345678 0x12345678 ``` **Bytes32 Encoding** Peer addresses are stored as `Bytes32` (a struct containing a `u256`). For EVM addresses (20 bytes), left-pad with zeros to 32 bytes. **Calldata format for `set_peer(eid: u32, peer: Bytes32)`:** 1. `eid` - endpoint ID as hex (e.g., `0x7595` = 30101 for Ethereum Mainnet) 2. `peer.value.low` - lower 128 bits of the padded address 3. `peer.value.high` - upper 128 bits of the padded address Use `--calldata` with space-separated hex values (not `--arguments`) for complex types like `Bytes32`. *** ## Working Example: Minimal OApp A minimal OApp on Starknet: ```rust wrap theme={null} #[starknet::contract] pub mod MyOApp { use layerzero::oapps::oapp::oapp_core::OAppCoreComponent; use layerzero::common::structs::packet::Origin; use lz_utils::bytes::Bytes32; use openzeppelin::access::ownable::OwnableComponent; use starknet::ContractAddress; // Declare components component!(path: OAppCoreComponent, storage: oapp_core, event: OAppCoreEvent); component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); // Embed OAppCore implementation (exposes external functions) #[abi(embed_v0)] impl OAppCoreImpl = OAppCoreComponent::OAppCoreImpl; #[abi(embed_v0)] impl ILayerZeroReceiverImpl = OAppCoreComponent::LayerZeroReceiverImpl; #[abi(embed_v0)] impl IOAppReceiverImpl = OAppCoreComponent::OAppReceiverImpl; impl OAppCoreInternalImpl = OAppCoreComponent::InternalImpl; // Embed Ownable implementation #[abi(embed_v0)] impl OwnableImpl = OwnableComponent::OwnableImpl; impl OwnableInternalImpl = OwnableComponent::InternalImpl; #[storage] struct Storage { #[substorage(v0)] oapp_core: OAppCoreComponent::Storage, #[substorage(v0)] ownable: OwnableComponent::Storage, // Your custom storage here data: felt252, } #[event] #[derive(Drop, starknet::Event)] pub enum Event { #[flat] OAppCoreEvent: OAppCoreComponent::Event, #[flat] OwnableEvent: OwnableComponent::Event, } #[constructor] fn constructor( ref self: ContractState, endpoint: ContractAddress, owner: ContractAddress, native_token: ContractAddress, ) { // Initialize OAppCore with endpoint, owner (delegate), and native token self.oapp_core.initializer(endpoint, owner, native_token); self.ownable.initializer(owner); } // Implement OAppHooks to handle incoming messages impl OAppHooks of OAppCoreComponent::OAppHooks { fn _lz_receive( ref self: OAppCoreComponent::ComponentState, origin: Origin, guid: Bytes32, message: ByteArray, executor: ContractAddress, extra_data: ByteArray, value: u256, ) { // Your receive logic here // Access contract state via get_contract_mut! } } } ``` ## Required Components ### OAppCoreComponent The core LayerZero integration: ```rust wrap theme={null} // Storage fields pub struct Storage { pub OAppCore_endpoint: ContractAddress, // LayerZero Endpoint pub OAppCore_native_token: ContractAddress, // Fee payment token pub OAppCore_peers: Map, // Trusted peers per chain } ``` **Provided Functions**: | Function | Description | | ------------------------ | ---------------------------- | | `set_peer(eid, peer)` | Set trusted peer for a chain | | `get_peer(eid)` | Get peer address for a chain | | `set_delegate(delegate)` | Set configuration delegate | | `endpoint()` | Get Endpoint address | ### OwnableComponent OpenZeppelin's ownership management: ```rust wrap theme={null} // Provided Functions owner() -> ContractAddress transfer_ownership(new_owner) renounce_ownership() ``` *** ## How OApp Messaging Works ### Peer Configuration: Establishing Trust Peers must be set bidirectionally for two OApps to communicate: #### Setting a Peer The `OAppCoreComponent` provides `set_peer` automatically when you embed `OAppCoreImpl`. You call it directly on your deployed contract: ```bash theme={null} sncast invoke --contract-address --function set_peer --calldata ``` Internally, the component implements it as: ```rust wrap theme={null} // Inside OAppCoreComponent::OAppCoreImpl (already embedded) fn set_peer(ref self: ComponentState, eid: u32, peer: Bytes32) { self._assert_only_owner(); self.OAppCore_peers.entry(eid).write(peer); self.emit(PeerSet { eid, peer }); } ``` #### Peer Address Format Peers are stored as `Bytes32` for cross-VM compatibility: ```rust wrap theme={null} // Starknet address → Bytes32 let starknet_peer: Bytes32 = starknet_address.into(); // EVM address (20 bytes) → Bytes32 (left-padded with zeros) let evm_peer: Bytes32 = Bytes32 { value: 0x000000000000000000000000_ABCDEF1234567890ABCDEF1234567890ABCDEF12 }; ``` #### Bidirectional Setup ``` Chain A (Starknet) Chain B (EVM) ┌─────────────────┐ ┌─────────────────┐ │ OApp A │ │ OApp B │ │ │ │ │ │ peers[B] = 0xB │◄──────────►│ peers[A] = 0xA │ └─────────────────┘ └─────────────────┘ ``` Both sides must set peers before messages can flow. *** ### Sending Messages #### Step 1: Quote the Fee ```rust wrap theme={null} use layerzero::oapps::oapp::oapp_core::OAppCoreComponent; use layerzero::common::structs::messaging::{MessagingParams, MessagingFee}; fn quote( self: @ContractState, dst_eid: u32, message: ByteArray, options: ByteArray, pay_in_lz_token: bool, ) -> MessagingFee { let oapp_core = get_dep_component!(self, OAppCore); OAppCoreComponent::OAppSenderImpl::_quote( oapp_core, dst_eid, message, options, pay_in_lz_token, ) } ``` #### Step 2: Build Options Options specify execution parameters on the destination chain: ```rust wrap theme={null} use lz_utils::byte_array_ext::byte_array_ext::ByteArrayTraitExt; const EXECUTOR_WORKER_ID: u8 = 1; const OPTION_TYPE_LZRECEIVE: u8 = 1; /// Build executor options for lz_receive with specified gas limit fn build_options(gas_limit: u128) -> ByteArray { // Build params (gas only, no native value) let mut params: ByteArray = Default::default(); params.append_u128(gas_limit); // Build options with Type 3 format let mut options: ByteArray = Default::default(); options.append_u16(3); // Option type 3 header options.append_u8(EXECUTOR_WORKER_ID); // Worker ID (Executor = 1) options.append_u16(params.len().try_into().unwrap() + 1); // Length (params + option type) options.append_u8(OPTION_TYPE_LZRECEIVE); // LzReceive option type options.append(@params); // Gas limit (16 bytes) options } ``` #### Step 3: Send the Message The `_lz_send` function handles fee payment internally. It expects the caller to have approved the OApp contract (not the endpoint) to spend their tokens. The function will: 1. Transfer tokens from caller to the contract 2. Approve the endpoint to spend the tokens 3. Send the message via the endpoint ```rust wrap theme={null} fn send( ref self: ContractState, caller: ContractAddress, dst_eid: u32, message: ByteArray, options: ByteArray, fee: MessagingFee, refund_address: ContractAddress, ) -> MessageReceipt { // _lz_send handles token transfer and endpoint approval internally let mut oapp_core = get_dep_component_mut!(ref self, OAppCore); OAppCoreComponent::OAppSenderImpl::_lz_send( ref oapp_core, caller, // Caller who approved this contract for fee payment dst_eid, message, options, fee, refund_address, ) } ``` #### Complete Send Example ```rust wrap theme={null} #[external(v0)] fn send_message( ref self: ContractState, dst_eid: u32, message: ByteArray, ) { let caller = get_caller_address(); // Build options (200,000 gas for lz_receive) let options = build_options(200000); // Quote fee let fee = self.quote(dst_eid, message.clone(), options.clone(), false); // IMPORTANT: Caller must have approved THIS CONTRACT (not the endpoint) // to spend native_fee amount of the native token BEFORE calling this function. // The _lz_send function will: // 1. transfer_from(caller, this_contract, fee) // 2. approve(endpoint, fee) // 3. endpoint.send(...) // Send message - _lz_send handles all token transfers internally let mut oapp_core = get_dep_component_mut!(ref self, OAppCore); let receipt = OAppCoreComponent::OAppSenderImpl::_lz_send( ref oapp_core, caller, dst_eid, message, options, fee, caller, // refund_address ); // Emit event with guid for tracking self.emit(MessageSent { guid: receipt.guid, dst_eid }); } ``` *** ### Receiving Messages #### Implementing OAppHooks The `OAppHooks` trait defines how your OApp handles incoming messages: ```rust wrap theme={null} impl OAppHooks of OAppCoreComponent::OAppHooks { fn _lz_receive( ref self: OAppCoreComponent::ComponentState, origin: Origin, // Source chain info guid: Bytes32, // Message unique ID message: ByteArray, // Your payload executor: ContractAddress, // Who executed the message extra_data: ByteArray, // Additional data from executor value: u256, // Native tokens forwarded ) { // 1. Decode your message payload let (action, data) = decode_message(@message); // 2. Access contract state if needed let mut contract = self.get_contract_mut(); // 3. Execute your logic match action { Action::Store => { contract.data.write(data); }, Action::Execute => { // Call other contracts, update state, etc. }, } // 4. Emit events for tracking contract.emit(MessageReceived { guid, src_eid: origin.src_eid, sender: origin.sender, }); } } ``` #### Origin Verification The OAppCore ensures only the Endpoint can call `lz_receive` and that the sender matches the trusted peer: ```rust wrap theme={null} // Inside OAppCoreComponent::LayerZeroReceiverImpl fn lz_receive( ref self: ComponentState, origin: Origin, guid: Bytes32, message: ByteArray, executor: ContractAddress, extra_data: ByteArray, value: u256, ) { // Only the Endpoint can call lz_receive self._assert_only_endpoint(); // Verify peer is set and matches sender let expected_peer = self._get_peer_or_revert(origin.src_eid); assert_with_byte_array( expected_peer == origin.sender, err_only_peer(origin.src_eid, origin.sender), ); // Call your _lz_receive implementation (via OAppHooks trait) self._lz_receive(origin, guid, message, executor, extra_data, value); } ``` *** ## Events ### Standard OApp Events ```rust wrap theme={null} // Peer configuration changed (emitted by OAppCoreComponent) #[derive(Drop, starknet::Event)] pub struct PeerSet { #[key] pub eid: u32, #[key] pub peer: Bytes32, } ``` **DelegateSet Event** The `DelegateSet` event is emitted by the **Endpoint contract** (not the OApp) when `set_delegate` is called. Listen for it on the Endpoint address, not your OApp. ### Custom Events Add your own events for tracking: ```rust wrap theme={null} #[derive(Drop, starknet::Event)] pub struct MessageSent { #[key] pub guid: Bytes32, pub dst_eid: u32, } #[derive(Drop, starknet::Event)] pub struct MessageReceived { #[key] pub guid: Bytes32, pub src_eid: u32, pub sender: Bytes32, } ``` *** ## Network Addresses | Network | Resource | Address | | ---------------- | ------------------ | -------------------------------------------------------------------- | | Starknet Sepolia | LayerZero Endpoint | `0x0316d70a6e0445a58c486215fac8ead48d3db985acde27efca9130da4c675878` | | Starknet Sepolia | STRK Token | `0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d` | | Starknet Mainnet | LayerZero Endpoint | `0x524e065abff21d225fb7b28f26ec2f48314ace6094bc085f0a7cf1dc2660f68` | | Starknet Mainnet | STRK Token | `0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d` | *** ## Next Steps * [OFT Overview](/v2/developers/starknet/oft/overview) - Token transfers * [Protocol Overview](/v2/developers/starknet/protocol-overview) - Message lifecycle * [Configuration Guide](/v2/developers/starknet/configuration/dvn-executor-config) - DVN setup * [Troubleshooting](/v2/developers/starknet/troubleshooting/common-errors) - Common errors # LayerZero V2 OFT on Starknet Source: https://docs.layerzero.network/v2/developers/starknet/oft/overview Create and send Omnichain Fungible Tokens on Starknet with LayerZero V2. Learn OFT variants, deployment, and crosschain token transfers. The **Omnichain Fungible Token (OFT)** standard enables crosschain token transfers on Starknet. OFTs extend the OApp pattern with built-in token handling, allowing seamless movement of fungible tokens between chains. ## What is an OFT on Starknet? An OFT on Starknet is a Cairo contract that extends the OApp functionality to enable crosschain token transfers. It integrates with Starknet's native ERC20 system (token contracts, approvals, and metadata) while providing LayerZero's omnichain capabilities. This guide will walk you through OFT concepts on Starknet; currently **OFTAdapter** and **OFTMintBurnAdapter** are available for deployment. To understand how OFTs integrate with Starknet's ERC20 system and the differences between mint/burn and lock/unlock token management strategies, see [Integration with Starknet ERC20 System](#integration-with-starknet-erc20-system). ## Integration with Starknet ERC20 System ### OFT Types Starknet provides three OFT variants for different use cases: | Variant | Token Ownership | Use Case | | ---------------------- | ---------------------------- | ------------------------------------------ | | **OFT** | OFT contract owns the token | New tokens native to LayerZero | | **OFTAdapter** | Wraps existing ERC20 | Bridge existing tokens (lock/unlock) | | **OFTMintBurnAdapter** | Delegates to minter contract | Existing tokens with mint/burn permissions | **Availability** At the moment, **OFTAdapter** and **OFTMintBurnAdapter** are available on Starknet. **OFT** is not yet supported for deployment. #### Decision Matrix ```mermaid theme={null} flowchart TD A[Do you have an existing token?] -->|No| B[Use OFT] A -->|Yes| C[Can you grant mint/burn to adapter?] C -->|Yes| D[Use OFTMintBurnAdapter] C -->|No| E[Use OFTAdapter] ``` *** ### OFT Adapter Use when bridging an existing token where you **cannot grant mint/burn permissions** to the adapter. #### How It Works * **Send**: Locks tokens in the adapter contract * **Receive**: Unlocks tokens from the adapter contract * **Liquidity Required**: Adapter must hold sufficient token balance **OFTAdapter** class hash: **0x07085790a9702314791b55d7ac1e1202abf152174cc61d8fc3cab36ac4750171** ([View on explorer](https://sepolia.voyager.online/class/0x07085790a9702314791b55d7ac1e1202abf152174cc61d8fc3cab36ac4750171)) *** ### OFT Mint/Burn Adapter Use when bridging an existing token where you **can grant mint/burn permissions** to the adapter. #### How It Works * **Send**: Burns tokens via minter contract * **Receive**: Mints tokens via minter contract * **No Liquidity Required**: Mint/burn eliminates liquidity constraints #### Additional Features The OFTMintBurnAdapter includes: * **Rate Limiting**: Control transfer volume per chain * **Fee Collection**: Charge fees on transfers * **Pausability**: Emergency pause functionality * **Role-Based Access**: Granular permission control * **Upgradeability**: Contract upgrade support **OFTMintBurnAdapter** class hash: **0x07c02E3797d2c7B848FA94820FfB335617820d2c44D82d6B8Cf71c71fbE7dd6E** ([View on explorer](https://sepolia.voyager.online/class/0x07c02E3797d2c7B848FA94820FfB335617820d2c44D82d6B8Cf71c71fbE7dd6E)) #### Role Management The OFTMintBurnAdapter uses OpenZeppelin's AccessControl with the following roles: | Role | felt252 Value | Permissions | | --------------------------- | ----------------------------- | ------------------------ | | `DEFAULT_ADMIN_ROLE` | `0x0` | Grant/revoke other roles | | `FEE_MANAGER_ROLE` | `'FEE_MANAGER_ROLE'` | Withdraw collected fees | | `PAUSE_MANAGER_ROLE` | `'PAUSE_MANAGER_ROLE'` | Pause/unpause contract | | `RATE_LIMITER_MANAGER_ROLE` | `'RATE_LIMITER_MANAGER_ROLE'` | Configure rate limits | | `UPGRADE_MANAGER_ROLE` | `'UPGRADE_MANAGER_ROLE'` | Upgrade contract | **Role Constants** Roles are defined as short strings (felt252). To grant a role via sncast, use the string's felt252 encoding. For example, `'FEE_MANAGER_ROLE'` encodes to `0x4645455f4d414e414745525f524f4c45`. ```bash theme={null} # Grant FEE_MANAGER_ROLE to an address sncast --account invoke \ --contract-address 0x \ --function grant_role \ --url \ --arguments '0x4645455f4d414e414745525f524f4c45, ' ``` ## Deployment Before building an OFT, install the required dependencies. **New to Starknet?** If you haven't used Starknet before, start with [Getting Started on Starknet](/v2/developers/starknet/getting-started) to understand the account model, tooling, and development basics. **Prerequisites**: * Scarb and Starknet Foundry installed (see [Getting Started](/v2/developers/starknet/getting-started)) * Node.js and npm for installing LayerZero packages * A funded Starknet account and RPC URL for deployment (see [Getting Started](/v2/developers/starknet/getting-started)) **Deployment workflow for OFTMintBurnAdapter:** 1. Deploy `ERC20MintBurnUpgradeable` as your token 2. Deploy `OFTMintBurnAdapter` with the token address as both `erc20_token` and `minter_burner` 3. Grant the adapter's address permission to mint/burn on the token contract ### Step 1: Deploy ERC20MintBurnUpgradeable For OFTMintBurnAdapter deployments, LayerZero provides a reference ERC20 token implementation with built-in mint/burn permissions: This contract: * Implements the `IMintableToken` interface * Supports role-based access for mint/burn permissions * Is upgradeable via OpenZeppelin's `UpgradeableComponent` The **ERC20MintBurnUpgradeable** class has been declared and has been verified - [view on explorer](https://sepolia.voyager.online/class/0x01bea3900ebe975f332083d441cac55f807cf5de7b1aa0b7ccbda1de53268500). ```bash theme={null} # Deploy sncast --account deploy \ --class-hash 0x01bea3900ebe975f332083d441cac55f807cf5de7b1aa0b7ccbda1de53268500 \ --url \ --arguments '"MyToken", "MTK", 18, ' ``` Constructor parameters: * `name` (ByteArray) - Token name (use quoted string) * `symbol` (ByteArray) - Token symbol (use quoted string) * `decimals` (u8) - Token decimals (e.g., 18) * `default_admin` (ContractAddress) - Address granted `DEFAULT_ADMIN_ROLE`. You can set this to your address. Running the above successfully would return an output like: ``` Success: Deployment completed Contract Address: Transaction Hash: To see deployment details, visit: contract: https://sepolia.starkscan.co/contract/ transaction: https://sepolia.starkscan.co/tx/ ``` Copy the Contract Address and set it aside for use in the next step. ### Step 2: Deploy OFTMintBurnAdapter ```bash theme={null} sncast --account deploy \ --class-hash 0x07c02E3797d2c7B848FA94820FfB335617820d2c44D82d6B8Cf71c71fbE7dd6E \ --url \ --arguments ', , 0x0316d70a6e0445a58c486215fac8ead48d3db985acde27efca9130da4c675878, , 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d, ' ``` Constructor parameters: * `erc20_token`: ERC20 token contract address * `minter_burner`: Minter/burner contract address (use the token address) * `lz_endpoint`: LayerZero Endpoint address (`0x0316d70a6e0445a58c486215fac8ead48d3db985acde27efca9130da4c675878` for Sepolia, `0x524e065abff21d225fb7b28f26ec2f48314ace6094bc085f0a7cf1dc2660f68` for Mainnet) * `owner`: Contract owner address (your deployer account) * `native_token`: Fee payment token (STRK token address shown above) * `shared_decimals`: Shared decimals across chains (u8, e.g., 6) **STRK Token Address** The address `0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d` is the STRK token contract on Starknet (same on both Sepolia testnet and Mainnet). This is used to pay LayerZero messaging fees. **Network addresses** For endpoint IDs and LayerZero contract addresses, see [Deployed Contracts](/v2/deployments/deployed-contracts). *** ## Deployment for Custom OFT If you need to build a custom OFT contract (e.g., with additional logic or modifications), follow these steps to set up your project before declaring and deploying. You can use the OFTMintBurnAdapter as a starting point. ### Step 1: Install LayerZero Cairo Contracts The LayerZero Cairo packages are currently published on NPM. ```bash theme={null} # Create your project directory mkdir my-oft-project && cd my-oft-project # Initialize npm and install LayerZero Starknet packages npm init -y ``` ```bash theme={null} npm install @layerzerolabs/protocol-starknet-v2 @layerzerolabs/oft-mint-burn-starknet ``` ```bash theme={null} npm install @layerzerolabs/protocol-starknet-v2 @layerzerolabs/oft-adapter-starknet ``` ### Step 2: Copy the contracts Copy the contracts into your project's directory: ```bash theme={null} cp -R node_modules/@layerzerolabs/oft-mint-burn-starknet/contracts/oft_mint_burn/. . ``` ```bash theme={null} cp -R node_modules/@layerzerolabs/oft-adapter-starknet/contracts/oft_adapter/. . ``` ### Step 3: Modify dependency paths In the `Scarb.toml`, remove the parent directory references in the `path` fields: ``` - lz_utils = { path = "../../node_modules/@layerzerolabs/protocol-starknet-v2/libs/lz_utils" } - layerzero = { path = "../../node_modules/@layerzerolabs/protocol-starknet-v2/layerzero" } + lz_utils = { path = "node_modules/@layerzerolabs/protocol-starknet-v2/libs/lz_utils" } + layerzero = { path = "node_modules/@layerzerolabs/protocol-starknet-v2/layerzero" } ``` ### Step 4: Make your customizations Modify the contract as necessary. ### Step 5: Build, Declare, and Deploy ```bash theme={null} # Build the contract scarb build # Declare the contract class sncast declare --contract-name MyCustomOFT # Deploy with your constructor arguments sncast deploy \ --class-hash \ --arguments '' ``` *** ## Core Operations ### Sending Tokens #### Step 1: Quote ```rust wrap theme={null} // Get fee and receipt estimates let send_param = SendParam { dst_eid: 30101, // Ethereum Mainnet to: recipient_bytes32, amount_ld: 1000000000000000000_u256, // 1 token min_amount_ld: 900000000000000000_u256, // 0.9 token minimum extra_options: build_options(200000), }; let quote = oft.quote_oft(send_param); // quote.receipt.amount_sent_ld = actual amount debited // quote.receipt.amount_received_ld = amount received on destination ``` #### Step 2: Get Messaging Fee ```rust wrap theme={null} let messaging_fee = oft.quote_send(send_param, false); // messaging_fee.native_fee = STRK/ETH needed for LayerZero ``` #### Step 3: Send ```rust wrap theme={null} // Approve tokens if using OFTAdapter if oft.approval_required() { token.approve(oft_address, send_param.amount_ld); } // Approve native token for messaging fee native_token.approve(oft_address, messaging_fee.native_fee); // Send tokens let result = oft.send(send_param, messaging_fee, refund_address); // result.message_receipt.guid = unique message ID // result.oft_receipt = actual amounts sent/received ``` *** ## Decimal Precision ### Token Amounts and u256 All token amounts in Starknet OFT contracts use **`u256`**, matching Solidity's `uint256` and OpenZeppelin's Cairo ERC20 interface for crosschain compatibility. ```rust wrap theme={null} // OFT amounts are always u256 let amount_ld: u256 = 1000000000000000000_u256; // 1 token (18 decimals) let min_amount_ld: u256 = 900000000000000000_u256; // 0.9 token minimum ``` ### Local vs Shared Decimals OFTs use two decimal representations: | Type | Description | Typical Value | | ------------------- | --------------------------------- | ------------- | | **Local Decimals** | Token decimals on this chain | 18 | | **Shared Decimals** | Common decimals across all chains | 6 | ```rust wrap theme={null} // Shared decimals = 6 means max precision of 6 decimal places // A token with 18 local decimals has conversion rate of 10^12 const SHARED_DECIMALS: u8 = 6; let local_decimals: u8 = 18; let conversion_rate = 10_u256.pow((local_decimals - SHARED_DECIMALS).into()); // 10^12 ``` ### Dust Removal When converting from local to shared decimals, precision is lost ("dust"): ```rust wrap theme={null} // Sending 1.123456789012345678 tokens (18 decimals) // Shared representation: 1.123456 (6 decimals) // Dust lost: 0.000000789012345678 fn _remove_dust(self: @ComponentState, amount_ld: u256) -> u256 { let conversion_rate = self.OFTCore_decimal_conversion_rate.read(); (amount_ld / conversion_rate) * conversion_rate } ``` Always use `quote_oft` before sending to see the exact amounts after dust removal and fees. *** ## Configuration After deployment, configure your OFT to enable crosschain transfers. Use the SDK for DVN/executor config and set peers last. **Critical order** Configure security settings before setting peers. Setting peers opens the pathway. **Endpoint IDs** For endpoint IDs and LayerZero contract addresses, see [Deployed Contracts](/v2/deployments/deployed-contracts). ### SDK Setup Install the SDK dependencies: ```bash theme={null} npm install starknet @layerzerolabs/lz-v2-protocol-starknet @layerzerolabs/lz-v2-utilities @layerzerolabs/lz-definitions npm install -D tsx ``` Create `config.ts` (or equivalent) and load your compiled artifact from `target/release/*.contract_class.json`: ```typescript wrap theme={null} import {readFileSync} from 'node:fs'; import {RpcProvider, Account, Contract} from 'starknet'; import { getEndpointV2Contract, getOAppContract, encodeUlnConfig, encodeExecutorConfig, MessageLibConfigType, } from '@layerzerolabs/lz-v2-protocol-starknet'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {EndpointId, ChainName, Environment} from '@layerzerolabs/lz-definitions'; async function main() { const RPC_URL = process.env.RPC_URL!; const ACCOUNT_ADDRESS = process.env.ACCOUNT_ADDRESS!; const PRIVATE_KEY = process.env.PRIVATE_KEY!; const OFT_ADDRESS = process.env.OFT_ADDRESS!; const OFT_ARTIFACT_PATH = process.env.OFT_ARTIFACT_PATH!; const compiledArtifact = JSON.parse(readFileSync(OFT_ARTIFACT_PATH, 'utf8')); const provider = new RpcProvider({nodeUrl: RPC_URL}); const account = new Account({provider, address: ACCOUNT_ADDRESS, signer: PRIVATE_KEY}); const endpoint = await getEndpointV2Contract(ChainName.STARKNET, Environment.TESTNET, provider); const oapp = await getOAppContract(OFT_ADDRESS, provider); const oappOptions = new Contract({ abi: compiledArtifact.abi, address: OFT_ADDRESS, provider, }).typedv2(compiledArtifact.abi); const remoteEid = EndpointId.ETHEREUM_V2_MAINNET; // Configuration code goes here... } main().catch(console.error); ``` Run the script: ```bash theme={null} RPC_URL=... \ ACCOUNT_ADDRESS=0x... \ PRIVATE_KEY=0x... \ OFT_ADDRESS=0x... \ OFT_ARTIFACT_PATH=./target/release/my_oft_OFT.contract_class.json \ npx tsx config.ts ``` ### Prerequisite: Set Delegate (required if configuring via external account) Endpoint configuration calls (`set_send_library`, `set_receive_library`, `set_send_configs`, `set_receive_configs`) require the caller to be the OApp itself or an authorized delegate. If you're configuring from an external account, set a delegate first (owner-only): ```typescript wrap theme={null} const setDelegateCall = oapp.populateTransaction.set_delegate(DELEGATE_ADDRESS); await account.execute([setDelegateCall]); ``` Use the address of the account that will submit the endpoint configuration transactions. ### Step 1: Set Message Libraries (optional) Use custom send/receive libraries when defaults are unavailable for your EID. ```typescript wrap theme={null} const setSendLibCall = endpoint.populateTransaction.set_send_library( OFT_ADDRESS, remoteEid, SEND_LIB_ADDRESS, ); const setReceiveLibCall = endpoint.populateTransaction.set_receive_library( OFT_ADDRESS, remoteEid, RECEIVE_LIB_ADDRESS, 0, // Grace period in blocks ); await account.execute([setSendLibCall, setReceiveLibCall]); const sendLibAddress = (await endpoint.get_send_library(OFT_ADDRESS, remoteEid)).lib; const receiveLibAddress = (await endpoint.get_receive_library(OFT_ADDRESS, remoteEid)).lib; ``` ### Step 2: Configure DVNs (recommended) Configure ULN settings for both send and receive. DVN addresses must be sorted ascending. ```typescript wrap theme={null} const sendUlnConfig = encodeUlnConfig({ confirmations: 15, has_confirmations: true, required_dvns: [LAYERZERO_DVN_ADDRESS], has_required_dvns: true, optional_dvns: [], optional_dvn_threshold: 0, has_optional_dvns: false, }); const setSendConfigCall = endpoint.populateTransaction.set_send_configs( OFT_ADDRESS, sendLibAddress, [ { eid: remoteEid, config_type: MessageLibConfigType.ULN, // 2 config: sendUlnConfig, }, ], ); await account.execute([setSendConfigCall]); const receiveUlnConfig = encodeUlnConfig({ confirmations: 15, has_confirmations: true, required_dvns: [LAYERZERO_DVN_ADDRESS], has_required_dvns: true, optional_dvns: [], optional_dvn_threshold: 0, has_optional_dvns: false, }); const setReceiveConfigCall = endpoint.populateTransaction.set_receive_configs( OFT_ADDRESS, receiveLibAddress, [ { eid: remoteEid, config_type: MessageLibConfigType.ULN, // 2 config: receiveUlnConfig, }, ], ); await account.execute([setReceiveConfigCall]); ``` ### Step 3: Configure Executor (recommended) Executor settings apply to send direction. ```typescript wrap theme={null} const executorConfig = encodeExecutorConfig({ max_message_size: 10000, executor: EXECUTOR_ADDRESS, }); const setExecutorConfigCall = endpoint.populateTransaction.set_send_configs( OFT_ADDRESS, sendLibAddress, [ { eid: remoteEid, config_type: MessageLibConfigType.EXECUTOR, // 1 config: executorConfig, }, ], ); await account.execute([setExecutorConfigCall]); ``` ### Step 4: Set Enforced Options (optional) All OFT variants include the `OAppOptionsType3Component` for managing execution options. Use it to set minimum gas requirements per destination chain. If `getOAppContract` does not expose `set_enforced_options`, load your compiled artifact from `target/dev/*.contract_class.json` as shown in the SDK setup. ```typescript wrap theme={null} const options = Options.newOptions() .addExecutorLzReceiveOption(200000, 0) // 200k gas for lz_receive .toBytes(); const setOptionsCall = oappOptions.populateTransaction.set_enforced_options([ { eid: remoteEid, msg_type: 1, // SEND options, }, ]); await account.execute([setOptionsCall]); ``` If you prefer `sncast`, you can call the entrypoint directly: ```bash theme={null} # Set enforced options for SEND message type (type 1) # Options format: 0x0003 (type 3 header) + executor options sncast --account invoke \ --contract-address \ --function set_enforced_options \ --url \ --arguments 'array![layerzero::oapps::common::oapp_options_type_3::structs::EnforcedOptionParam { eid: , msg_type: 1, options: }]' ``` For ``, pass a ByteArray expression (see the Starknet Foundry calldata transformation docs). With `--arguments`, use a raw ByteArray struct literal for arbitrary bytes, e.g. `core::byte_array::ByteArray { data: array![0x..., 0x...], pending_word: 0x..., pending_word_len: 0 }` (data are 31-byte chunks; pending\_word\_len is 0-30). Example: lzReceive gas = 120000, value = 0: ```bash theme={null} sncast --account invoke \ --contract-address \ --function set_enforced_options \ --url \ --arguments 'array![layerzero::oapps::common::oapp_options_type_3::structs::EnforcedOptionParam { eid: , msg_type: 1, options: core::byte_array::ByteArray { data: array![], pending_word: 0x0003010011010000000000000000000000000001d4c0, pending_word_len: 22_u8 } }]' ``` ### Step 5: Set Peer (required, last) Set the remote peer after security configuration. EVM addresses must be left-padded to 32 bytes. ```typescript wrap theme={null} const peerBytes32 = {value: BigInt('0x000000000000000000000000' + EVM_OFT_ADDRESS.slice(2))}; const setPeerCall = oapp.populateTransaction.set_peer(remoteEid, peerBytes32); await account.execute([setPeerCall]); ``` **Configuration order**: 1. Set delegate (required if configuring via external account) 2. Set message libraries (optional) 3. Configure DVNs (recommended) 4. Configure executor (recommended) 5. Set enforced options (optional) 6. Set peer (required, last) See the [Configuration Guide](/v2/developers/starknet/configuration/dvn-executor-config) for detailed options encoding, DVN ordering, and gas recommendations. *** ## Events ### OFT-Specific Events ```rust wrap theme={null} // Tokens sent to another chain #[derive(Drop, starknet::Event)] pub struct OFTSent { #[key] pub guid: Bytes32, pub dst_eid: u32, pub from: ContractAddress, pub amount_sent_ld: u256, pub amount_received_ld: u256, } // Tokens received from another chain #[derive(Drop, starknet::Event)] pub struct OFTReceived { #[key] pub guid: Bytes32, pub src_eid: u32, pub to: ContractAddress, pub amount_received_ld: u256, } ``` *** ## Best Practices & Deployment Checklist 1. **Install dependencies** - npm install LayerZero packages 2. **Choose OFT variant** based on your token situation 3. **Build contract** via `scarb build` 4. **Declare contract** via `sncast declare` 5. **Deploy contract** via `sncast deploy` with `--arguments` 6. **Verify contract** via `sncast verify` using Voyager or Walnut 7. **Configure DVNs and executor** for security (see [Configuration Guide](/v2/developers/starknet/configuration/dvn-executor-config)) 8. **Set enforced options** for minimum gas 9. **Set peers last** on both chains (bidirectional) 10. **Test on testnet** before mainnet deployment *** ## Next Steps * [Configuration Guide](/v2/developers/starknet/configuration/dvn-executor-config) - DVN and security setup * [Protocol Overview](/v2/developers/starknet/protocol-overview) - Message lifecycle * [Technical Reference](/v2/developers/starknet/technical-reference/starknet-guidance) - Deployment tooling * [Troubleshooting](/v2/developers/starknet/troubleshooting/common-errors) - Common errors # LayerZero V2 Starknet Contracts Source: https://docs.layerzero.network/v2/developers/starknet/overview Overview of Starknet Cairo Contracts on LayerZero V2. Learn the architecture, features, and how to get started building crosschain applications on Starknet. The LayerZero Protocol on Starknet consists of Cairo contracts designed to facilitate the secure movement of data, tokens, and digital assets between different blockchain environments. LayerZero provides **Starknet Cairo Contracts** that can communicate directly with the equivalent [Solidity Contract Libraries](/v2/developers/evm/overview) and other blockchain implementations deployed across supported chains. ## Starknet and LayerZero Starknet uses the [Cairo programming language](https://book.cairo-lang.org/) and employs a unique execution model based on **account abstraction** (all accounts are smart contracts) and the **[dispatcher pattern](https://book.cairo-lang.org/ch102-02-interacting-with-another-contract.html#the-dispatcher-pattern)** (typed cross-contract calls via generated interfaces) to achieve crosschain functionality. ### Starknet Cairo Contracts Learn how the LayerZero V2 Protocol operates on Starknet, including prerequisites and key concepts. Deep dive into Starknet account abstraction, Cairo fundamentals, and dispatcher patterns. Build the contracts necessary for sending arbitrary data and external function calls crosschain on Starknet. Create and send Omnichain Fungible Tokens (OFTs) on the Starknet blockchain. #### Starknet Protocol Configurations Configure which decentralized verifier networks (DVNs) secure your messages. Configure who executes your messages on the destination chain. Set the amount of gas to deliver to the destination chain.
Starknet contract deployment requires native Starknet tooling (`sncast`). See the [Technical Reference](/v2/developers/starknet/technical-reference/starknet-guidance) for deployment instructions. ### Tooling and Resources Starknet development relies on the [Cairo programming language](https://book.cairo-lang.org/) and the [Starknet CLI tools](https://docs.starknet.io/tools/). For comprehensive information, see the [Starknet Documentation](https://docs.starknet.io/). LayerZero provides developer tooling to simplify the contract development, testing, and deployment process: [LayerZero Scan](/v2/developers/layerzero-scan-explorer): a comprehensive crosschain explorer, search, API, and analytics platform for tracking and debugging your omnichain transactions. **Development Tools**: * [Scarb](https://docs.swmansion.com/scarb/) - Cairo build tool and package manager * [sncast](https://foundry-rs.github.io/starknet-foundry/starknet/sncast.html) - CLI for declaring and deploying contracts * [snforge](https://foundry-rs.github.io/starknet-foundry/) - Testing framework for Cairo contracts (analogous to Foundry's `forge` in the EVM world) You can also ask for help or follow development in the [Discord](https://discord.com/invite/ktbvm8Nkcr). # LayerZero V2 Starknet Protocol Overview Source: https://docs.layerzero.network/v2/developers/starknet/protocol-overview Deep technical dive into the LayerZero V2 protocol implementation on Starknet covering message lifecycle, DVN verification, and recovery operations. This page provides a deep technical dive into the LayerZero V2 protocol implementation on Starknet, documenting the complete message lifecycle with contract-level code samples and function signatures. **What you'll find**: * Complete send workflow (quote, approve, send, endpoint processing) * DVN verification process and verification status checks * Executor delivery and OApp receive handling * Recovery operations (skip, clear, nilify, burn) * Security considerations for payload hashes and reentrancy **Target audience**: Developers who understand Starknet basics and want to deeply understand the protocol implementation. **Prerequisites** Before reading this page, familiarize yourself with Starknet fundamentals in [Technical Overview](/v2/developers/starknet/technical-overview). For SDK usage and practical implementation, see [OApp](/v2/developers/starknet/oapp/overview) or [OFT](/v2/developers/starknet/oft/overview) guides. *** This page documents the complete message lifecycle with contract-level implementation details: * **Send Workflow:** Message initiation, fee calculation, nonce management, and packet dispatch * **Verification Workflow:** DVN submission, verification checks, and execution readiness * **Receive Workflow:** Executor delivery, payload clearing, and OApp processing ## Send Overview When an OApp initiates a crosschain message, the following high-level steps occur on the source chain. ### Message Lifecycle Overview The LayerZero protocol enables secure crosschain messaging through a three-phase process: ```mermaid theme={null} sequenceDiagram participant User participant OApp_Src as OApp (Source) participant Endpoint_Src as Endpoint (Source) participant DVN participant ReceiveLib as Receive Library (ULN) participant Executor participant Endpoint_Dst as Endpoint (Dest) participant OApp_Dst as OApp (Dest) User->>OApp_Src: invoke send() OApp_Src->>Endpoint_Src: quote() + send() Endpoint_Src-->>DVN: PacketSent event DVN->>DVN: verify packet DVN->>ReceiveLib: submit verification ReceiveLib->>Endpoint_Dst: verify() Executor->>Endpoint_Dst: lz_receive() Endpoint_Dst->>OApp_Dst: lz_receive callback ``` ### Core Data Structures #### Packet The `Packet` structure represents a crosschain message: ```rust wrap theme={null} #[derive(Clone, Drop, Serde)] pub struct Packet { pub nonce: u64, // Sequential message number pub src_eid: u32, // Source Endpoint ID pub sender: ContractAddress, // Source OApp address pub dst_eid: u32, // Destination Endpoint ID pub receiver: Bytes32, // Destination OApp (bytes32 for cross-VM compatibility) pub guid: Bytes32, // Globally Unique Identifier pub message: ByteArray, // Application payload } ``` #### Origin The `Origin` structure identifies the source of an incoming message: ```rust wrap theme={null} #[derive(Clone, Drop, Serde, Debug, PartialEq, Default)] pub struct Origin { pub src_eid: u32, // Source chain Endpoint ID pub sender: Bytes32, // Source OApp address (bytes32) pub nonce: u64, // Message nonce for ordering } ``` #### MessagingParams Parameters for sending a message: ```rust wrap theme={null} pub struct MessagingParams { pub dst_eid: u32, // Destination Endpoint ID pub receiver: Bytes32, // Recipient OApp address pub message: ByteArray, // Application payload pub options: ByteArray, // Execution options (gas, etc.) pub pay_in_lz_token: bool, // Pay fees in ZRO token } ``` #### MessagingFee Fee structure returned by quote operations: ```rust wrap theme={null} pub struct MessagingFee { pub native_fee: u256, // Fee in native token (STRK/ETH) pub lz_token_fee: u256, // Fee in ZRO token (if applicable) } ``` ### Send Workflow When an OApp initiates a crosschain message, the following steps occur: #### Step 1: Quote the Fee Before sending, get a fee estimate: ```rust wrap theme={null} // In your OApp or client code fn quote_send( self: @ContractState, dst_eid: u32, message: ByteArray, options: ByteArray, ) -> MessagingFee { let params = MessagingParams { dst_eid, receiver: self.peers.read(dst_eid), message, options, pay_in_lz_token: false, }; let endpoint = IEndpointV2Dispatcher { contract_address: self.endpoint.read() }; endpoint.quote(params, get_contract_address()) } ``` #### Step 2: Approve Fees The caller must approve the Endpoint to spend their tokens: ```rust wrap theme={null} // Approve native token for fee payment let native_token = IERC20Dispatcher { contract_address: native_token_address }; native_token.approve(endpoint_address, fee.native_fee); ``` #### Step 3: Send the Message The OApp calls the Endpoint's `send` function: ```rust wrap theme={null} fn send( ref self: ContractState, dst_eid: u32, message: ByteArray, options: ByteArray, fee: MessagingFee, refund_address: ContractAddress, ) -> MessageReceipt { let params = MessagingParams { dst_eid, receiver: self.peers.read(dst_eid), message, options, pay_in_lz_token: false, }; let endpoint = IEndpointV2Dispatcher { contract_address: self.endpoint.read() }; endpoint.send(params, refund_address) } ``` #### Step 4: Endpoint Processing The Endpoint performs the following: 1. **Creates the Packet** with a unique GUID and incremented nonce 2. **Looks up the Send Library** for this OApp/destination pair 3. **Routes to Message Library** (ULN302) for worker fee calculation 4. **Pays Workers** (DVNs, Executor) via ERC20 transfers 5. **Emits PacketSent event** with encoded packet and options ```rust wrap theme={null} // Internal Endpoint logic (simplified) fn send(ref self: ContractState, params: MessagingParams, refund_address: ContractAddress) -> MessageReceipt { let sender = get_caller_address(); // Create packet with new nonce let nonce = self.outbound_nonce(sender, params.dst_eid, params.receiver) + 1; let packet = Packet { nonce, src_eid: self.eid.read(), sender, dst_eid: params.dst_eid, receiver: params.receiver, guid: GUID::generate(nonce, src_eid, sender, dst_eid, receiver), message: params.message, }; // Send through message library and pay workers let result = message_lib.send(packet, params.options, params.pay_in_lz_token); self._pay_workers(sender, result.receipt, refund_address, params.pay_in_lz_token); // Emit event for off-chain listeners self.emit(PacketSent { encoded_packet: result.encoded_packet, options: params.options, send_library }); result.message_receipt } ``` #### Events Emitted During Send | Event | Description | | ------------ | ---------------------------------------------------------- | | `PacketSent` | Contains encoded packet, options, and send library address | *** ## Verification Workflow After a `PacketSent` event is emitted, DVNs verify the message and the destination Endpoint records verification data. ### DVN Verification Process #### Step 1: DVN Monitors Source Chain DVNs monitor the source chain for `PacketSent` events and extract the packet data. #### Step 2: DVN Submits Verification Once a DVN has verified the packet (e.g., confirmed finality), it submits the verification to the receive library. The receive library then calls `verify` on the destination Endpoint. ```rust wrap theme={null} // Called by receive library on destination chain after DVN quorum fn verify( ref self: ContractState, origin: Origin, receiver: ContractAddress, payload_hash: Bytes32, ) { // Verify caller is a valid receive library self._assert_only_receive_library(receiver, origin.src_eid); // Store the payload hash self.inbound_payload_hash.write( (receiver, origin.src_eid, origin.sender, origin.nonce), payload_hash ); self.emit(PacketVerified { origin, receiver, payload_hash }); } ``` #### Step 3: Check Verification Status The Executor (or anyone) can check if a message is ready for execution: ```rust wrap theme={null} fn executable(self: @ContractState, origin: Origin, receiver: ContractAddress) -> ExecutionState { let payload_hash = self.inbound_payload_hash(receiver, origin.src_eid, origin.sender, origin.nonce); if payload_hash == EMPTY_PAYLOAD_HASH && nonce <= lazy_inbound_nonce { return ExecutionState::Executed; // Already executed } if payload_hash != NIL_PAYLOAD_HASH && nonce <= inbound_nonce { return ExecutionState::Executable; // Ready to execute } if payload_hash != EMPTY_PAYLOAD_HASH && payload_hash != NIL_PAYLOAD_HASH { return ExecutionState::VerifiedButNotExecutable; // Verified but blocked } ExecutionState::NotExecutable } ``` ### Events Emitted During Verification | Event | Description | | ---------------- | ------------------------------------------- | | `PacketVerified` | Contains origin, receiver, and payload hash | *** ## Receive Workflow Once verified, the Executor delivers the message to the destination OApp. ### Executor Delivery #### Step 1: Executor Calls lz\_receive ```rust wrap theme={null} // Called by Executor on destination chain fn lz_receive( ref self: ContractState, origin: Origin, receiver: ContractAddress, guid: Bytes32, message: ByteArray, extra_data: ByteArray, value: u256, // Native token value to forward ) { // Clear payload hash first (prevents reentrancy) let payload = self._create_payload(guid, @message); self._clear_payload(receiver, @origin, @payload); // Transfer value to receiver (if any) if value > 0 { native_token.transfer_from(executor, receiver, value); } // Call receiver's lz_receive let receiver_dispatcher = ILayerZeroReceiverDispatcher { contract_address: receiver }; receiver_dispatcher.lz_receive(origin, guid, message, executor, extra_data, value); self.emit(PacketDelivered { origin, receiver }); } ``` #### Step 2: OApp Handles the Message Your OApp implements the `ILayerZeroReceiver` interface: ```rust wrap theme={null} impl OAppHooks of OAppCoreComponent::OAppHooks { fn _lz_receive( ref self: OAppCoreComponent::ComponentState, origin: Origin, guid: Bytes32, message: ByteArray, executor: ContractAddress, extra_data: ByteArray, value: u256, ) { // Decode and process the message // The OApp has access to: // - origin.src_eid: source chain // - origin.sender: source OApp (bytes32) // - message: application payload // - value: native tokens forwarded // Your custom logic here } } ``` ### Events Emitted During Receive | Event | Description | | ----------------- | ---------------------------------------- | | `PacketDelivered` | Confirms successful delivery to receiver | | `LzReceiveAlert` | Emitted if lz\_receive execution fails | *** ## Recovery Operations LayerZero provides mechanisms for handling stuck or problematic messages: ### Skip Skip an unverified message (before DVN verification): ```rust wrap theme={null} fn skip(ref self: ContractState, oapp: ContractAddress, src_eid: u32, sender: Bytes32, nonce: u64) { // Only callable by OApp owner/delegate // Marks nonce as processed without verification } ``` **Use Case**: Skip a message that will never be verified (e.g., source chain reorg). ### Clear Clear a verified but unexecuted message: ```rust wrap theme={null} fn clear( ref self: ContractState, origin: Origin, receiver: ContractAddress, guid: Bytes32, message: ByteArray, ) { self._assert_authorized(receiver); let payload = self._create_payload(guid, @message); self._clear_payload(receiver, @origin, @payload); self.emit(PacketDelivered { origin, receiver }); } ``` **Use Case**: Clear a message that's blocking subsequent messages due to ordering. ### Nilify Reset a verification to allow re-verification: ```rust wrap theme={null} fn nilify(ref self: ContractState, oapp: ContractAddress, src_eid: u32, sender: Bytes32, nonce: u64, payload_hash: Bytes32) { // Sets payload hash to NIL_PAYLOAD_HASH // Message must be re-verified before execution } ``` **Use Case**: Dispute a verification or handle DVN misbehavior. ### Burn Permanently block a message: ```rust wrap theme={null} fn burn(ref self: ContractState, oapp: ContractAddress, src_eid: u32, sender: Bytes32, nonce: u64, payload_hash: Bytes32) { // Permanently marks message as non-executable // Cannot be reversed } ``` **Use Case**: Permanently reject a malicious or invalid message. | Operation | Reversible | When to Use | | --------- | ---------- | -------------------------- | | `skip` | Yes | Message won't be verified | | `clear` | No | Unblock message ordering | | `nilify` | Yes | Dispute verification | | `burn` | No | Permanently reject message | *** ## Security Considerations ### Payload Hash Verification The Endpoint stores only the hash of the payload, not the full message. This: * Saves storage costs * Prevents spam attacks * Requires Executor to provide correct message data ### Reentrancy Protection The Endpoint uses OpenZeppelin's `ReentrancyGuard` component: ```rust wrap theme={null} fn send(ref self: ContractState, params: MessagingParams, refund_address: ContractAddress) -> MessageReceipt { self.reentrancy_guard.start(); // Lock // ... send logic ... self.reentrancy_guard.end(); // Unlock message_receipt } ``` ### Clear Before Execute The `lz_receive` function clears the payload hash before calling the receiver's handler: ```rust wrap theme={null} // Clear first (prevents reentrancy attacks) self._clear_payload(receiver, @origin, @payload); // Then execute (safe even if receiver calls back) receiver_dispatcher.lz_receive(...); ``` This "clear-then-execute" pattern prevents reentrancy attacks where a malicious receiver could attempt to re-execute the same message. *** ## Next Steps * [Technical Overview](/v2/developers/starknet/technical-overview) - Starknet architecture details * [OApp Overview](/v2/developers/starknet/oapp/overview) - Building custom OApps * [OFT Overview](/v2/developers/starknet/oft/overview) - Token transfer patterns * [Configuration Guide](/v2/developers/starknet/configuration/dvn-executor-config) - DVN and Executor setup # Starknet Fundamentals for LayerZero Developers Source: https://docs.layerzero.network/v2/developers/starknet/technical-overview Deep dive into Starknet-specific concepts for LayerZero development including Cairo types, storage model, dispatcher patterns, and gas model. This page introduces the Starknet-specific concepts you need to understand before building LayerZero applications. If you're coming from EVM chains, this guide explains how Starknet differs and why LayerZero's implementation works the way it does. **What you'll learn**: * Cairo's `felt252` type system and `u256` representation * Starknet storage model and component-based composition * Transaction types, versions, and typed dispatcher calls * Multi-call patterns for atomic configuration * Resource bounds, fee estimation, and common gas pitfalls * Reentrancy protections and clear-then-execute behavior * Bytes32 address encoding for crosschain peers For complete protocol workflows with detailed code, see [Protocol Overview](/v2/developers/starknet/protocol-overview). For hands-on implementation, see [OApp](/v2/developers/starknet/oapp/overview) or [OFT](/v2/developers/starknet/oft/overview) guides. ## VM Architecture Starknet uses the Cairo programming language and a field-element-based type system. These fundamentals shape how LayerZero contracts represent addresses, amounts, and payloads. ### The felt252 Type Starknet's native type is `felt252` (field element), a \~251-bit unsigned integer: ```rust wrap theme={null} // felt252 is the native Cairo type let value: felt252 = 123; // Maximum value is approximately 2^251 // Operations are performed modulo a prime field ``` **Key Properties**: * Native to the STARK proof system (efficient proving) * Wraps around on overflow (unlike Solidity's revert behavior) * Can represent addresses, integers, and short strings ### Common Types ```rust wrap theme={null} // Unsigned integers (built on felt252) let a: u8 = 255; let b: u32 = 4294967295; let c: u64 = 18446744073709551615; let d: u128 = 340282366920938463463374607431768211455; let e: u256 = 0xffffffff_u256; // Two felt252 values internally // Signed integers let f: i32 = -100; let g: i128 = -1000000; // Boolean let flag: bool = true; // Contract Address (wrapper around felt252) let addr: ContractAddress = contract_address_const::<0x123>(); // ByteArray for dynamic bytes (like Solidity's bytes) let data: ByteArray = "Hello, World!"; ``` ### u256 Representation Unlike EVM's native 256-bit integers, Starknet represents `u256` as two `felt252` values: ```rust wrap theme={null} // u256 is stored as (low: u128, high: u128) let amount: u256 = 1000000000000000000_u256; // Conversion to/from felt252 requires care let as_felt: felt252 = amount.low.into(); // Only works if high == 0 ``` **Implications for LayerZero**: * Crosschain amount encoding must handle this difference * OFT uses `u64` for shared decimals to ensure compatibility ## Message Flow Overview LayerZero messages on Starknet flow through the Endpoint and verification system before reaching the destination OApp: * **Send**: OApp -> Endpoint -> message library -> workers (DVNs/Executor) * **Verify**: DVNs verify and submit to the receive library * **Receive**: Executor calls `lz_receive` on the destination OApp **Complete Protocol Details** For detailed send/verify/receive workflows with contract code and event flows, see [Protocol Overview](/v2/developers/starknet/protocol-overview). ## Transaction Execution Model Starknet uses distinct transaction types and typed dispatchers. These patterns determine how LayerZero contracts are deployed, called, and configured. ### Transaction Types Starknet has distinct transaction types for different operations: #### DECLARE Publishes contract code to the network: ```bash theme={null} sncast declare --contract-name MyContract ``` **Result**: `class_hash` - unique identifier for the contract code **When to use**: First time deploying a new contract version #### DEPLOY\_ACCOUNT Deploys an account contract: ```bash theme={null} sncast account deploy --name my_account ``` **Prerequisite**: The computed account address must be pre-funded **When to use**: Setting up a new wallet/signer #### INVOKE Executes contract functions: ```bash theme={null} sncast invoke --contract-address 0x... --function set_peer --arguments '...' ``` **When to use**: All regular contract interactions ### Transaction Versions * **v0/v1/v2**: Deprecated and unsupported on current Starknet networks * **v3**: Current transaction format with resource bounds (recommended) ```rust wrap theme={null} // INVOKE v3 includes resource bounds struct InvokeTransactionV3 { resource_bounds: ResourceBoundsMapping, // l1_gas, l2_gas, l1_data_gas limits tip: u64, // Priority tip // ... other fields } ``` ### Dispatcher Pattern Starknet doesn't support dynamic dispatch (no `delegatecall` equivalent). Instead, cross-contract calls use typed **dispatchers**. For more details, see the [Cairo Book: Dispatcher Pattern](https://book.cairo-lang.org/ch102-02-interacting-with-another-contract.html#the-dispatcher-pattern). #### Interface Definition ```rust wrap theme={null} #[starknet::interface] pub trait IEndpointV2 { fn send(ref self: TContractState, params: MessagingParams, refund_address: ContractAddress) -> MessageReceipt; fn quote(self: @TContractState, params: MessagingParams, sender: ContractAddress) -> MessagingFee; fn get_eid(self: @TContractState) -> u32; } ``` #### Generated Dispatcher The compiler generates a dispatcher for each interface: ```rust wrap theme={null} // Auto-generated by the compiler pub struct IEndpointV2Dispatcher { pub contract_address: ContractAddress, } impl IEndpointV2DispatcherTrait of IEndpointV2Dispatcher { fn send(self: IEndpointV2Dispatcher, params: MessagingParams, refund_address: ContractAddress) -> MessageReceipt { // Serializes params, calls contract, deserializes result } } ``` #### Using Dispatchers ```rust wrap theme={null} use layerzero::endpoint::interfaces::endpoint_v2::{IEndpointV2Dispatcher, IEndpointV2DispatcherTrait}; fn call_endpoint(endpoint_address: ContractAddress, params: MessagingParams) -> MessagingFee { let endpoint = IEndpointV2Dispatcher { contract_address: endpoint_address }; // Type-safe cross-contract call endpoint.quote(params, get_contract_address()) } ``` **Benefits**: * Compile-time type checking * Automatic serialization/deserialization * Clear error messages **vs EVM**: | EVM | Starknet | | ------------------------------------ | --------------------------- | | `interface.function{value: x}(args)` | `dispatcher.function(args)` | | Dynamic dispatch via address | Typed dispatcher | | `abi.encode/decode` | Automatic Serde | ### Multi-Call Transactions Multicall is implemented at the account-contract level: a single INVOKE can execute multiple calls atomically when the account supports it. Most major account implementations (Ready Wallet, formerly Argent; Braavos; OpenZeppelin Account) expose multicall by default. If an account contract does not implement multicall, batching is not available for that account. #### Batching with Account.execute ```typescript wrap theme={null} // Using starknet.js const calls = [ { contractAddress: oftAddress, entrypoint: 'set_peer', calldata: [dstEid, peerAddressLow, peerAddressHigh], }, { contractAddress: oftAddress, entrypoint: 'set_enforced_options', calldata: [...], }, { contractAddress: oftAddress, entrypoint: 'set_dvn_config', calldata: [...], }, ]; // All calls execute atomically const response = await account.execute(calls); ``` #### Benefits * **Atomicity**: All calls succeed or all fail * **Gas efficiency**: Single transaction overhead * **Configuration safety**: Set all config before enabling pathway #### LayerZero Configuration Pattern ```typescript wrap theme={null} // Recommended: Configure everything in one transaction const configCalls = [ // 1. Set library (optional) { contractAddress: oft, entrypoint: 'set_send_library', calldata: [...] }, // 2. Configure DVNs { contractAddress: oft, entrypoint: 'set_dvn_config', calldata: [...] }, // 3. Set enforced options { contractAddress: oft, entrypoint: 'set_enforced_options', calldata: [...] }, // 4. Set peer (LAST - enables pathway) { contractAddress: oft, entrypoint: 'set_peer', calldata: [...] }, ]; await account.execute(configCalls); ``` ## State Management Model Starknet contracts use a key-value storage model and component-based composition rather than inheritance. These patterns shape how LayerZero contracts store configuration and expose functionality. ### Contract Storage Model #### Storage Structure Starknet contracts use a key-value storage model with `felt252` keys: ```rust wrap theme={null} #[storage] struct Storage { // Simple values owner: ContractAddress, total_supply: u256, // Mappings balances: Map, allowances: Map<(ContractAddress, ContractAddress), u256>, // Component substorages #[substorage(v0)] erc20: ERC20Component::Storage, #[substorage(v0)] oapp_core: OAppCoreComponent::Storage, } ``` #### Storage Access ```rust wrap theme={null} use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; fn example(ref self: ContractState) { // Read let current_owner = self.owner.read(); let balance = self.balances.entry(some_address).read(); // Write self.owner.write(new_owner); self.balances.entry(some_address).write(new_balance); } ``` #### Storage Layout Storage keys are computed deterministically: * Simple variables: `sn_keccak(variable_name)` * Mappings: `h(h(variable_name), key1, key2, ...)` This is abstracted by the compiler, but understanding it helps with: * Debugging storage reads/writes * Computing storage proofs for crosschain verification ### Component System Cairo uses a **component system** instead of inheritance: #### Defining a Component ```rust wrap theme={null} #[starknet::component] pub mod OAppCoreComponent { #[storage] pub struct Storage { OAppCore_endpoint: ContractAddress, OAppCore_peers: Map, } #[event] #[derive(Drop, starknet::Event)] pub enum Event { PeerSet: PeerSet, } #[embeddable_as(OAppCoreImpl)] impl OAppCore> of IOAppCore> { fn set_peer(ref self: ComponentState, eid: u32, peer: Bytes32) { // Implementation } } } ``` #### Using Components in a Contract ```rust wrap theme={null} #[starknet::contract] mod MyOApp { use layerzero::oapps::oapp::oapp_core::OAppCoreComponent; use openzeppelin::access::ownable::OwnableComponent; // Declare components component!(path: OAppCoreComponent, storage: oapp_core, event: OAppCoreEvent); component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); // Embed implementations (exposes external functions) #[abi(embed_v0)] impl OAppCoreImpl = OAppCoreComponent::OAppCoreImpl; // Internal implementations (not exposed) impl OAppCoreInternalImpl = OAppCoreComponent::InternalImpl; #[storage] struct Storage { #[substorage(v0)] oapp_core: OAppCoreComponent::Storage, #[substorage(v0)] ownable: OwnableComponent::Storage, } #[event] #[derive(Drop, starknet::Event)] enum Event { #[flat] OAppCoreEvent: OAppCoreComponent::Event, #[flat] OwnableEvent: OwnableComponent::Event, } } ``` **vs Solidity Inheritance**: | Solidity | Cairo | | -------------------- | --------------------------------- | | `contract A is B, C` | `component!(path: B, ...)` | | `override` | Trait impl | | Diamond problem | No conflicts (explicit embedding) | ## Security & Permission Model Starknet's execution model affects how reentrancy is handled in LayerZero contracts. ### Reentrancy Model Unlike EVM, Starknet's execution model provides some inherent reentrancy protections: #### Sequential Execution Transactions are executed sequentially within a block, not concurrently. However, within a single transaction, reentrancy is still possible. #### ReentrancyGuard Component LayerZero contracts use OpenZeppelin's ReentrancyGuard: ```rust wrap theme={null} use openzeppelin::security::ReentrancyGuardComponent; component!(path: ReentrancyGuardComponent, storage: reentrancy_guard, event: ReentrancyGuardEvent); fn protected_function(ref self: ContractState) { self.reentrancy_guard.start(); // Acquire lock // Protected logic here // External calls are safe self.reentrancy_guard.end(); // Release lock } ``` #### Clear-Then-Execute Pattern The Endpoint uses this pattern for `lz_receive`: ```rust wrap theme={null} fn lz_receive(ref self: ContractState, ...) { // 1. Clear payload hash first (marks as executed) self._clear_payload(receiver, @origin, @payload); // 2. Transfer value native_token.transfer_from(executor, receiver, value); // 3. Execute callback (safe even if it calls back) receiver_dispatcher.lz_receive(origin, guid, message, executor, extra_data, value); } ``` ## Gas Model Starknet's fee model differs from EVM and uses explicit resource bounds. ### Gas Model | Resource | Description | | --------------- | -------------------------- | | **L1 Gas** | Cost for DA on Ethereum L1 | | **L2 Gas** | Compute cost on Starknet | | **L1 Data Gas** | Calldata size on L1 | ### Setting Resource Bounds ```bash theme={null} # Using sncast (fee cap; tooling derives resource bounds) sncast invoke \ --contract-address 0x... \ --function transfer \ --network sepolia \ --arguments '0x123, 1000' ``` ### Estimating Fees ```typescript wrap theme={null} // Using starknet.js const { suggestedMaxFee } = await account.estimateInvokeFee({ contractAddress: oftAddress, entrypoint: 'send', calldata: [...], }); // Add buffer for safety and use as a fee cap const maxFee = suggestedMaxFee * 1.2n; ``` ### Common Issues | Error | Cause | Solution | | ---------------------- | ----------------------- | ------------------------------------------------- | | "Insufficient max fee" | Resource bounds too low | Increase fee cap (`--max-fee`) or resource bounds | | "Insufficient balance" | Account underfunded | Add STRK/ETH | | "Transaction reverted" | Contract logic error | Check calldata | ## Key Starknet Concepts for LayerZero Address encoding is critical for crosschain peer verification. ### Address Encoding for Crosschain #### Bytes32 for Cross-VM Compatibility LayerZero uses `Bytes32` for addresses to support different address sizes: ```rust wrap theme={null} // Starknet addresses (felt252) -> Bytes32 use lz_utils::bytes::{Bytes32, ContractAddressIntoBytes32}; let starknet_addr: ContractAddress = ...; let as_bytes32: Bytes32 = starknet_addr.into(); // EVM addresses (20 bytes) come padded to 32 bytes let evm_peer: Bytes32 = Bytes32 { value: 0x000000000000000000000000abcdef... }; ``` #### Setting Peers ```rust wrap theme={null} fn set_peer(ref self: ContractState, eid: u32, peer: Bytes32) { self.ownable.assert_only_owner(); self.peers.write(eid, peer); } // When receiving, verify the peer fn _lz_receive(ref self: ..., origin: Origin, ...) { let expected_peer = self.peers.read(origin.src_eid); assert(origin.sender == expected_peer, 'invalid peer'); // Process message } ``` ## Key Takeaways * `felt252` and `u256` encoding affect how LayerZero represents amounts and addresses. * Storage and components replace inheritance; configuration lives in structured storage. * Typed dispatchers provide safe cross-contract calls without dynamic dispatch. * Multicall enables atomic configuration before opening pathways. * Resource bounds and fee estimation require explicit handling on Starknet. * Clear-then-execute prevents reentrancy during `lz_receive`. * Bytes32 encoding standardizes peer addresses across chains. ## Next Steps * [OApp Overview](/v2/developers/starknet/oapp/overview) - Building OApps * [OFT Overview](/v2/developers/starknet/oft/overview) - Token transfers * [Technical Reference](/v2/developers/starknet/technical-reference/starknet-guidance) - Toolchain guide * [Troubleshooting](/v2/developers/starknet/troubleshooting/common-errors) - Common errors # Starknet Development Guidance Source: https://docs.layerzero.network/v2/developers/starknet/technical-reference/starknet-guidance Testing, networking, authority management, constraints, and upgradeability for LayerZero contracts on Starknet. This guide covers testing, networking, authority management, constraints, and upgradeability for LayerZero contracts on Starknet. For toolchain installation and account setup, see [Getting Started](/v2/developers/starknet/getting-started). For project scaffolding and deployment steps, see the OApp and OFT overviews. ## Project Scaffolding, Build, and Deploy For project layout, `Scarb.toml`, build, declare/deploy, and verification steps, see: * [OApp Overview](/v2/developers/starknet/oapp/overview) * [OFT Overview](/v2/developers/starknet/oft/overview) *** ## Testing ### Running Tests ```bash theme={null} # Run all tests snforge test # Run specific test snforge test test_send_tokens # Run tests with detailed traces snforge test --trace-verbosity detailed # Run tests matching pattern snforge test test_oft ``` ### Test Structure ```rust wrap theme={null} use snforge_std::{ declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address, stop_cheat_caller_address, }; #[test] fn test_oft_send() { // Deploy contract let contract = declare("MyOFT").unwrap().contract_class(); let constructor_calldata = array![ endpoint.into(), owner.into(), native_token.into(), ]; let (contract_address, _) = contract.deploy(@constructor_calldata).unwrap(); // Create dispatcher let oft = IOFTDispatcher { contract_address }; // Cheat caller for owner operations start_cheat_caller_address(contract_address, owner); // Set peer oft.set_peer(30101, peer_address); // Verify let stored_peer = oft.get_peer(30101); assert(stored_peer == peer_address, 'peer mismatch'); stop_cheat_caller_address(contract_address); } ``` ### Test Utilities ```rust wrap theme={null} use snforge_std::{ start_cheat_caller_address, // Mock caller stop_cheat_caller_address, start_cheat_block_timestamp, // Mock block time stop_cheat_block_timestamp, spy_events, // Capture events EventSpyAssertionsTrait, }; ``` *** ## Network Configuration **Endpoint IDs and addresses** For up-to-date Starknet endpoint IDs and LayerZero contract addresses, use [V2 Protocol Contracts](/v2/deployments/deployed-contracts) or query the [Endpoint Metadata API](https://metadata.layerzero-api.com/v1/metadata). ### RPC Endpoints | Network | URL | | ------- | ------------------------------------------------- | | Mainnet | `` | | Sepolia | `` | | Alchemy | `https://starknet-mainnet.g.alchemy.com/v2/` | | Infura | `https://starknet-mainnet.infura.io/v3/` | ### LayerZero Contract Addresses Check the [LayerZero Deployments](/v2/deployments/deployed-contracts) page for current addresses: | Contract | Address | | ------------- | -------------------- | | EndpointV2 | See deployments page | | ULN302 | See deployments page | | LayerZero DVN | See deployments page | | Executor | See deployments page | *** ## Public Key vs Address Starknet accounts are smart contracts (native account abstraction), so an account address is a contract address, not a public-key-derived identifier. There is no default deterministic link between the key(s) controlling an account and its address, and keys can be rotated by the account logic. See [Accounts](https://docs.starknet.io/learn/protocol/accounts) and [Account keys and addresses derivation standard](https://community.starknet.io/t/account-keys-and-addresses-derivation-standard/1230). Practical takeaways: * Use the account address anywhere an API expects a `ContractAddress`. * Treat the public key as signer metadata owned by the account contract, not as the account identifier. *** ## Authority Management ### Ownership Pattern LayerZero Starknet contracts use OpenZeppelin's `OwnableComponent`: ```rust wrap theme={null} // Transfer ownership fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) { self.ownable.transfer_ownership(new_owner); } // Renounce ownership (irreversible!) fn renounce_ownership(ref self: ContractState) { self.ownable.renounce_ownership(); } ``` ### Delegate Pattern Set a delegate for configuration without transferring ownership: ```rust wrap theme={null} // Set delegate via Endpoint fn set_delegate(ref self: ContractState, delegate: ContractAddress) { let endpoint = IEndpointV2Dispatcher { contract_address: self.oapp_core.OAppCore_endpoint.read() }; endpoint.set_delegate(delegate); } ``` Delegates can: * Set library configurations * Update DVN settings * Manage pathway configurations Delegates cannot: * Transfer ownership * Set peers (owner only) *** ## Technical Constraints ### Contract Size Starknet has contract size limits (see the [chain info cheat sheet](https://docs.starknet.io/learn/cheatsheets/chain-info) for current values): | Metric | Limit | | ------------------------------------- | --------------- | | Max contract bytecode size | 81,920 felts | | Max contract class size (Sierra file) | 4,089,446 bytes | | Contract classes per tx | 1 | **Mitigation**: Split large contracts into components or use libraries. ### Storage | Constraint | Details | | ------------- | -------------------------------- | | Storage key | `felt252` | | Storage value | `felt252` | | Complex types | Serialized across multiple slots | ### Compute | Resource | Notes | | -------- | --------------------------- | | Steps | Varies by transaction type | | Builtins | Pedersen, Range Check, etc. | | Memory | Managed by Cairo VM | *** ## Upgradeability ### Upgradeable Contracts Use OpenZeppelin's `UpgradeableComponent`: ```rust wrap theme={null} use openzeppelin::upgrades::UpgradeableComponent; component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); #[external(v0)] fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { self.ownable.assert_only_owner(); self.upgradeable.upgrade(new_class_hash); } ``` ### Upgrade Process 1. **Declare** new contract version 2. **Call** `upgrade(new_class_hash)` on existing contract 3. **Verify** new implementation ```bash theme={null} # Declare new version sncast declare --contract-name MyOFTv2 # Upgrade existing contract sncast invoke \ --contract-address \ --function upgrade \ --network sepolia \ --arguments '' ``` Ensure storage layout compatibility between versions! *** ## Common Commands Reference ### Scarb Commands ```bash theme={null} scarb build # Build contracts scarb clean # Clean build artifacts scarb fmt # Format code scarb test # Run Cairo tests (non-blockchain) ``` ### snforge Commands ```bash theme={null} snforge test # Run all tests snforge test # Run specific test snforge test -v # Verbose output snforge test --coverage # Generate coverage ``` ### sncast Commands ```bash theme={null} sncast account create # Create new account sncast account deploy # Deploy account contract sncast declare # Declare contract class sncast deploy # Deploy contract instance sncast invoke # Call external function sncast call # Call view function sncast tx-status # Check transaction status ``` *** ## Class Hash Mismatch Errors When declaring contracts, you may encounter: ``` Error: Mismatch compiled class hash for class with hash 0x... Actual: 0x..., Expected: 0x... ``` This error occurs when the CASM (Cairo Assembly) hash computed locally doesn't match what the network expects. ### Common Causes | Cause | Solution | | ------------------------------------- | ----------------------------------------------------------- | | **Scarb version mismatch** | Use Scarb 2.14.0 (matches `starknet = "2.14.0"` dependency) | | **Starknet Foundry version mismatch** | Use snfoundry 0.53.0 with Scarb 2.14.0 | | **Stale build artifacts** | Run `scarb clean && scarb build` before declaring | | **RPC version incompatibility** | Use RPC v0.9.0+ (see snfoundry.toml section above) | ### Version Compatibility Matrix | Scarb | Cairo | Starknet Foundry | RPC Version | | ------ | ------ | ---------------- | ---------------- | | 2.14.0 | 2.14.0 | 0.53.0 | v0.9.0 - v0.10.0 | | 2.13.1 | 2.13.1 | 0.49.0 | v0.9.0 | Always match your `starknet` dependency version in `Scarb.toml` with your installed Scarb version. Run `scarb --version` to check. ### Debugging Steps 1. **Verify versions match:** ```bash theme={null} scarb --version # Should show 2.14.0 sncast --version # Should show 0.53.0 ``` 2. **Clean and rebuild:** ```bash theme={null} scarb clean rm -rf Scarb.lock scarb build ``` 3. **Check Scarb.toml dependency:** ```toml theme={null} [dependencies] starknet = "2.14.0" # Must match scarb version ``` 4. **Verify RPC version:** ```bash theme={null} curl -X POST "" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"starknet_specVersion","params":[],"id":1}' # Should return "0.9.0" or "0.10.0" ``` *** ## Next Steps * [OApp Overview](/v2/developers/starknet/oapp/overview) - Building OApps * [OFT Overview](/v2/developers/starknet/oft/overview) - Token transfers * [Configuration Guide](/v2/developers/starknet/configuration/dvn-executor-config) - Security setup * [Troubleshooting](/v2/developers/starknet/troubleshooting/common-errors) - Common errors # Common Errors on Starknet Source: https://docs.layerzero.network/v2/developers/starknet/troubleshooting/common-errors Troubleshoot common errors when developing and deploying LayerZero contracts on Starknet including account, declare, deploy, and crosschain errors. This guide catalogs common errors encountered when developing and deploying LayerZero contracts on Starknet, with explanations and solutions. ## Account Errors ### Account Not Deployed **Error:** ``` Account contract at address 0x... is not deployed ``` **Cause:** You're trying to use an account that hasn't been deployed yet. On Starknet, accounts are smart contracts that must be deployed before use. **Solution:** ```bash theme={null} # 1. Check if account is created sncast account list # 2. Fund the computed address with STRK/ETH # 3. Deploy the account sncast account deploy \ --name your_account \ --url ``` *** ### Insufficient Balance for Fee **Error:** ``` Insufficient balance for fee. Required: X, Available: Y ``` **Cause:** Your account contract doesn't have enough STRK or ETH to pay transaction fees. **Solution:** 1. Fund your account with STRK or ETH 2. For testnet, use the [Starknet Faucet](https://starknet-faucet.vercel.app/) 3. For mainnet, bridge funds via [Starkgate](https://starkgate.starknet.io/) *** ### Account Prefunding Required **Error:** ``` Transaction reverted: Insufficient funds for transaction ``` **Cause:** Before deploying an account contract, the **computed address** must be funded. Starknet computes the address deterministically, so you can fund it before deployment. **Solution:** ```bash theme={null} # 1. Create account (doesn't deploy yet) sncast account create --name my_account --type oz # Output shows: Address: 0x123... # 2. Fund that address BEFORE deploying # 3. Then deploy sncast account deploy --name my_account ``` *** ## Declare Errors ### Class Already Declared **Error:** ``` Class with hash 0x... is already declared ``` **Cause:** You're trying to declare a contract class that already exists on the network. **Solution:** ```bash theme={null} # Use the existing class_hash instead sncast deploy --class-hash --arguments '...' # Or if you modified the contract, rebuild to get a new hash scarb clean && scarb build ``` *** ### Compilation Failed **Error:** ``` error: could not compile `my_contract` due to previous errors ``` **Cause:** Cairo compilation errors in your contract. **Solution:** ```bash theme={null} # Check compilation errors scarb build 2>&1 | less # Common issues: # - Missing imports # - Type mismatches # - Trait bound errors ``` *** ## Deploy Errors ### Wrong Owner (UDC Footgun) **Error:** After deployment, the owner is set to an unexpected address (the UDC address). **Cause:** When deploying via the Universal Deployer Contract (UDC), `get_caller_address()` in the constructor returns the UDC address, not your account. **Wrong pattern:** ```rust wrap theme={null} #[constructor] fn constructor(ref self: ContractState, endpoint: ContractAddress) { // BUG: This sets UDC as owner! self.ownable.initializer(get_caller_address()); } ``` **Solution:** ```rust wrap theme={null} #[constructor] fn constructor( ref self: ContractState, endpoint: ContractAddress, owner: ContractAddress, // Pass owner explicitly ) { self.ownable.initializer(owner); } ``` *** ### Invalid Constructor Calldata **Error:** ``` Entry point EntryPointSelector(0x...) not found in contract ``` or ``` Failed to deserialize param ``` **Cause:** Constructor calldata doesn't match the expected parameters. **Solution:** 1. Verify parameter order matches constructor signature 2. Check ByteArray encoding (length, data, pending\_word, pending\_len) 3. Verify felt252 encoding for addresses and numbers ```bash theme={null} # Example: constructor(endpoint, owner, native_token) sncast deploy \ --class-hash 0x... \ --network sepolia \ --arguments ', , ' ``` *** ### Resource Bounds Exceeded **Error:** ``` Insufficient max fee. Required: X, Provided: Y ``` or ``` Transaction execution failed: resource bounds exceeded ``` **Cause:** The transaction requires more resources than your specified limits. **Solution:** ```bash theme={null} # Increase fee cap (tooling derives resource bounds) sncast deploy \ --class-hash 0x... \ --network sepolia \ --arguments '...' \ --max-fee # Fee cap used to derive resource bounds ``` > 1 STRK = 10^18 fri *** ## Configuration Errors ### Peer Not Set **Error:** ``` Assertion failed: peer not set ``` **Cause:** Attempting to send a message to a chain without a configured peer. **Solution:** ```bash theme={null} # Set peer for destination chain sncast invoke \ --contract-address \ --function set_peer \ --network sepolia \ --arguments ', (, )' ``` Remember: Peers must be set bidirectionally on both chains. *** ### Invalid Peer **Error:** ``` Assertion failed: invalid peer ``` **Cause:** Received a message from an address that doesn't match the configured peer. **Causes:** 1. Peer set incorrectly on either chain 2. Using Object ID instead of contract address 3. Peer not set at all on sending chain **Solution:** ```rust wrap theme={null} // Verify peer configuration let peer = oft.get_peer(src_eid); // Compare with expected remote contract address ``` *** ### Library Not Set **Error:** ``` No library configured for eid: X ``` **Cause:** No send or receive library configured for the pathway. **Solution:** Use default libraries or set custom ones: ```rust wrap theme={null} // Usually defaults are sufficient // If you need custom libraries: endpoint.set_send_library(oapp, dst_eid, library_address); ``` *** ## Serialization Errors ### Felt Overflow **Error:** ``` Value does not fit in felt252 ``` **Cause:** Attempting to store a value larger than \~2^251 in a felt252. **Solution:** ```rust wrap theme={null} // Use u256 for large numbers let big_value: u256 = 1000000000000000000000000_u256; // When encoding for crosschain: // u256 is serialized as two felt252 (low, high) ``` *** ### ByteArray Encoding Error **Error:** ``` Failed to deserialize ByteArray ``` **Cause:** Incorrect ByteArray encoding in calldata. **ByteArray structure:** ``` [ pending_word_len, // Number of bytes in pending_word data_len, // Number of 31-byte chunks data[0..data_len], // Full 31-byte chunks pending_word // Remaining bytes (< 31) ] ``` **Solution:** For short strings (\< 31 bytes), use simplified encoding with `--arguments`: ```bash theme={null} # "MyToken" as a ByteArray using --arguments sncast invoke ... --arguments '"MyToken"' ``` When using `--arguments`, sncast handles ByteArray encoding automatically. You can pass strings directly in quotes. *** ## Execution Errors ### Unauthorized **Error:** ``` Assertion failed: unauthorized ``` or ``` Caller is not the owner ``` **Cause:** Calling an owner-only function from a non-owner account. **Solution:** ```bash theme={null} # Ensure you're using the owner account sncast invoke \ --account owner_account \ --contract-address \ --function set_peer \ --network sepolia \ --arguments ', (, )' ``` *** ### Contract Not Pausable **Error:** ``` Assertion failed: contract is paused ``` **Cause:** Calling a function on a paused contract. **Solution:** ```bash theme={null} # Unpause the contract (owner or PAUSE_MANAGER_ROLE) sncast invoke \ --account admin \ --contract-address \ --function unpause ``` *** ### Rate Limit Exceeded **Error:** ``` Rate limit exceeded for eid: X ``` **Cause:** Transfer volume exceeds configured rate limits (OFTMintBurnAdapter). **Solution:** ```rust wrap theme={null} // Check current rate limit config let limit = oft.get_rate_limit(eid, direction); // Increase limits if needed (RATE_LIMITER_MANAGER_ROLE) oft.set_rate_limits(new_limits, direction); ``` *** ## Crosschain Errors ### Insufficient Fee **Error:** ``` Insufficient fee. Required native: X, Supplied: Y ``` **Cause:** Not enough tokens approved or sent for LayerZero messaging fees. **Solution:** ```rust wrap theme={null} // 1. Quote the fee first let fee = oft.quote_send(send_param, false); // 2. Approve sufficient amount native_token.approve(oft_address, fee.native_fee + buffer); // 3. Send with correct fee oft.send(send_param, fee, refund_address); ``` *** ### Slippage Exceeded **Error:** ``` Slippage exceeded. Received: X, Minimum: Y ``` **Cause:** After dust removal and fees, the received amount is less than `min_amount_ld`. **Solution:** ```rust wrap theme={null} // Quote first to see actual amounts let quote = oft.quote_oft(send_param); // Adjust min_amount_ld to account for: // - Dust removal // - Fees (if using OFTMintBurnAdapter) let safe_min = quote.receipt.amount_received_ld * 99 / 100; // 1% buffer ``` *** ### Message Execution Failed **Error (on LayerZero Scan):** ``` LzReceiveAlert: execution failed ``` **Cause:** The destination contract's `_lz_receive` reverted. **Debug Steps:** 1. Check LayerZero Scan for the transaction details 2. Look for the `LzReceiveAlert` event 3. Decode the `reason` array for error details 4. Simulate the transaction locally **Common causes:** * Insufficient gas (increase enforced options) * Contract paused on destination * Rate limit exceeded * Application logic error *** ## Build Errors ### Missing Dependencies **Error:** ``` error: cannot find crate `layerzero` ``` **Cause:** Dependencies not properly configured in Scarb.toml. **Solution:** ```toml wrap theme={null} [dependencies] starknet = "2.14.0" openzeppelin = "2.0.0" lz_utils = { path = "./node_modules/@layerzerolabs/protocol-starknet-v2/libs/lz_utils" } layerzero = { path = "./node_modules/@layerzerolabs/protocol-starknet-v2/layerzero" } ``` Install the LayerZero Starknet package first: `npm install @layerzerolabs/protocol-starknet-v2` *** ### Version Mismatch **Error:** ``` Incompatible Cairo version. Expected: 2.8.2, Found: 2.7.0 ``` **Cause:** Scarb/Cairo version doesn't match project requirements. **Solution:** ```bash theme={null} # Check current version scarb --version # Install correct version (check Scarb.toml for required version) asdf install scarb 2.14.0 asdf local scarb 2.14.0 ``` *** ## Debugging Tips ### 1. Use LayerZero Scan Track your crosschain transactions at [LayerZero Scan](https://layerzeroscan.com/). ### 2. Check Transaction Status ```bash theme={null} sncast tx-status ``` ### 3. Simulate Locally Test your logic with `snforge test` before deploying. ### 4. Increase Verbosity ```bash theme={null} sncast invoke ... --json 2>&1 | jq . ``` ### 5. Check Events Query contract events via block explorer or RPC: ```bash theme={null} # Using starkli or similar tools starkli events --from-block 12345 --to-block latest --address ``` *** ## Next Steps * [FAQ](/v2/developers/starknet/troubleshooting/faq) - Frequently asked questions * [Protocol Overview](/v2/developers/starknet/protocol-overview) - Message lifecycle * [Configuration Guide](/v2/developers/starknet/configuration/dvn-executor-config) - Security setup * [Discord](https://discord.com/invite/ktbvm8Nkcr) - Community support # Starknet FAQ Source: https://docs.layerzero.network/v2/developers/starknet/troubleshooting/faq Frequently asked questions about developing LayerZero applications on Starknet including account abstraction, OFT decimals, and crosschain transfers. Frequently asked questions about developing LayerZero applications on Starknet. ## General ### Why does Starknet use account contracts instead of EOAs? Starknet implements **native account abstraction**, meaning all accounts are smart contracts. This provides: * **Flexible signature validation**: Support for different signature schemes * **Custom transaction logic**: Batching, session keys, social recovery * **Gas abstraction**: Pay fees in different tokens * **Enhanced security**: Multi-sig, spending limits, etc. Before you can deploy any contract, you must first deploy and fund an account contract (for example, Ready Wallet, formerly Argent; Braavos; or OpenZeppelin Account). *** ### What's the difference between class\_hash and contract\_address? | Concept | Description | Analogy | | --------------------- | ------------------------------------------- | ----------------------- | | **class\_hash** | Unique identifier for contract **code** | Like a class/template | | **contract\_address** | Unique identifier for contract **instance** | Like an object/instance | ``` class_hash = 0x123... (the "OFT" code template) │ ├── contract_address = 0xaaa... (OFT instance for Token A) ├── contract_address = 0xbbb... (OFT instance for Token B) └── contract_address = 0xccc... (OFT instance for Token C) ``` Multiple contracts can share the same `class_hash` but have different addresses and state. *** ### Do I need to deploy my own Endpoint? **No.** LayerZero deploys and maintains the Endpoint contract on each supported chain. You only need to: 1. Deploy your OApp/OFT contract 2. Reference the existing Endpoint address in your constructor 3. Configure your security settings (DVNs, peers, etc.) Check [LayerZero Deployments](/v2/deployments/deployed-contracts) for the official Endpoint address. *** ### What's the difference between STRK and ETH on Starknet? Starknet supports two fee tokens: | Token | Purpose | Notes | | -------- | --------------------- | ------------------------------ | | **STRK** | Native Starknet token | Primary fee token on Starknet | | **ETH** | Bridged Ethereum | Still supported as a fee token | LayerZero messaging fees are paid in the native token configured by your OApp (usually STRK on Starknet). *** ## Development ### How do I encode addresses for crosschain messages? LayerZero uses `Bytes32` for addresses to support different address sizes across VMs: ```rust wrap theme={null} // Starknet address (felt252) → Bytes32 let starknet_addr: ContractAddress = ...; let as_bytes32: Bytes32 = starknet_addr.into(); // EVM address (20 bytes) → Bytes32 (left-padded with zeros) // 0xABCDEF... becomes 0x000000000000000000000000ABCDEF... let evm_peer = Bytes32 { value: 0x000000000000000000000000_<20_BYTE_EVM_ADDRESS> }; ``` *** ### How do I batch multiple configuration calls? Multicall is implemented at the account-contract level: a single INVOKE can execute multiple calls atomically when the account supports it. Most major account implementations (Ready Wallet, formerly Argent; Braavos; OpenZeppelin Account) expose multicall by default. If an account contract does not implement multicall, batching is not available for that account. ```typescript wrap theme={null} // Using starknet.js const calls = [ { contractAddress: oft, entrypoint: 'set_enforced_options', calldata: [...] }, { contractAddress: oft, entrypoint: 'set_dvn_config', calldata: [...] }, { contractAddress: oft, entrypoint: 'set_peer', calldata: [...] }, // Last! ]; await account.execute(calls); // All execute atomically ``` This is useful for configuring all settings in one transaction, ensuring the pathway isn't opened until everything is ready. *** ### What is the shared decimals limit? OFTs use **shared decimals** (default: 6) to maintain consistency across chains with different token decimal precision: | Local Decimals | Shared Decimals | Conversion Rate | Max Precision | | -------------- | --------------- | --------------- | ------------- | | 18 | 6 | 10^12 | 0.000001 | | 8 | 6 | 10^2 | 0.000001 | | 6 | 6 | 1 | 0.000001 | **Implications:** * Amounts smaller than the conversion rate become "dust" and are removed * Always use `quote_oft` to see exact received amounts before sending *** ### Why is my constructor setting the wrong owner? When deploying via the Universal Deployer Contract (UDC), `get_caller_address()` returns the UDC address, not your account. **Wrong:** ```rust wrap theme={null} #[constructor] fn constructor(ref self: ContractState, endpoint: ContractAddress) { self.ownable.initializer(get_caller_address()); // Returns UDC! } ``` **Correct:** ```rust wrap theme={null} #[constructor] fn constructor( ref self: ContractState, endpoint: ContractAddress, owner: ContractAddress, // Pass explicitly ) { self.ownable.initializer(owner); } ``` *** ### How do I check my OApp configuration? Query configuration via the Endpoint or your OApp: ```rust wrap theme={null} // Check peer let peer = oapp.get_peer(eid); // Check delegate let delegate = endpoint.get_delegate(oapp_address); // Check library let send_lib = endpoint.get_send_library(oapp_address, dst_eid); // Check enforced options let options = oapp.get_enforced_options(dst_eid, msg_type); ``` Or use block explorers like [Voyager](https://voyager.online/) or [Starkscan](https://starkscan.co/). *** ## Deployment ### What tooling do I use to deploy Starknet contracts? Use **Starknet Foundry** (`sncast`): ```bash theme={null} # Declare (publish code) sncast declare --contract-name MyOFT # Deploy (create instance) sncast deploy --class-hash 0x... --arguments '...' ``` *** ### Can I use Hardhat or Foundry (EVM) for Starknet? No. Starknet uses Cairo, not Solidity. You need Starknet-specific tools: | EVM Tool | Starknet Equivalent | | -------- | ----------------------------------- | | Hardhat | Scarb + sncast | | Foundry | Starknet Foundry (sncast + snforge) | | Remix | N/A (use Scarb locally) | *** ### How do I verify my contract? Use `sncast verify` or block explorers: **Using sncast (recommended):** ```bash theme={null} sncast verify \ --class-hash \ --contract-name MyOFT \ --verifier voyager \ --network sepolia ``` **Using block explorers:** 1. **Voyager**: Go to contract → "Verify & Publish" 2. **Starkscan**: Go to contract → "Verify Contract" Upload your source files or provide a GitHub link. *** ## Crosschain ### How long do crosschain transfers take? Transfer time depends on: 1. **Source chain finality**: Time for DVNs to verify 2. **DVN verification**: Usually 1-5 minutes after finality 3. **Executor delivery**: Near-instant after verification Typical Starknet → EVM: 10-30 minutes Typical EVM → Starknet: 5-15 minutes Track your transfer on [LayerZero Scan](https://layerzeroscan.com/). *** ### Why did my crosschain message fail? Common reasons: 1. **Insufficient gas**: Increase enforced options 2. **Peer not set**: Configure bidirectional peers 3. **Contract paused**: Unpause the destination contract 4. **Rate limit exceeded**: Check OFTMintBurnAdapter limits 5. **Application error**: Bug in `_lz_receive` logic Check [LayerZero Scan](https://layerzeroscan.com/) for the `LzReceiveAlert` event details. *** ### Can I retry a failed message? If a message failed execution (not verification), you can: 1. **Fix the issue** on the destination contract 2. **Use recovery operations** if needed: * `clear`: Clear a blocking message * `nilify`: Reset verification * `skip`: Skip unverified message See [Protocol Overview - Recovery Operations](/v2/developers/starknet/protocol-overview#recovery-operations). *** ## Security ### What are the default security settings? If you don't configure custom settings: | Setting | Default | | --------------- | ----------------------- | | DVN | LayerZero Labs DVN | | Executor | LayerZero Labs Executor | | Send Library | ULN302 | | Receive Library | ULN302 | *** ### Should I use multiple DVNs? **Recommended for production.** Multiple DVNs provide: * Increased security (multiple independent verifiers) * Resilience (no single point of failure) * Trust minimization Example dual DVN setup using the TypeScript SDK: ```typescript wrap theme={null} import {encodeUlnConfig} from '@layerzerolabs/lz-v2-protocol-starknet'; const config = encodeUlnConfig({ confirmations: 15, has_confirmations: true, required_dvns: [LAYERZERO_DVN, PARTNER_DVN], // Sorted ascending has_required_dvns: true, optional_dvns: [], optional_dvn_threshold: 0, has_optional_dvns: false, }); ``` *** ### What's the difference between owner and delegate? | Role | Permissions | Use Case | | ------------ | ---------------------------------- | ---------------------- | | **Owner** | Full control (peers, ownership) | Long-term custody | | **Delegate** | Configuration only (DVNs, options) | Operational management | Delegates can manage day-to-day configuration without having power to set peers or transfer ownership. *** ## Costs ### What fees are involved in crosschain transfers? 1. **Source chain gas**: Pay for the `send` transaction 2. **LayerZero fees**: DVN and Executor fees (quoted via `quote_send`) 3. **Destination gas**: Paid by Executor, funded by step 2 Use `quote_send` to get the total LayerZero fee before sending. *** ### Why are my fees higher than expected? Fee factors: * **Options**: Higher gas limits = higher fees * **Message size**: Larger payloads cost more * **Destination chain**: Different chains have different costs * **DVN count**: More DVNs = higher verification costs Optimize by: * Using appropriate gas limits (not excessive) * Minimizing message payload size * Using efficient encoding *** ## Troubleshooting ### Where can I get help? 1. **Documentation**: You're here! Check other sections. 2. **LayerZero Scan**: Track and debug transactions 3. **Discord**: [LayerZero Discord](https://discord.com/invite/ktbvm8Nkcr) 4. **GitHub**: Report issues on the relevant repository *** ### How do I debug a transaction? 1. **Get transaction hash** from sncast output 2. **Check status**: ```bash theme={null} sncast tx-status ``` 3. **View on explorer**: Voyager or Starkscan 4. **Check events**: Look for error events 5. **Simulate locally**: Use `snforge test` *** ## Additional Resources * [Getting Started](/v2/developers/starknet/getting-started) * [OApp Overview](/v2/developers/starknet/oapp/overview) * [OFT Overview](/v2/developers/starknet/oft/overview) * [Common Errors](/v2/developers/starknet/troubleshooting/common-errors) * [Starknet Documentation](https://docs.starknet.io/) * [The Cairo Book](https://book.cairo-lang.org/) # DVN and Executor Configuration on Stellar Source: https://docs.layerzero.network/v2/developers/stellar/configuration/dvn-executor-config Configure the Decentralized Verifier Network (DVN) and Executor for your LayerZero OApp on Stellar. Set up message verification, execution, and security parameters. This guide covers how to configure the security and execution stack for your LayerZero OApp or OFT on Stellar. ## Overview Configuring a LayerZero application on Stellar involves these steps: 1. **Configure DVN** -- set verification parameters (required DVNs, optional DVNs, confirmations) 2. **Configure Executor** -- set execution parameters (max message size, executor address) 3. **Set Enforced Options** -- define minimum execution options per destination ## DVN Configuration DVNs (Decentralized Verifier Networks) verify crosschain messages. Configuration is done via the ULN-302 message library through the endpoint's `set_config` function. ### ULN Config Structure ```rust wrap theme={null} struct UlnConfig { confirmations: u64, // Block confirmations required required_dvns: Vec
, // DVNs that MUST ALL verify optional_dvns: Vec
, // Pool of optional DVNs optional_dvn_threshold: u32, // How many optional DVNs must verify } ``` | Field | Description | Default Behavior | | ------------------------ | -------------------------------------------------------- | -------------------------------------------------- | | `confirmations` | Source chain block confirmations before DVN verification | Uses default if `use_default_confirmations = true` | | `required_dvns` | All listed DVNs must verify every message | Uses default if `use_default_required_dvns = true` | | `optional_dvns` | Pool of additional DVNs | Uses default if `use_default_optional_dvns = true` | | `optional_dvn_threshold` | Minimum optional DVNs that must verify | Must be ≤ `optional_dvns.len()` | The effective config must have at least one DVN — either `required_dvns` must be non-empty, or `optional_dvn_threshold` must be greater than 0. ### Configure Send DVN Set the DVN configuration for outbound messages: ```bash wrap theme={null} stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ set_config \ --caller \ --oapp \ --lib \ --params '[{"eid": , "config_type": 2, "config": ""}]' ``` Config type `2` = `CONFIG_TYPE_SEND_ULN`. ### Configure Receive DVN Set the DVN configuration for inbound messages: ```bash wrap theme={null} stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ set_config \ --caller \ --oapp \ --lib \ --params '[{"eid": , "config_type": 3, "config": ""}]' ``` Config type `3` = `CONFIG_TYPE_RECEIVE_ULN`. ### OApp ULN Config When setting per-OApp config, use `OAppUlnConfig` which includes flags to fall back to defaults: ```rust wrap theme={null} struct OAppUlnConfig { use_default_confirmations: bool, use_default_required_dvns: bool, use_default_optional_dvns: bool, uln_config: UlnConfig, } ``` Set a field's `use_default_*` flag to `true` to inherit the network-wide default for that field, even if you customize other fields. When a `use_default_*` flag is `true`, the corresponding config values **must** be zero or empty. For example, if `use_default_confirmations` is `true`, `confirmations` must be `0`. If `use_default_required_dvns` is `true`, `required_dvns` must be empty. Violating this constraint will cause the transaction to fail. ## Executor Configuration The Executor delivers verified messages to the destination OApp. Configuration is also done via `set_config`: ```rust wrap theme={null} struct OAppExecutorConfig { max_message_size: u32, // 0 = use default executor: Option
, // None = use default } ``` ```bash wrap theme={null} stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ set_config \ --caller \ --oapp \ --lib \ --params '[{"eid": , "config_type": 1, "config": ""}]' ``` Config type `1` = `CONFIG_TYPE_EXECUTOR`. The `config` field in `SetConfigParam` is XDR-encoded bytes. Soroban contract types (`OAppUlnConfig`, `OAppExecutorConfig`) must be serialized to XDR before passing to `set_config`. Use the Stellar SDK's `toXDR()` method or the `stellar-xdr` Rust crate to encode these structs. ## Setting Enforced Options Enforced options define the **minimum execution parameters** for outbound messages. They are combined with any caller-provided options: ```bash wrap theme={null} stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ set_enforced_options \ --options '[{"eid": , "msg_type": 1, "options": ""}]' \ --operator ``` The `options` field is `Option` — pass `null` to remove enforced options for a given eid/msg\_type combination. | Message Type | Description | | ------------ | ----------------------------------------------------------------------- | | `1` | `SEND` -- standard message (OFT: token transfer) | | `2` | `SEND_AND_CALL` -- message with compose (OFT: token transfer + compose) | Always set enforced options for each destination. Without them, messages may fail due to insufficient gas on the destination chain. For `SEND_AND_CALL` (type 2), ensure the gas limit accounts for the compose execution. ## Gas Options Example When setting enforced options or passing `extra_options` in a send, you must specify the gas for `lzReceive` on the destination chain. The following examples use the standard LayerZero Type 3 encoding: | Operation | Example Gas | Options Hex | | --------------------------------- | ----------- | ---------------------------------------------- | | Basic `lz_receive` (OFT transfer) | 200,000 | `00030100110100000000000000000000000000030d40` | | `lz_receive` + `lz_compose` | 500,000 | `0003010011010000000000000000000000000007a120` | The options format is: `0x0003` (Type 3 header) + `01` (executor worker ID) + `0011` (length = 17 bytes) + `01` (lzReceive option type) + gas as `uint128`. Sending with **empty `extra_options`** and no enforced options will cause the send to fail with error `#1114` at the message library level. Always include executor options specifying the gas for `lzReceive` on the destination. ## Reading Configuration Query the current configuration for your OApp: ```bash wrap theme={null} # Get effective send ULN config stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ effective_send_uln_config \ --sender \ --dst_eid ``` ```bash wrap theme={null} # Get effective receive ULN config stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ effective_receive_uln_config \ --receiver \ --src_eid ``` ```bash wrap theme={null} # Get effective executor config stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ effective_executor_config \ --sender \ --dst_eid ``` ## Next Steps * **[Technical Overview](/v2/developers/stellar/technical-overview)**: Understand how DVN verification and executor delivery work end-to-end. * **[Troubleshooting](/v2/developers/stellar/troubleshooting/common-errors)**: Common configuration errors and solutions. # Getting Started with LayerZero on Stellar Source: https://docs.layerzero.network/v2/developers/stellar/getting-started Key differences between EVM and Stellar for LayerZero developers. Understand the authorization model, storage tiers, TTL management, and other core concepts. LayerZero's universal messaging protocol enables any blockchain that supports state propagation and events to participate in crosschain communication — including **Stellar**. LayerZero provides **Stellar Soroban contracts** that can communicate with the equivalent [Solidity Contract Libraries](/v2/developers/evm/overview) deployed on EVM chains. If you're new to LayerZero, we recommend reviewing [**"What is LayerZero?"**](/v2/concepts/getting-started/what-is-layerzero) before continuing. ## Differences from the Ethereum Virtual Machine Stellar's [Soroban](https://soroban.stellar.org/) smart contract platform differs from the EVM in several fundamental ways. This section covers the key differences you'll encounter when building LayerZero applications on Stellar: ### Comparison Table | Concept | EVM (Solidity) | Stellar (Soroban) | | ------------------------ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Language** | Solidity | Rust (compiled to WASM) | | **Token standard** | ERC-20 | SEP-41 / Stellar Asset Contract (SAC) | | **Address format** | 20 bytes | 35 bytes StrKey-encoded (1-byte version + 32-byte payload + 2-byte checksum), with contract (C-) and account (G-) [types](/v2/developers/stellar/technical-overview#address-types) | | **Authorization** | `msg.sender` / `onlyOwner` | `require_auth()` / Soroban auth framework | | **Contract composition** | Inheritance | Trait composition + proc macros | | **Storage** | Mapping / slot-based | Typed enum with instance, persistent, and temporary storage | | **Gas model** | Gas (EVM opcodes) | CPU instructions + memory + storage I/O | | **Contract upgrades** | Proxy pattern | Native WASM hash replacement | | **Fee payment** | `msg.value` (ETH sent with tx) | Explicit SEP-41 token transfer | | **Deploy model** | Deploy bytecode | Upload WASM, constructor called on deploy | | **Reentrancy** | Reentrancy possible, requires contract-level guards | Reentrancy prohibited natively | ### Authorization Model In Solidity, you check `msg.sender` to verify who called a function. Soroban uses a fundamentally different approach -- the **Soroban Authorization Framework**: ```solidity wrap theme={null} // EVM: check msg.sender function transfer(address to, uint256 amount) external { require(msg.sender == owner, "not authorized"); // ... } ``` ```rust wrap theme={null} // Soroban: require explicit authorization fn transfer(env: &Env, from: &Address, to: &Address, amount: i128) { from.require_auth(); // ... } ``` In Soroban, the caller explicitly authorizes the action. This works with both contract-to-contract calls and user wallets, and supports batched authorization across multiple calls in a single transaction. ### Trait Composition vs Inheritance Solidity uses inheritance to build contract hierarchies (`contract MyOFT is OFT`). Soroban uses **trait composition with proc macros**: ```rust wrap theme={null} // Soroban: #[lz_contract] provides contract, TTL, and auth; #[oapp] generates OApp trait implementations #[lz_contract] #[oapp] pub struct MyOApp; // You implement the receive logic impl LzReceiveInternal for MyOApp { fn __lz_receive(env: &Env, origin: &Origin, guid: &BytesN<32>, message: &Bytes, extra_data: &Bytes, executor: &Address, value: i128) { // Your application logic } } ``` The `#[oapp]` macro generates implementations for `OAppCore`, `OAppReceiver`, `OAppSenderInternal`, and `OAppOptionsType3`. You can customize specific traits using `#[oapp(custom = [receiver])]`. ### Storage Model Soroban has three storage tiers with different costs and lifetimes: | Storage Type | Lifetime | Use Case | | -------------- | -------------------------------------------------- | -------------------------------------------- | | **Instance** | Tied to contract instance | Configuration, addresses, immutable settings | | **Persistent** | Survives contract upgrades, requires TTL extension | Peer mappings, user data | | **Temporary** | Short-lived, cheapest | Caches, intermediate state | All Soroban storage entries have a **Time-To-Live (TTL)**. If a persistent entry's TTL expires, it becomes archived and must be restored before it can be read. LayerZero contracts automatically manage TTL extension for critical storage entries. ### Native Fee Payment On EVM, LayerZero fees are paid via `msg.value`. On Stellar, fees are paid by explicitly transferring the native token (XLM) to the Endpoint before calling `send`: ```rust wrap theme={null} // Soroban: explicit fee transfer (handled internally by OApp) // 1. Transfer native fee to endpoint native_token.transfer(fee_payer, endpoint_address, native_fee); // 2. Call endpoint.send(...) ``` This is handled internally by the OApp base contract -- you don't need to manage fee transfers manually. ## Next Steps * **[Technical Overview](/v2/developers/stellar/technical-overview)**: Deep dive into Soroban's architecture, protocol lifecycle, and how it affects LayerZero development. * **[Build an OApp](/v2/developers/stellar/oapp/overview)**: Create your first Omnichain Application on Stellar. * **[Build an OFT](/v2/developers/stellar/oft/overview)**: Deploy an Omnichain Fungible Token with SEP-41 integration. * **[Troubleshooting](/v2/developers/stellar/troubleshooting/common-errors)**: Common errors and how to resolve them. # OApp on Stellar Source: https://docs.layerzero.network/v2/developers/stellar/oapp/overview Build an Omnichain Application (OApp) on Stellar using Soroban smart contracts. Learn the contract structure, deployment, peer configuration, and crosschain messaging. The OApp Standard provides developers with a generic message passing interface to send and receive arbitrary pieces of data between contracts existing on different blockchain networks. How the data is interpreted and what actions it triggers depend on the specific OApp implementation. ## What is an OApp on Stellar? An **Omnichain Application (OApp)** on Stellar is a Soroban smart contract that can send and receive crosschain messages via the LayerZero protocol. OApps serve as the base for all LayerZero integrations on Stellar, including OFTs. ### Differences from EVM OApps | Aspect | EVM (Solidity) | Stellar (Soroban) | | -------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------- | | **Contract pattern** | Inherit from `OApp.sol` | Use `#[lz_contract]` + `#[oapp]` proc macros on struct | | **Authorization** | `msg.sender` checks | `require_auth()` framework | | **Send pattern** | `_lzSend(dstEid, message, options, fee, refund)` | `__lz_send(env, dst_eid, message, options, fee_payer, fee, refund_address)` | | **Receive pattern** | Override `_lzReceive(origin, guid, message, executor, extraData)` | Implement `LzReceiveInternal` trait | ## Build a Minimal OApp Follow these steps to set up your project, configure dependencies, and implement a minimal OApp contract. For a complete working example, see the [Counter OApp](https://github.com/LayerZero-Labs/monorepo-external/tree/main/apps/project-types/omni-counter-app/contracts/stellar) in the LayerZero monorepo. ### Prerequisites * [Stellar CLI](https://developers.stellar.org/docs/tools/developer-tools/cli/stellar-cli) (v25.1.0+) * [Rust](https://www.rust-lang.org/tools/install) (v1.90.0+) with `wasm32v1-none` target (`rustup target add wasm32v1-none`) * Familiarity with [Soroban smart contracts](https://developers.stellar.org/docs/learn/smart-contract-internals) and [Rust on Soroban](https://developers.stellar.org/docs/build/guides) ### Step 1: Set Up Your Project Create a new Soroban project: ```bash wrap theme={null} stellar contract init my-oapp cd my-oapp ``` ### Step 2: Configure Dependencies Add LayerZero dependencies to your `Cargo.toml`: ```toml wrap theme={null} [package] name = "my-oapp" version = "0.1.0" edition = "2021" [dependencies] soroban-sdk = "25.1.1" # LayerZero OApp dependencies # oapp = { ... } # oapp-macros = { ... } # endpoint-v2 = { ... } # common-macros = { ... } # utils = { ... } [dev-dependencies] soroban-sdk = { version = "25.1.1", features = ["testutils"] } [lib] crate-type = ["cdylib"] ``` Check the [OApp contracts](https://github.com/LayerZero-Labs/monorepo-external/tree/main/apps/oapp-app/contracts/stellar), [protocol contracts](https://github.com/LayerZero-Labs/monorepo-external/tree/main/contracts/protocol/stellar/contracts) on LayerZero GitHub for the latest Stellar contract packages. ### Step 3: Project Structure Organize your contract: ``` my-oapp/ ├── Cargo.toml ├── src/ │ └── lib.rs # Contract implementation └── rust-toolchain.toml # Pin to compatible Rust version ``` ### Step 4: Implement the Contract Here's a minimal OApp that sends and receives crosschain messages: ```rust wrap theme={null} #![no_std] use soroban_sdk::{Address, Bytes, BytesN, Env}; use common_macros::{contract_impl, lz_contract}; use endpoint_v2::{MessagingFee, Origin}; use oapp::oapp_core::init_ownable_oapp; use oapp::oapp_receiver::LzReceiveInternal; use oapp::oapp_sender::{FeePayer, OAppSenderInternal}; use oapp_macros::oapp; // The #[lz_contract] macro generates contract, ownable, TTL traits // The #[oapp] macro generates OApp trait implementations #[lz_contract] #[oapp] pub struct MyOApp; // Constructor #[contract_impl] impl MyOApp { pub fn __constructor( env: &Env, owner: &Address, endpoint: &Address, delegate: &Address, ) { init_ownable_oapp::(env, owner, endpoint, delegate); } /// Estimate the messaging fee for sending a crosschain message. pub fn quote(env: &Env, dst_eid: u32, options: &Bytes, pay_in_zro: bool) -> MessagingFee { // Encode the outbound message payload before estimating the messaging fee. // let message = msg_codec::encode(); Self::__quote(env, dst_eid, &message, options, pay_in_zro) } /// Send a crosschain message to the destination chain. pub fn send(env: &Env, caller: &Address, dst_eid: u32, options: &Bytes, fee: &MessagingFee) { caller.require_auth(); // Encode the outbound message payload before sending the cross-chain message. // let message = msg_codec::encode(); // Send the message — caller already authorized via require_auth() above Self::__lz_send(env, dst_eid, &message, options, &FeePayer::Verified(caller.clone()), fee, caller); } } // You must implement LzReceiveInternal for custom receive logic impl LzReceiveInternal for MyOApp { fn __lz_receive( env: &Env, origin: &Origin, guid: &BytesN<32>, message: &Bytes, extra_data: &Bytes, executor: &Address, value: i128, ) { // Decode and process the incoming message // Example: store the message or trigger an action } } ``` ## OApp Components The `#[oapp]` macro generates implementations for these traits: | Trait | Purpose | Key Functions | | -------------------- | ------------------------ | ------------------------------------------------------------------------------------ | | `OAppCore` | Base OApp functionality | `endpoint()`, `peer()`, `set_peer()`, `set_delegate()` | | `OAppSenderInternal` | Internal send helpers | `__quote()`, `__lz_send()` | | `OAppReceiver` | Receive message handling | `lz_receive()`, `allow_initialize_path()`, `next_nonce()`, `is_compose_msg_sender()` | | `OAppOptionsType3` | Execution options | `enforced_options()`, `set_enforced_options()`, `combine_options()` | **You must always implement:** `LzReceiveInternal` -- this is your custom receive logic. To provide a custom implementation for any generated trait, use: ```rust wrap theme={null} #[lz_contract] #[oapp(custom = [receiver, options_type3])] pub struct MyCustomOApp; // Now you must implement OAppReceiver and OAppOptionsType3 yourself ``` ## How OApp Messaging Works ### Peer Configuration Before sending or receiving messages, configure the trusted peer address for each remote chain: The `set_peer` function requires the contract `Owner`. ```rust wrap theme={null} // Set the peer for a remote endpoint ID (owner only) oapp.set_peer(&dst_eid, &Some(remote_oapp_bytes32), &caller); ``` Peers are stored in **persistent storage** (keyed by endpoint ID) and validated on every inbound message. You must set peers on **both sides** of the connection. If chain A's OApp sets chain B as a peer, chain B's OApp must also set chain A as a peer. Messages from unregistered peers are rejected. ### Message Flow #### Send Flow ``` ┌─────────────┐ │ OApp │ 1. User calls send() │ (Your App) │ └──────┬──────┘ │ 2. Transfer fee, build message ▼ ┌─────────────┐ │ Endpoint │ 3. Assign nonce, compute GUID └──────┬──────┘ │ 4. Route to send library ▼ ┌─────────────┐ │ ULN302 │ 5. Assign jobs to workers └──────┬──────┘ │ 6. Calculate fees ▼ ┌─────────────────┐ │ DVNs + Executor │ 7. Monitor and deliver on destination └─────────────────┘ ``` #### Receive Flow The OApp is the entry point, not the Endpoint. This avoids reentrancy issues and supports ABA messaging patterns. ``` ┌─────────────┐ │ Executor │ 1. Authorizes via require_auth() └──────┬──────┘ │ 2. Calls OApp directly ▼ ┌─────────────┐ │ OApp │ 3. Validates peer, forwards value │ (Your App) │ 4. Calls endpoint.clear() │ │ 5. Executes __lz_receive() └──────┬──────┘ │ ▼ ┌─────────────┐ │ Endpoint │ 6. Verifies payload hash │ │ 7. Emits PacketDelivered └─────────────┘ ``` ### Sending Messages Sending a crosschain message involves four steps: #### Step 1: Encode Your Message Structure your message payload. LayerZero transports raw bytes — how you encode them depends on your application: ```rust wrap theme={null} // let message = msg_codec::encode(); ``` #### Step 2: Build Execution Options Options specify how the message should be executed on the destination chain (gas limit, native value, etc.): ```rust wrap theme={null} // Type 3 options are combined: enforced options + caller-provided options let combined = oapp.combine_options(&dst_eid, &msg_type, &extra_options); ``` #### Step 3: Quote the Fee ```rust wrap theme={null} let fee = MyOApp::__quote( &env, dst_eid, // Destination endpoint ID &message, // Encoded message payload &options, // Execution options false, // pay_in_zro ); ``` #### Step 4: Send the Message ```rust wrap theme={null} let receipt = MyOApp::__lz_send( &env, dst_eid, &message, &combined_options, &FeePayer::Unverified(from.clone()), &fee, &refund_address, ); ``` The `FeePayer` enum tracks whether the payer has already been authorized, preventing duplicate `require_auth()` calls. ### Receiving Messages When a message arrives, the `lz_receive` flow (provided by the `OAppReceiver` trait's default implementation) handles validation and routing: 1. **Executor authenticates**: `executor.require_auth()` 2. **Peer validation**: Asserts `origin.sender` matches the configured peer for `origin.src_eid` 3. **Value forwarding**: Transfers native token from executor to OApp if value != 0 4. **Payload clearing**: Calls `endpoint.clear()` to mark the message as delivered 5. **Your logic**: Calls `__lz_receive()` with the decoded message ```rust wrap theme={null} impl LzReceiveInternal for MyOApp { fn __lz_receive( env: &Env, origin: &Origin, // Source chain info (src_eid, sender, nonce) guid: &BytesN<32>, // Message GUID message: &Bytes, // Your application payload extra_data: &Bytes, // Additional data from executor executor: &Address, // Executor that delivered the message value: i128, // Native token value forwarded ) { // Decode your message format // Execute business logic // Emit events as needed } } ``` ### Message Inspection Optionally, set an external inspector contract to validate outbound messages before they're sent: ```rust wrap theme={null} // Set a message inspector (owner only) oapp.set_msg_inspector(&Some(inspector_address), &operator); ``` The inspector implements the `IOAppMsgInspector` trait: ```rust wrap theme={null} trait IOAppMsgInspector { fn inspect(env: &Env, oapp: &Address, message: &Bytes, options: &Bytes) -> bool; } ``` The inspector can reject a message by returning `false` or by panicking. Note: the OFT standard ignores the return value and relies on panics for rejection — if building a custom OApp, you can check the boolean return in your send logic. The `set_msg_inspector` function is available on OFT contracts. If building a custom OApp, you can implement message inspection in your send logic directly. ## Deployment ### Step 1: Build the Contract ```bash wrap theme={null} stellar contract build ``` This compiles your contract to WASM at `target/wasm32v1-none/release/my_oapp.wasm`. ### Step 2: Deploy to Testnet ```bash wrap theme={null} stellar contract deploy \ --wasm target/wasm32v1-none/release/my_oapp.wasm \ --network testnet \ --source my-account \ -- \ --owner \ --endpoint \ --delegate ``` The constructor arguments are passed after `--`. The contract is deployed and initialized atomically. ### Step 3: Configure Peers After deployment, set the peer addresses for each remote chain: ```bash wrap theme={null} stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ set_peer \ --eid \ --peer \ --operator ``` The Stellar CLI (v25.1.0) has a known bug where `Option>` arguments (like `--peer`) are always set to `None` regardless of the format provided. If the CLI `set_peer` command does not work, use the [Stellar JavaScript SDK](https://stellar.github.io/js-stellar-sdk/) as a workaround: ```javascript wrap theme={null} import { Contract, xdr, nativeToScVal, Address, TransactionBuilder, rpc } from '@stellar/stellar-sdk'; const contract = new Contract(YOUR_OAPP_ADDRESS); // Left-pad EVM address (20 bytes) with 12 zero bytes to get 32 bytes const peerBytes = Buffer.from('000000000000000000000000' + evmAddress.slice(2).toLowerCase(), 'hex'); const tx = new TransactionBuilder(account, { fee: '10000000', networkPassphrase }) .addOperation( contract.call('set_peer', nativeToScVal(dstEid, { type: 'u32' }), xdr.ScVal.scvBytes(peerBytes), new Address(deployer.publicKey()).toScVal() ) ) .setTimeout(30).build(); const sim = await server.simulateTransaction(tx); const prepared = rpc.assembleTransaction(tx, sim).build(); prepared.sign(keypair); await server.sendTransaction(prepared); ``` ### Step 4: Change Delegate (Optional) The delegate is set during deployment via the constructor's `delegate` parameter. The delegate is the address authorized to call endpoint configuration functions (`set_config`, `set_send_library`, `set_receive_library`) on behalf of your OApp. To change the delegate after deployment: ```bash wrap theme={null} stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ set_delegate \ --delegate \ --operator ``` ### Step 5: Set Message Libraries (Optional) Configure custom send and receive libraries if you don't want the default. The `new_lib` parameter is `Option
` — pass `None` to reset to the default library: ```bash wrap theme={null} # Set custom send library stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ set_send_library \ --caller \ --sender \ --dst_eid \ --new_lib ``` ```bash wrap theme={null} # Set custom receive library stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ set_receive_library \ --caller \ --receiver \ --src_eid \ --new_lib \ --grace_period 0 ``` ### Step 6: Set Enforced Options (Optional) Optionally, configure enforced execution options for each destination: ```bash wrap theme={null} stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ set_enforced_options \ --options '[{"eid": , "msg_type": 1, "options": ""}]' \ --operator ``` See [Message Execution Options](/v2/developers/evm/configuration/options) for how to build the options bytes for worker configuration (gas limits, native value, etc.). ## Core Methods These are the primary functions your OApp will use to send and receive crosschain messages. ### \_\_quote() Estimates the fee required to send a crosschain message without actually sending it. Returns a `MessagingFee` containing the native and ZRO fee breakdown. **When to use**: Before sending to determine the required fee, or to display estimated costs to users. ### \_\_lz\_send() Sends a crosschain message to a destination chain. Internally transfers the fee to the Endpoint, looks up the peer for the destination, and calls the Endpoint's `send` function. **Key parameters**: * `dst_eid`: Destination chain endpoint ID * `message`: Your encoded payload (raw bytes) * `options`: Execution parameters (gas limits, native value) * `fee_payer`: `FeePayer::Unverified(addr)` or `FeePayer::Verified(addr)` * `fee`: The `MessagingFee` from `__quote()` * `refund_address`: Where to send excess fees **FeePayer enum**: * `FeePayer::Unverified(addr)` — Safe default. `__lz_send` will call `addr.require_auth()`. * `FeePayer::Verified(addr)` — Use when the caller has already called `require_auth()` to avoid duplicate authorization in the Soroban auth tree. ### lz\_receive() Processes incoming messages delivered by the Executor. The default implementation (provided by `OAppReceiver`) performs validation, payload clearing, and delegates to your `__lz_receive()` implementation. **Your implementation**: Implement `LzReceiveInternal` with your custom business logic. The base validation (executor auth, peer check, payload clearing) is handled before your code runs. *** ## Message Encoding Your OApp is responsible for encoding and decoding message payloads. LayerZero transports raw bytes — how you structure them depends on your application. **Key principles**: * Use consistent byte order (big-endian recommended for cross-VM compatibility with EVM) * Use fixed-width fields where possible for deterministic parsing * Test encoding/decoding on both source and destination chains *** ## Configuration Configuration functions have different authorization requirements: | Function | Caller | Description | | ------------------------------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `set_peer` | Owner | Register trusted remote OApp addresses | | `set_enforced_options` | Owner | Define minimum execution options per destination | | `set_delegate` | Owner | Change the delegate address on the Endpoint | | `set_config` (DVN/Executor) | Delegate or OApp | Configure DVN and Executor via the Endpoint ([DVN & Executor Config](/v2/developers/stellar/configuration/dvn-executor-config)) | | `set_send_library` / `set_receive_library` | Delegate or OApp | Override default message libraries on the Endpoint | ### Events | Event | Topics | Data | | ------------------- | ------ | -------------------------------------- | | `PeerSet` | -- | `eid: u32`, `peer: Option>` | | `EnforcedOptionSet` | -- | `Vec` | ## Configuring Remote Chains to Send to Stellar When configuring OApps on other chains (e.g., EVM, Solana) to send messages **to Stellar**, follow standard LayerZero configuration with these Stellar-specific details: ### 1. Use Payload as Peer Stellar addresses use [StrKey encoding](https://stellar.org/protocol/sep-23): 1-byte version + 32-byte payload + 2-byte CRC16 checksum. LayerZero uses the **32-byte payload** (contract ID hash) as the bytes32 peer address. ```solidity wrap theme={null} // On EVM: Use Stellar OApp contract's 32-byte payload (not the full C-address) myOApp.setPeer( , // Stellar endpoint ID bytes32(payload) // 32-byte payload from Stellar C-address ); ``` To extract the 32-byte payload from a C-address, decode the base32 StrKey and take bytes 1–32 (skipping the version byte and CRC16 checksum). ### 2. Set Enforced Options for Stellar Destination Configure minimum execution options for messages sent to Stellar: ```solidity wrap theme={null} // On EVM: Set enforced options for Stellar pathway bytes memory options = Options.newOptions() .addExecutorLzReceiveOption(5000, 0); // Gas units, no msg.value myOApp.setEnforcedOptions( EnforcedOptionParam({ eid: , msgType: SEND, options: options }) ); ``` ### 3. Standard DVN Configuration DVN configuration on remote chains follows standard LayerZero patterns — no Stellar-specific changes needed. See the platform-specific implementation guides for your source chain's configuration. *** ## Best Practices ### Configure Security Before Setting Peers Configure DVNs, Executor, and enforced options **before** setting peers. Setting a peer opens the messaging pathway for that remote chain. ### Use FeePayer::Verified When Appropriate If your OApp's `send` function already calls `require_auth()` on the caller, pass `FeePayer::Verified(caller)` to `__lz_send()` to avoid a duplicate authorization node in Soroban's auth tree. ### Monitor TTL for Persistent Storage Peer mappings are stored in persistent storage with TTL. LayerZero contracts automatically extend TTL when entries are accessed, but low-activity OApps should monitor and extend TTL for critical state to prevent archival. ### Test Bidirectional Messaging Always test both sending and receiving on testnet before deploying to mainnet. Verify that messages are correctly encoded on the source chain and decoded on the destination chain. *** ## Security Considerations ### Critical Validations * **Peer verification**: Every inbound message is checked against the configured peer. Only messages from the registered peer address for a given source chain are accepted. * **Payload hash verification**: The `endpoint.clear()` operation verifies the message against the stored payload hash, ensuring the delivered message matches what was verified by DVNs. If your contract uses the `#[oapp]` macro, this is already handled for you. ### Common Pitfalls * Forgetting to set peers on both sides of the connection * Not setting enforced options, resulting in failed execution on the destination chain * Using `FeePayer::Unverified` when the caller is already authorized, causing auth tree issues * Not monitoring TTL for low-activity contracts, leading to archived storage entries ## Next Steps * **[Build an OFT](/v2/developers/stellar/oft/overview)**: Deploy a token with built-in crosschain transfer. * **[DVN & Executor Config](/v2/developers/stellar/configuration/dvn-executor-config)**: Configure your security stack. * **[Technical Overview](/v2/developers/stellar/technical-overview)**: Understand the full message lifecycle. * **[Troubleshooting](/v2/developers/stellar/troubleshooting/common-errors)**: Common errors and solutions. # OFT on Stellar Source: https://docs.layerzero.network/v2/developers/stellar/oft/overview Deploy an Omnichain Fungible Token (OFT) on Stellar using Soroban smart contracts. Learn deployment, token integration, send/receive mechanics, and extensions. ## What is an OFT on Stellar? An **Omnichain Fungible Token (OFT)** on Stellar is a Soroban smart contract that enables crosschain token transfers via the LayerZero protocol. It extends the [OApp](/v2/developers/stellar/oapp/overview) contract with token handling logic for debiting (lock/burn) and crediting (unlock/mint) tokens. Stellar OFTs integrate with **SEP-41** tokens -- Stellar's standard token interface (analogous to ERC-20 on EVM). ### Classic Assets Receiving Requirements These requirements apply when your OFT token is a **SAC (wrapped classic asset)**. If you are using a custom contract token, trustlines are not required for any address type. #### G-Address (EOA) G-address recipients must meet two prerequisites before they can receive classic assets: 1. **Account activation**: The account must hold a minimum of 1 XLM to exist on the Stellar network. 2. **Trustline**: The account must have an explicit trustline for the classic asset being received. If `lz_receive` fails due to unmet prerequisites, delivery can be retried once the recipient account is activated and the trustline is established. #### C-Address (Smart Contract) C-address recipients are not subject to these restrictions. As long as the contract address exists on-chain, it can receive assets directly. ## OFT Types Stellar uses a single OFT contract with an `OftType` enum to determine behavior: ```rust wrap theme={null} enum OftType { LockUnlock, // Lock tokens on send, unlock on receive MintBurn(Address), // Burn tokens on send, mint on receive via Mintable contract } ``` | Mode | Send (Debit) | Receive (Credit) | When to Use | | -------------- | --------------------------- | --------------------------------------- | ------------------------------------------------------------- | | **LockUnlock** | Lock tokens in OFT contract | Unlock tokens from OFT contract | Wrapping an existing token that you don't control minting for | | **MintBurn** | Burn tokens from sender | Mint tokens to recipient via `Mintable` | You control the token supply (e.g., via SAC Manager) | **MintBurn** is the recommended approach when you have control over the token supply. ## Setup & Deployment For the full OFT reference implementation, see the [OFT source](https://github.com/LayerZero-Labs/monorepo-external/tree/main/apps/oft-app/contracts/stellar/oft) in the LayerZero monorepo. **Prerequisites**: * [Stellar CLI](https://developers.stellar.org/docs/tools/developer-tools/cli/stellar-cli) (v25.1.0+) * [Rust](https://www.rust-lang.org/tools/install) (v1.90.0+) with `wasm32v1-none` target (`rustup target add wasm32v1-none`) * Familiarity with [Soroban smart contracts](https://developers.stellar.org/docs/learn/smart-contract-internals) and [Rust on Soroban](https://developers.stellar.org/docs/build/guides) This guide uses **MintBurn** mode. For LockUnlock mode, see [Using LockUnlock Mode](#using-lockunlock-mode) below. **The MintBurn 3-contract pattern:** ``` Token (SAC) ← SAC Manager ← OFT ``` 1. **Token (SAC)**: Your SEP-41 token 2. **SAC Manager**: RBAC-controlled admin wrapper that implements `Mintable`. The OFT calls it to mint; it calls the SAC as its admin. 3. **OFT**: Deployed with `oft_type: MintBurn(sac_manager_address)` ### Step 1: Create and Deploy Your Token If you don't already have a token, deploy a [SEP-41](https://stellar.org/protocol/sep-41)-compliant fungible token on Soroban. The simplest approach is to wrap a [Stellar classic asset](https://developers.stellar.org/docs/tokens/stellar-asset-contract) as a **Stellar Asset Contract (SAC)**: ```bash wrap theme={null} # Deploy the classic asset as a SAC on testnet stellar contract asset deploy \ --asset MY_TOKEN:YOUR_ISSUER_ADDRESS \ --network testnet \ --source my-account ``` This returns the SAC contract address — your SEP-41 token address. Alternatively, you can build a custom Soroban contract token using [OpenZeppelin Stellar Contracts](https://docs.openzeppelin.com/stellar-contracts) for more control over token logic. ### Step 2: Deploy the SAC Manager (Mintable Contract) For MintBurn mode, the OFT needs a contract that implements the `Mintable` trait to mint tokens on crosschain receive: ```rust wrap theme={null} pub trait Mintable { fn mint(env: &Env, to: &Address, amount: i128, operator: &Address); } ``` The recommended approach is to use the **SAC Manager** contract provided by LayerZero, which wraps a SAC with role-based access control. **Create SAC Manager project**: ```bash wrap theme={null} stellar contract init sac-manager cd sac-manager ``` **Configure `Cargo.toml`** with LayerZero dependencies: ```toml wrap theme={null} [package] name = "sac-manager" version = "0.1.0" edition = "2021" [dependencies] soroban-sdk = "25.1.1" cfg-if = "1.0" # LayerZero dependencies # common-macros = { ... } # utils = { ... } [dev-dependencies] soroban-sdk = { version = "25.1.1", features = ["testutils"] } [lib] crate-type = ["cdylib"] ``` **Implement SAC Manager**: ```rust wrap theme={null} use common_macros::{contract_impl, lz_contract, only_role, storage}; use soroban_sdk::{token::StellarAssetClient, Address, Env}; use utils::rbac::RoleBasedAccessControl; #[storage] pub enum SACManagerStorage { #[instance(Address)] SacToken, } #[lz_contract] pub struct SACManager; const MINTER_ROLE: &str = "MINTER_ROLE"; const ADMIN_MANAGER_ROLE: &str = "ADMIN_MANAGER_ROLE"; const BLACKLISTER_ROLE: &str = "BLACKLISTER_ROLE"; const CLAWBACK_ROLE: &str = "CLAWBACK_ROLE"; #[contract_impl] impl SACManager { pub fn __constructor(env: &Env, sac_token: &Address, owner: &Address) { Self::init_owner(env, owner); SACManagerStorage::set_sac_token(env, sac_token); } pub fn underlying_sac(env: &Env) -> Address { SACManagerStorage::sac_token(env).unwrap() } } // Each method is role-gated — only addresses with the matching role can call it #[contract_impl(contracttrait)] impl SACAdminWrapper for SACManager { #[only_role(operator, ADMIN_MANAGER_ROLE)] fn set_admin(env: &Env, new_admin: &Address, operator: &Address) { sac_client(env).set_admin(new_admin); } #[only_role(operator, BLACKLISTER_ROLE)] fn set_authorized(env: &Env, id: &Address, authorize: bool, operator: &Address) { sac_client(env).set_authorized(id, &authorize); } #[only_role(operator, CLAWBACK_ROLE)] fn clawback(env: &Env, from: &Address, amount: i128, operator: &Address) { sac_client(env).clawback(from, &amount); } // This is the method the OFT calls to mint tokens on crosschain receive #[only_role(operator, MINTER_ROLE)] fn mint(env: &Env, to: &Address, amount: i128, operator: &Address) { sac_client(env).mint(to, &amount); } } #[contract_impl(contracttrait)] impl RoleBasedAccessControl for SACManager {} fn sac_client(env: &Env) -> StellarAssetClient<'_> { StellarAssetClient::new(env, &SACManager::underlying_sac(env)) } ``` The SAC Manager source is available in the [LayerZero Stellar contracts](https://github.com/LayerZero-Labs/monorepo-external/blob/main/apps/oft-app/contracts/stellar/sac-manager/src/sac_manager.rs). You can also implement your own contract — any contract that satisfies the `Mintable` trait will work. **Build and deploy**: ```bash wrap theme={null} # Build the SAC Manager stellar contract build --release # Deploy the SAC Manager stellar contract deploy \ --wasm target/wasm32v1-none/release/sac_manager.wasm \ --network testnet \ --source my-account \ -- \ --sac_token \ --owner # Set SAC Manager as the token admin (so it can mint) stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ set_admin \ --new_admin ``` **The issuer account must be locked (master weight set to 0).** In Stellar classic assets, transfers from/to the issuer are equivalent to minting/burning. The issuer can always mint more tokens and perform other classic operations directly, even when an explicit admin (this contract) is set. If the issuer account is not locked, the RBAC model enforced by this contract can be bypassed, breaking the trust model. ### Step 3: Create and Deploy the OFT Contract **Create OFT project**: ```bash wrap theme={null} stellar contract init my-oft cd my-oft ``` **Configure `Cargo.toml`** with LayerZero dependencies: ```toml wrap theme={null} [package] name = "my-oft" version = "0.1.0" edition = "2021" [dependencies] soroban-sdk = "25.1.1" cfg-if = "1.0" # LayerZero OFT dependencies # oapp = { ... } # oapp-macros = { ... } # oft-core = { ... } # endpoint-v2 = { ... } # common-macros = { ... } # utils = { ... } [dev-dependencies] soroban-sdk = { version = "25.1.1", features = ["testutils"] } [lib] crate-type = ["cdylib"] ``` Check the [OFT core](https://github.com/LayerZero-Labs/monorepo-external/tree/main/apps/oft-app/contracts/stellar/oft-core), [OApp contracts](https://github.com/LayerZero-Labs/monorepo-external/tree/main/apps/oapp-app/contracts/stellar), [protocol contracts](https://github.com/LayerZero-Labs/monorepo-external/tree/main/contracts/protocol/stellar/contracts) on LayerZero GitHub for the latest Stellar contract packages. **Implement OFT**: ```rust wrap theme={null} #![no_std] use soroban_sdk::{Address, Env}; use common_macros::{contract_impl, lz_contract, storage}; use oapp_macros::oapp; use oft_core::{impl_oft_lz_receive, OFTCore, OFTInternal}; use oft::{lock_unlock, mint_burn, OftType}; // The #[lz_contract] macro generates contract, ownable, TTL traits // The #[oapp] macro generates OApp trait implementations // Storage for OFT-specific state #[storage] enum OFTStorage { #[instance(OftType)] OftType, } #[lz_contract] #[oapp] pub struct OFT; // Handle incoming crosschain messages impl_oft_lz_receive!(OFT); // Constructor #[contract_impl] impl OFT { pub fn __constructor( env: &Env, token: &Address, // SEP-41 token address shared_decimals: u32, // Crosschain decimal precision (typically 6) oft_type: OftType, // LockUnlock or MintBurn(mintable_address) endpoint: &Address, // LayerZero Endpoint V2 address delegate: &Address, // Initial owner and endpoint delegate ) { Self::__initialize_oft(env, token, shared_decimals, delegate, endpoint, delegate); OFTStorage::set_oft_type(env, &oft_type); } } // Expose OFTCore public methods (quote_oft, quote_send, send) // All methods have default implementations — override to customize (e.g., fee details, rate limits) #[contract_impl(contracttrait)] impl OFTCore for OFT {} // Internal OFT logic — you must implement __debit() and __credit() impl OFTInternal for OFT { /// Debits tokens from the sender for crosschain transfer. /// Returns (amount_sent, amount_received) after dust removal and fees. fn __debit(env: &Env, from: &Address, amount_ld: i128, min_amount_ld: i128, dst_eid: u32) -> (i128, i128) { match Self::oft_type(env) { OftType::LockUnlock => { // Transfer tokens from sender to this contract (lock) lock_unlock::debit::(env, &Self::token(env), from, amount_ld, min_amount_ld, dst_eid) } OftType::MintBurn(_) => { // Burn tokens directly from the sender mint_burn::debit::(env, &Self::token(env), from, amount_ld, min_amount_ld, dst_eid) } } } /// Credits tokens to the recipient after receiving a crosschain transfer. /// Returns the amount actually credited. fn __credit(env: &Env, to: &Address, amount_ld: i128, src_eid: u32) -> i128 { match Self::oft_type(env) { OftType::LockUnlock => { // Transfer tokens from this contract to recipient (unlock) lock_unlock::credit::(env, &Self::token(env), to, amount_ld, src_eid) } OftType::MintBurn(mintable) => { // Mint tokens to the recipient via the Mintable contract mint_burn::credit::(env, &mintable, to, amount_ld, src_eid) } } } } ``` The key traits generated and implemented: | Trait | Description | | ------------------- | ----------------------------------------------------------------------------------------------------- | | `OFTCore` | Public methods: `quote_oft()`, `quote_send()`, `send()`. Expose via `#[contract_impl(contracttrait)]` | | `OFTInternal` | `__debit()` and `__credit()` — defines how tokens are locked/burned and unlocked/minted | | `LzReceiveInternal` | Handles incoming messages via `impl_oft_lz_receive!` macro. Decodes OFT messages and credits tokens | **Build and deploy**: ```bash wrap theme={null} # Build the OFT contract stellar contract build # Deploy the OFT contract stellar contract deploy \ --wasm target/wasm32v1-none/release/my_oft.wasm \ --network testnet \ --source my-account \ -- \ --token \ --shared_decimals 6 \ --oft_type '{"MintBurn":""}' \ --endpoint \ --delegate ``` **Constructor parameters:** | Parameter | Type | Description | | ----------------- | --------- | -------------------------------------------------------------- | | `token` | `Address` | SEP-41 token contract address | | `shared_decimals` | `u32` | Crosschain decimal precision (typically 6) | | `oft_type` | `OftType` | `LockUnlock` or `MintBurn(mintable_address)` | | `endpoint` | `Address` | LayerZero Endpoint V2 address | | `delegate` | `Address` | Initial contract owner and delegate for endpoint configuration | Unlike the OApp constructor (which takes separate `owner` and `delegate` parameters), the OFT constructor uses `delegate` as both the initial owner and the endpoint delegate. ### Step 4: Grant Minting Permissions Grant the `MINTER_ROLE` on the SAC Manager to the OFT contract, so it can mint tokens during crosschain receives: ```bash wrap theme={null} stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ grant_role \ --account \ --role MINTER_ROLE \ --caller ``` ### Step 5: Configure the OFT The `set_peer` and `set_enforced_options` functions require the contract's **AUTHORIZER** (the owner by default). After deployment, configure peers and enforced options: ```bash wrap theme={null} # Set peer for each remote chain stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ set_peer \ --eid \ --peer \ --operator ``` The Stellar CLI (v25.1.0) has a known bug with `Option>` arguments -- `set_peer` may silently set the peer to `None`. If this happens, use the JavaScript SDK workaround described in the [OApp overview](/v2/developers/stellar/oapp/overview#step-3-configure-peers). ```bash wrap theme={null} # Set enforced options stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ set_enforced_options \ --options '[{"eid": , "msg_type": 1, "options": ""}]' \ --operator ``` ### Using LockUnlock Mode If you don't have mint authority over the token (e.g., bridging an existing asset), use **LockUnlock** mode instead. The OFT holds tokens in its own balance — locking on send, unlocking on receive. No SAC Manager or Mintable contract is needed. Simply deploy the OFT with `LockUnlock`: ```bash wrap theme={null} stellar contract deploy \ --wasm target/wasm32v1-none/release/my_oft.wasm \ --network testnet \ --source my-account \ -- \ --token \ --shared_decimals 6 \ --oft_type "LockUnlock" \ --endpoint \ --delegate ``` Then proceed to [Step 5: Configure the OFT](#step-5-configure-the-oft). ## Core Operations **Do not use the issuer account as a sender or recipient.** On Stellar, transfers from/to the token issuer are equivalent to minting/burning — the issuer does not hold a balance of its own asset. Tokens sent to the issuer are silently destroyed, which would cause asset loss in crosschain transfers. When testing OFT transfers, always use a **non-issuer** account. To set up a non-issuer holder: 1. Generate a separate keypair and fund it via Friendbot 2. Add a trustline for the custom asset: `Operation.changeTrust({ asset })` 3. Transfer tokens from the issuer to the holder: `Operation.payment({ destination, asset, amount })` ### Sending Tokens Sending tokens crosschain involves quoting the fee and executing the transfer: #### Step 1: Build SendParam ```rust wrap theme={null} let send_param = SendParam { dst_eid: 30101, // Destination endpoint ID to: recipient_bytes32, // Recipient address (32 bytes) amount_ld: 1_000_000_000, // Amount in local decimals min_amount_ld: 990_000_000, // Minimum after fees (slippage protection) extra_options: Bytes::new(&env), // Additional execution options compose_msg: Bytes::new(&env), // Optional compose message oft_cmd: Bytes::new(&env), // Custom OFT command }; ``` #### Step 2: Quote the Transfer ```rust wrap theme={null} // Preview the transfer (limits, fees, expected amounts after dust removal) let (limits, fee_details, receipt) = oft.quote_oft(&from, &send_param); // Quote the LayerZero messaging fee let fee = oft.quote_send(&from, &send_param, &false); ``` `quote_oft` returns: | Return | Type | Description | | ------------- | ------------------- | --------------------------------------------------------------------- | | `limits` | `OFTLimit` | Min/max amounts in local decimals | | `fee_details` | `Vec` | Breakdown of OFT-level fees | | `receipt` | `OFTReceipt` | Expected `amount_sent_ld` and `amount_received_ld` after dust removal | Both `quote_oft` and `quote_send` are read-only — they do not execute any state changes. #### Step 3: Execute Send ```rust wrap theme={null} let (messaging_receipt, oft_receipt) = oft.send( &from, // Sender (must authorize) &send_param, &fee, // From quote_send &refund_address, // Excess fee refund recipient ); ``` The sender must authorize the call (`from.require_auth()` is called internally). ### Receiving Tokens Token receipt is handled automatically by the OFT contract's `lz_receive` implementation: 1. Decode the `OFTMessage` from the inbound payload 2. Resolve the recipient address — the crosschain OFT message carries only the 32-byte address payload, but Stellar addresses are 35 bytes (1-byte version + 32-byte payload + 2-byte checksum) with two possible types (C-address for contracts, G-address for accounts). The `resolve_address` utility disambiguates by checking if a contract exists at the 32-byte address; if so, it resolves to a C-address, otherwise it falls back to a G-address. This is handled automatically. 3. Convert amount from shared decimals to local decimals 4. Credit tokens: * **LockUnlock**: `token.transfer(from: oft_contract, to: recipient, amount)` * **MintBurn**: `mintable.mint(to: recipient, amount, operator: oft_contract)` 5. If a compose message is included, register it via `endpoint.send_compose` for subsequent execution 6. Emit `OFTReceived` event ## Decimal Precision OFTs use a **shared decimal** system for crosschain consistency. The shared decimals value determines the precision used in the crosschain message, while local decimals are the token's actual precision. | Parameter | Description | Example | | ------------------------- | ------------------------------------- | -------------------- | | `local_decimals` | Token's native decimals (from SEP-41) | 7 (Stellar standard) | | `shared_decimals` | Crosschain precision | 6 | | `decimal_conversion_rate` | `10^(local - shared)` | 10 | Any amount below the `decimal_conversion_rate` is considered **dust** and is removed before sending. For example, with a conversion rate of 10, an amount of `1,000,015` becomes `1,000,010` (the trailing 5 is dust). **Recommended configuration:** | Source Token Decimals | Recommended `shared_decimals` | Conversion Rate | | --------------------- | ----------------------------- | --------------- | | 7 (Stellar default) | 6 | 10 | | 8 | 6 | 100 | | 18 (EVM default) | 6 | 10^12 | ## OFT Extensions The Stellar OFT supports three optional extensions that can be enabled at deployment: ### Pausable Pause and unpause all OFT operations (send, receive, quote): ```rust wrap theme={null} // Pause the OFT (requires PAUSER role) oft.pause(&operator); // Unpause the OFT (requires UNPAUSER role) oft.unpause(&operator); // Check pause status let paused = oft.is_paused(); ``` When paused, `send`, `quote_send`, `quote_oft`, and `lz_receive` (credit) all revert with error code `3110` (`Paused`). ### OFT Fee Charge a fee on outbound transfers, configurable per destination or as a default: ```rust wrap theme={null} // Set a default fee of 0.5% (50 bps) -- requires FEE_CONFIG_MANAGER_ROLE oft.set_default_fee_bps(&Some(50), &operator); // Override for a specific destination (1% fee to chain 30101) oft.set_fee_bps(&30101, &Some(100), &operator); // Explicitly set 0% fee for a destination (overrides default) oft.set_fee_bps(&30102, &Some(0), &operator); // Remove destination override (falls back to default) oft.set_fee_bps(&30102, &None, &operator); // Set where fees are collected -- requires AUTHORIZER (contract owner) oft.set_fee_deposit_address(&Some(treasury_address), &operator); ``` **Fee resolution order:** Per-destination fee > Default fee > 0 (no fee) Fee calculation: `fee = amount_ld * fee_bps / 10_000` ### Rate Limiter Control transfer volume with a **leaky bucket** algorithm: ```rust wrap theme={null} use oft::rate_limiter::{Direction, Mode, RateLimitConfig}; // Set outbound rate limit: 1M tokens per hour -- requires RATE_LIMITER_MANAGER_ROLE oft.set_rate_limit(&Direction::Outbound, &dst_eid, &Some(RateLimitConfig { limit: 1_000_000_000_000, // In local decimals window_seconds: 3600, mode: Mode::Net, // Net: inbound releases decrement outbound in-flight }), &operator); // Check current capacity let capacity = oft.rate_limit_capacity(&Direction::Outbound, &dst_eid); let in_flight = oft.rate_limit_in_flight(&Direction::Outbound, &dst_eid); ``` **Rate limit modes:** | Mode | Behavior | | ------- | ------------------------------------------------------------------------------------ | | `Net` | Inbound credits release outbound capacity (and vice versa). Good for balanced flows. | | `Gross` | Each direction tracked independently. More restrictive. | ## OFT Wire Format For developers building custom integrations, the OFT message wire format: ``` [send_to: 32 bytes][amount_sd: 8 bytes][compose_from: 32 bytes (optional)][compose_msg: variable (optional)] ``` * **Message type 1** (`SEND`): `send_to` + `amount_sd` * **Message type 2** (`SEND_AND_CALL`): `send_to` + `amount_sd` + `compose_from` + `compose_msg` ## Events | Event | Topics | Data | | ---------------------- | ------------------------- | ----------------------------------------------------- | | `OFTSent` | `guid`, `dst_eid`, `from` | `amount_sent_ld`, `amount_received_ld` | | `OFTReceived` | `guid`, `src_eid`, `to` | `amount_received_ld` | | `MsgInspectorSet` | -- | `inspector: Option
` | | `PausedSet` | -- | `paused: bool` | | `DefaultFeeBpsSet` | -- | `fee_bps: Option` | | `FeeBpsSet` | -- | `dst_eid`, `fee_bps: Option` | | `FeeDepositAddressSet` | -- | `fee_deposit_address: Option
` | | `RateLimitSet` | -- | `direction`, `eid`, `config: Option` | ## Best Practices & Deployment Checklist * **Set `shared_decimals` carefully** -- it cannot be changed after deployment. Use 6 for compatibility with most chains. * **Configure peers on both sides** -- the remote OFT must also set your Stellar OFT as a peer. * **Set enforced options** -- always configure minimum gas for each destination to prevent failed deliveries. * **Test with small amounts** -- verify the full send/receive flow with minimal amounts before large transfers. * **Monitor rate limits** -- if enabled, ensure limits are set appropriately for expected volume. * **Grant minting permissions** -- for MintBurn mode, ensure the OFT has the required role on the Mintable contract. ## Next Steps * **[DVN & Executor Config](/v2/developers/stellar/configuration/dvn-executor-config)**: Configure your security stack. * **[Technical Overview](/v2/developers/stellar/technical-overview)**: Understand the full message lifecycle. * **[Troubleshooting](/v2/developers/stellar/troubleshooting/common-errors)**: Common errors and solutions. # LayerZero V2 Stellar Contracts Source: https://docs.layerzero.network/v2/developers/stellar/overview Overview of Stellar Soroban Contracts on LayerZero V2. Learn the architecture, features, and how to get started building crosschain applications on Stellar. LayerZero extends its universal messaging protocol to [Stellar](https://stellar.org/) via [Soroban](https://soroban.stellar.org/), Stellar's smart contract platform. Developers can now build **Omnichain Applications (OApps)** and **Omnichain Fungible Tokens (OFTs)** that connect Stellar to any LayerZero-supported chain. Stellar Soroban contracts are written in **Rust** and compiled to WASM. LayerZero's Stellar integration follows the same protocol design as other supported chains, with Soroban-specific adaptations for authorization, storage, and token handling. ### Stellar Soroban Contracts Learn the key differences between EVM and Stellar development for LayerZero. Create an Omnichain Application (OApp) on Stellar using Soroban smart contracts. Deploy an Omnichain Fungible Token (OFT) on Stellar with crosschain transfer support.
#### Stellar Protocol Configurations Configure which decentralized verifier networks (DVNs) secure your messages. Configure who executes your messages on the destination chain. Set the amount of gas to deliver to the destination chain. ### Tooling and Resources Stellar Soroban development uses Rust and the Stellar CLI. For more information, see the [Soroban Getting Started Guide](https://soroban.stellar.org/docs/getting-started). * **[Stellar CLI](https://developers.stellar.org/docs/build/smart-contracts/getting-started/setup)**: Build, deploy, and interact with Soroban contracts. * **[LayerZero Scan](https://layerzeroscan.com/)**: Track crosschain messages involving Stellar. * **[Ecosystem Tools](/v2/developers/ecosystem-tools)**: Explore community-built tools and integrations. You can also ask for help or follow development in the [Discord](https://layerzero.network/community). # Stellar SDK Source: https://docs.layerzero.network/v2/developers/stellar/sdk Use the LayerZero Stellar TypeScript packages to interact with protocol, OFT, and SAC Manager contracts. LayerZero's Stellar TypeScript bindings are split across three packages: * `@layerzerolabs/lz-v2-stellar-sdk` provides clients for protocol, worker, and view contracts. * `@layerzerolabs/oft-stellar-types` provides the standard OFT client and its OApp methods. * `@layerzerolabs/sac-manager-stellar-types` provides the standard SAC Manager client. Each generated `Client` extends `ContractClient` from `@stellar/stellar-sdk/contract`, providing typed methods for the corresponding Soroban contract. Import clients only from the package roots shown below. The `generated/*` deep imports are not public package exports, and the core SDK no longer exports the OFT or SAC Manager clients. ## Installation ```bash wrap theme={null} npm install @layerzerolabs/lz-v2-stellar-sdk @stellar/stellar-sdk ``` Install the application-specific bindings you use: ```bash wrap theme={null} npm install @layerzerolabs/oft-stellar-types @layerzerolabs/sac-manager-stellar-types ``` ## Client Initialization All generated clients use the same initialization options, but their import styles differ. The protocol SDK exposes each contract as a namespace, while the OFT and SAC Manager packages export `Client` directly: ```typescript wrap theme={null} import { Client as OFTClient } from '@layerzerolabs/oft-stellar-types'; import { endpoint } from '@layerzerolabs/lz-v2-stellar-sdk'; import { Keypair, Networks } from '@stellar/stellar-sdk'; import { basicNodeSigner } from '@stellar/stellar-sdk/contract'; const keypair = Keypair.fromSecret('S...'); const clientOptions = { networkPassphrase: Networks.TESTNET, rpcUrl: 'https://soroban-testnet.stellar.org', publicKey: keypair.publicKey(), ...basicNodeSigner(keypair, Networks.TESTNET), }; const endpointClient = new endpoint.Client({ ...clientOptions, contractId: 'C...ENDPOINT_CONTRACT_ID', }); const oftClient = new OFTClient({ ...clientOptions, contractId: 'C...OFT_CONTRACT_ID', }); ``` All generated clients accept the same `ContractClientOptions`: | Parameter | Type | Description | | ------------------- | ---------- | ------------------------------------------------------------ | | `contractId` | `string` | The deployed Soroban contract address | | `networkPassphrase` | `string` | Network identifier (`Networks.TESTNET` or `Networks.PUBLIC`) | | `rpcUrl` | `string` | Stellar Soroban RPC endpoint URL | | `publicKey` | `string` | Signer's Stellar public key | | `signTransaction` | `function` | Transaction signing callback | | `signAuthEntry` | `function` | Authorization entry signing callback | The `basicNodeSigner(keypair, networkPassphrase)` helper from `@stellar/stellar-sdk/contract` provides both `signTransaction` and `signAuthEntry`. For custom signing flows (e.g., hardware wallets), provide these fields directly. ## Available Clients | Export | Package | Description | | -------------------- | ------------------------------------------ | ---------------------------------------------------------------------------- | | `endpoint` | `@layerzerolabs/lz-v2-stellar-sdk` | Endpoint V2 messaging, nonce management, library configuration, and compose | | `uln302` | `@layerzerolabs/lz-v2-stellar-sdk` | ULN-302 configuration, verification, and commit | | `dvn` | `@layerzerolabs/lz-v2-stellar-sdk` | DVN worker management, verification, and fee calculation | | `executor` | `@layerzerolabs/lz-v2-stellar-sdk` | Executor worker management, execution, and fee calculation | | `priceFeed` | `@layerzerolabs/lz-v2-stellar-sdk` | Price data used for fee calculation | | `layerzeroView` | `@layerzerolabs/lz-v2-stellar-sdk` | Read-only protocol state queries | | `OFT Client` | `@layerzerolabs/oft-stellar-types` | OFT transfers, quoting, OApp configuration, rate limiting, fees, and pausing | | `SAC Manager Client` | `@layerzerolabs/sac-manager-stellar-types` | SAC Manager RBAC and Stellar Asset Contract management | Import protocol clients by their namespace export: ```typescript wrap theme={null} import { endpoint, uln302 } from '@layerzerolabs/lz-v2-stellar-sdk'; const endpointClient = new endpoint.Client({ ...options }); const uln302Client = new uln302.Client({ ...options }); ``` ## Core Methods Reference ### OFT Client The OFT client provides methods for crosschain token transfers and OFT management. #### Token Operations | Method | Parameters | Description | | ------------ | --------------------------------------------- | ---------------------------------- | | `send` | `from`, `send_param`, `fee`, `refund_address` | Send tokens crosschain | | `quote_send` | `from`, `send_param`, `pay_in_zro` | Quote the messaging fee for a send | | `quote_oft` | `from`, `send_param` | Quote OFT token conversion amounts | #### Configuration | Method | Parameters | Description | | ---------------------- | ------------------------- | -------------------------------------------------------------- | | `set_peer` | `eid`, `peer`, `operator` | Set trusted peer address for a destination chain | | `set_delegate` | `delegate`, `operator` | Set the delegate address for Endpoint admin operations | | `set_enforced_options` | `options`, `operator` | Set minimum execution options (array of `EnforcedOptionParam`) | | `set_msg_inspector` | `inspector`, `operator` | Set the message inspector contract address | #### Rate Limiting | Method | Parameters | Description | | ---------------------- | ---------------------------------------- | ---------------------------------------- | | `set_rate_limit` | `direction`, `eid`, `config`, `operator` | Configure rate limit for a pathway | | `rate_limit_config` | `direction`, `eid` | Get rate limit configuration | | `rate_limit_capacity` | `direction`, `eid` | Get remaining capacity in current window | | `rate_limit_in_flight` | `direction`, `eid` | Get amount currently in-flight | #### Fee Management | Method | Parameters | Description | | ------------------------- | --------------------------------- | --------------------------------------------------- | | `set_default_fee_bps` | `default_fee_bps`, `operator` | Set the default fee rate in basis points | | `set_fee_bps` | `dst_eid`, `fee_bps`, `operator` | Set a pathway-specific fee rate | | `set_fee_deposit_address` | `fee_deposit_address`, `operator` | Set the fee recipient address | | `default_fee_bps` | -- | Get the default fee rate | | `fee_bps` | `dst_eid` | Get the fee rate for a specific pathway | | `effective_fee_bps` | `dst_eid` | Get the effective fee (pathway-specific or default) | | `fee_deposit_address` | -- | Get the current fee recipient | #### Pause Control | Method | Parameters | Description | | ----------- | ---------- | ----------------------------------------- | | `pause` | `operator` | Pause the OFT (requires PAUSER\_ROLE) | | `unpause` | `operator` | Unpause the OFT (requires UNPAUSER\_ROLE) | | `is_paused` | -- | Check whether the OFT is paused | #### Ownership | Method | Parameters | Description | | -------------------------- | ------------------ | ----------------------------------- | | `transfer_ownership` | `new_owner` | Transfer ownership immediately | | `begin_ownership_transfer` | `new_owner`, `ttl` | Begin a two-step ownership transfer | | `accept_ownership` | -- | Accept a pending ownership transfer | | `renounce_ownership` | -- | Renounce ownership permanently | #### Queries | Method | Parameters | Description | | ------------------------- | ----------------- | ----------------------------------------------- | | `token` | -- | Get the token contract address | | `shared_decimals` | -- | Get the shared decimal precision | | `decimal_conversion_rate` | -- | Get the local-to-shared decimal conversion rate | | `oft_type` | -- | Get the OFT type | | `oft_version` | -- | Get the OFT interface version | | `peer` | `eid` | Get the peer address for a destination chain | | `enforced_options` | `eid`, `msg_type` | Get enforced options for a pathway | | `msg_inspector` | -- | Get the message inspector address | #### TTL Management | Method | Parameters | Description | | --------------------- | ------------------------ | --------------------------------------------- | | `extend_instance_ttl` | `threshold`, `extend_to` | Extend the contract instance TTL | | `set_ttl_configs` | `instance`, `persistent` | Set TTL configuration parameters | | `ttl_configs` | -- | Get current TTL configuration | | `freeze_ttl_configs` | -- | Freeze TTL configs to prevent further changes | ### Endpoint Client The Endpoint client provides core LayerZero messaging functionality. #### Messaging | Method | Parameters | Description | | ------------------ | ------------------------------------------------------------------------------------------- | ------------------------------------------ | | `send` | `sender`, `params`, `refund_address` | Send a crosschain message | | `quote` | `sender`, `params` | Quote the messaging fee | | `verify` | `receive_lib`, `origin`, `receiver`, `payload_hash` | Verify a received message | | `lz_receive_alert` | `executor`, `origin`, `receiver`, `guid`, `gas`, `value`, `message`, `extra_data`, `reason` | Alert about lz\_receive execution result | | `clear` | `caller`, `origin`, `receiver`, `guid`, `message` | Clear a verified message without executing | | `skip` | `caller`, `receiver`, `src_eid`, `sender`, `nonce` | Skip a message nonce | | `nilify` | `caller`, `receiver`, `src_eid`, `sender`, `nonce`, `payload_hash` | Nilify a pending message | | `burn` | `caller`, `receiver`, `src_eid`, `sender`, `nonce`, `payload_hash` | Burn a nilified message | #### Compose | Method | Parameters | Description | | ------------------ | -------------------------------------------------------------------------------------------- | ------------------------------------ | | `send_compose` | `from`, `to`, `guid`, `index`, `message` | Send a compose message | | `clear_compose` | `composer`, `from`, `guid`, `index`, `message` | Clear a compose message | | `lz_compose_alert` | `executor`, `from`, `to`, `guid`, `index`, `gas`, `value`, `message`, `extra_data`, `reason` | Alert about compose execution result | | `compose_queue` | `from`, `to`, `guid`, `index` | Get compose queue status | #### Library Management | Method | Parameters | Description | | --------------------- | ---------------------------------------------------------- | ------------------------------------- | | `set_send_library` | `caller`, `sender`, `dst_eid`, `new_lib` | Set the send library for a pathway | | `set_receive_library` | `caller`, `receiver`, `src_eid`, `new_lib`, `grace_period` | Set the receive library for a pathway | | `set_config` | `caller`, `oapp`, `lib`, `params` | Set library-level configuration | | `set_delegate` | `oapp`, `new_delegate` | Set the delegate address | | `get_config` | `oapp`, `lib`, `eid`, `config_type` | Get library-level configuration | #### Nonce and State | Method | Parameters | Description | | ---------------------- | ---------------------------------------- | ---------------------------- | | `outbound_nonce` | `sender`, `dst_eid`, `receiver` | Get the outbound nonce | | `inbound_nonce` | `receiver`, `src_eid`, `sender` | Get the inbound nonce | | `next_guid` | `sender`, `dst_eid`, `receiver` | Get the next message GUID | | `inbound_payload_hash` | `receiver`, `src_eid`, `sender`, `nonce` | Get the inbound payload hash | #### Library Registration | Method | Parameters | Description | | -------------------------- | --------------------- | --------------------------------------------- | | `register_library` | `new_lib` | Register a new message library | | `is_registered_library` | `lib` | Check if a library is registered | | `get_registered_libraries` | `start`, `max_count` | Get registered libraries (paginated) | | `default_send_library` | `dst_eid` | Get the default send library | | `default_receive_library` | `src_eid` | Get the default receive library | | `get_send_library` | `sender`, `dst_eid` | Get the effective send library for an OApp | | `get_receive_library` | `receiver`, `src_eid` | Get the effective receive library for an OApp | ### ULN-302 Client The ULN-302 client configures the Ultra Light Node message verification library. #### Send Configuration | Method | Parameters | Description | | ------------------------------ | ------------------- | -------------------------------------------------------- | | `set_default_send_uln_configs` | `params` | Set default send ULN configs (admin) | | `default_send_uln_config` | `dst_eid` | Get default send ULN config | | `oapp_send_uln_config` | `sender`, `dst_eid` | Get OApp-specific send ULN config | | `effective_send_uln_config` | `sender`, `dst_eid` | Get the effective send config (OApp override or default) | #### Receive Configuration | Method | Parameters | Description | | --------------------------------- | --------------------- | --------------------------------------- | | `set_default_receive_uln_configs` | `params` | Set default receive ULN configs (admin) | | `default_receive_uln_config` | `src_eid` | Get default receive ULN config | | `oapp_receive_uln_config` | `receiver`, `src_eid` | Get OApp-specific receive ULN config | | `effective_receive_uln_config` | `receiver`, `src_eid` | Get the effective receive config | #### Executor Configuration | Method | Parameters | Description | | ------------------------------ | ------------------- | ------------------------------------ | | `set_default_executor_configs` | `params` | Set default executor configs (admin) | | `default_executor_config` | `dst_eid` | Get default executor config | | `oapp_executor_config` | `sender`, `dst_eid` | Get OApp-specific executor config | | `effective_executor_config` | `sender`, `dst_eid` | Get the effective executor config | #### Verification | Method | Parameters | Description | | --------------------- | ------------------------------------------------------- | --------------------------------------------- | | `verify` | `dvn`, `packet_header`, `payload_hash`, `confirmations` | Submit a DVN verification | | `commit_verification` | `packet_header`, `payload_hash` | Commit a fully verified message | | `confirmations` | `dvn`, `header_hash`, `payload_hash` | Get the confirmation count for a verification | | `verifiable` | `packet_header`, `payload_hash` | Check if a message can be committed | #### Queries | Method | Parameters | Description | | ------------------ | --------------------------------- | ------------------------------------ | | `set_config` | `oapp`, `params` | Set library-level configuration | | `get_config` | `eid`, `oapp`, `config_type` | Get library-level configuration | | `is_supported_eid` | `eid` | Check if an endpoint ID is supported | | `version` | -- | Get the library version | | `message_lib_type` | -- | Get the message library type | | `quote` | `packet`, `options`, `pay_in_zro` | Quote verification fees | | `send` | `packet`, `options`, `pay_in_zro` | Send a message through the library | ## Next Steps * [OFT Overview](/v2/developers/stellar/oft/overview) -- OFT architecture and deployment guide * [OApp Overview](/v2/developers/stellar/oapp/overview) -- Base messaging standard * [DVN/Executor Config](/v2/developers/stellar/configuration/dvn-executor-config) -- Security stack configuration * [Technical Overview](/v2/developers/stellar/technical-overview) -- Stellar architecture fundamentals # Stellar Technical Overview Source: https://docs.layerzero.network/v2/developers/stellar/technical-overview Comprehensive technical reference for LayerZero on Stellar. Covers Soroban architecture, the full message lifecycle (send, verify, receive), proc macros, storage model, and protocol internals. This article covers Stellar's Soroban smart contract platform and the full LayerZero V2 protocol lifecycle from the perspective of a LayerZero developer. **What you'll learn:** * Soroban's WASM-based VM architecture, address types, and gas model * The complete message lifecycle: send, verify, receive, and recovery * Contract development model: proc macros, storage, TTL, and upgrades * Operations and security: ownership, delegation, and RBAC * Technical constraints specific to Stellar If you're new to Stellar, start with the [Getting Started](/v2/developers/stellar/getting-started) guide to understand the key differences from EVM before diving into the technical details. ## VM Architecture Soroban is Stellar's smart contract runtime. Contracts are written in Rust, compiled to **WebAssembly (WASM)**, and executed in a sandboxed VM. ### Key Characteristics | Property | Detail | | ---------------------- | ------------------------------ | | **Language** | Rust | | **Compilation target** | `wasm32v1-none` | | **SDK** | `soroban-sdk` v25.1.1 | | **Execution model** | Single-threaded, deterministic | ### Address Types Stellar uses StrKey-encoded addresses with the structure: **1-byte version + 32-byte payload + 2-byte checksum** (35 bytes total, base32-encoded to 56 characters): | Address Type | First Char | Payload | Usage | | ------------- | ---------- | -------------------------- | ----------------------- | | **G-address** | `G` | 32-byte Ed25519 public key | Accounts (EOAs) | | **C-address** | `C` | 32-byte contract ID | Soroban smart contracts | #### Crosschain Address Format LayerZero V2 standardizes all crosschain addresses as `bytes32`. Since G-addresses and C-addresses share the same 32-byte payload structure, LayerZero uses this payload directly as the `bytes32` representation. The payload is guaranteed to be unique across both address types. When receiving crosschain messages, the protocol resolves the `bytes32` back to a Stellar address using **deterministic address resolution**: * **OApp and Composer addresses**: The `bytes32` value is interpreted directly as a contract ID (C-address). * **Native drop and OFT receiver addresses**: Contract-first detection is applied — if a contract exists at the address, it is treated as a C-address; otherwise, it is treated as a G-address (account). ### XDR Encoding Where EVM uses ABI encoding for function calls and configuration, Soroban uses **XDR (External Data Representation)**. This applies to message library configuration (ULN configs, executor configs), cross-contract calls, and event data. The TypeScript SDK handles XDR encoding/decoding automatically. ### Gas Model Soroban uses a **resource-based fee model** rather than EVM-style gas: | Resource | Description | | ---------------------- | --------------------------------- | | **CPU instructions** | Computation cost | | **Memory bytes** | Runtime memory allocation | | **Read/write bytes** | Ledger storage I/O | | **Read/write entries** | Number of ledger entries accessed | | **Transaction size** | Byte size of the transaction | | **Events** | Size of emitted events | Fees are calculated based on resource consumption and a network-wide base fee multiplier. Unlike EVM, there is no concept of "gas price" that fluctuates with demand -- fees are deterministic based on the resources declared upfront. Soroban transactions must **declare their resource limits upfront** (CPU, memory, storage). If the transaction exceeds the declared limits, it fails. The Stellar CLI and SDK estimate these limits automatically when simulating transactions. ## Message Lifecycle ### Overview ```mermaid theme={null} sequenceDiagram participant User participant OApp as OApp (Source) participant EP as Endpoint V2 (Source) participant ULN as ULN-302 (Source) participant DVN as DVN (Off-chain) participant Exec as Executor (Off-chain) participant DstULN as ULN-302 (Dest) participant DstEP as Endpoint V2 (Dest) participant DstOApp as OApp (Dest) User->>OApp: send(from, send_param, fee, refund_address) OApp->>EP: send(sender, messaging_params, refund_address) EP->>ULN: send(packet, options) Note over EP: Emit PacketSent event DVN-->>DVN: Observe event, verify on destination DVN->>DstULN: verify(dvn, packet_header, payload_hash, confirmations) Note over DstULN: Store DVN attestation DstULN->>DstULN: commit_verification(packet_header, payload_hash) DstULN->>DstEP: verify(receive_lib, origin, receiver, payload_hash) Note over DstEP: Emit PacketVerified event Exec->>DstOApp: lz_receive(executor, origin, guid, message, extra_data, value) DstOApp->>DstEP: clear(caller, origin, receiver, guid, message) Note over DstEP: Emit PacketDelivered event ``` ### Core Data Structures #### MessagingParams The parameters passed to the Endpoint's `send` function: ```rust wrap theme={null} struct MessagingParams { dst_eid: u32, // Destination endpoint ID receiver: BytesN<32>, // Recipient address (32-byte canonical form) message: Bytes, // Application-level message payload options: Bytes, // Execution options (gas, value, etc.) pay_in_zro: bool, // Pay fees in ZRO token instead of native } ``` #### Origin Identifies the source of an inbound message: ```rust wrap theme={null} struct Origin { src_eid: u32, // Source endpoint ID sender: BytesN<32>, // Sender address (32-byte canonical form) nonce: u64, // Message sequence number } ``` #### MessagingFee Fee breakdown for sending a message: ```rust wrap theme={null} struct MessagingFee { native_fee: i128, // Fee in native token (XLM) zro_fee: i128, // Fee in ZRO token (if pay_in_zro = true) } ``` #### MessagingReceipt Returned after a successful send: ```rust wrap theme={null} struct MessagingReceipt { guid: BytesN<32>, // Globally unique message identifier nonce: u64, // Outbound nonce for this path fee: MessagingFee, // Actual fees charged } ``` ### Send Workflow #### Step 1: Application Prepares Send The OApp (or OFT) prepares the message and quotes the fee: ```rust wrap theme={null} // Quote the messaging fee let fee = oft.quote_send(&from, &send_param, &false); // Execute the send let (receipt, oft_receipt) = oft.send(&from, &send_param, &fee, &refund_address); ``` #### Step 2: Endpoint Send The OApp calls the Endpoint's `send` function: 1. The OApp transfers the native fee to the Endpoint: `native_token.transfer(fee_payer, endpoint, native_fee)` 2. If paying in ZRO, transfers ZRO fee similarly 3. The Endpoint assigns the next outbound nonce and computes the GUID 4. The Endpoint calls the configured send library (ULN-302) #### Step 3: Message Library Processing ULN-302 processes the outbound packet: 1. Encodes the packet (header + payload) 2. Computes the payload hash 3. Calculates DVN, executor, and treasury fees After the send library returns, the Endpoint emits the `PacketSent` event with the encoded packet and options. **Send Events:** | Event | Topics | Data | | -------------------- | ------------------------- | ------------------------------------------- | | `PacketSent` | -- | `encoded_packet`, `options`, `send_library` | | `OFTSent` (OFT only) | `guid`, `dst_eid`, `from` | `amount_sent_ld`, `amount_received_ld` | ### Verification Workflow After the `PacketSent` event is emitted, off-chain DVNs observe the event and verify the message on the destination chain. #### Step 1: DVN Submits Verification Each configured DVN calls `verify` on the destination's ULN-302: ```rust wrap theme={null} // DVN submits verification (on destination chain) uln302.verify(&dvn, &packet_header, &payload_hash, &confirmations); ``` The DVN authenticates via Soroban's custom account contract (multisig with secp256k1 signatures). #### Step 2: Threshold Check ULN-302 checks whether enough DVNs have verified: * **All required DVNs** must verify * **At least `optional_dvn_threshold`** optional DVNs must verify The `verifiable` function checks the current verification status without committing: ```rust wrap theme={null} let is_ready = uln302.verifiable(&packet_header, &payload_hash); ``` #### Step 3: Commit Verification Once the threshold is met, anyone can call `commit_verification` (permissionless): ```rust wrap theme={null} uln302.commit_verification(&packet_header, &payload_hash); ``` This calls `endpoint.verify(...)`, which stores the payload hash and emits `PacketVerified`. **Verification Events:** | Event | Topics | Data | | ---------------- | -------------------- | -------------- | | `PacketVerified` | `origin`, `receiver` | `payload_hash` | ### Receive Workflow After verification, the Executor delivers the message to the destination OApp. #### Step 1: Executor Calls lz\_receive The Executor calls `lz_receive` on the destination OApp: ```rust wrap theme={null} oapp.lz_receive( &executor, // Executor address (must authorize) &origin, // Source chain info &guid, // Message GUID &message, // Application payload &extra_data, // Additional data &value, // Native token value to forward ); ``` #### Step 2: OApp Validates and Clears The OApp's `lz_receive` implementation (provided by the `OAppReceiver` trait's default method): 1. **Validates executor**: `executor.require_auth()` 2. **Validates sender**: Checks `origin.sender` matches the configured peer for `origin.src_eid` 3. **Forwards value**: If `value != 0`, transfers native token from executor to OApp 4. **Clears payload**: Calls `endpoint.clear(oapp, origin, oapp, guid, message)` to mark the message as delivered 5. **Executes business logic**: Calls `__lz_receive(origin, guid, message, extra_data, executor, value)` (your custom implementation) #### Step 3: Application Logic Your `__lz_receive` implementation processes the decoded message. For OFTs, this includes token crediting (unlock or mint), address resolution, and optional compose handling -- see [OFT Overview](/v2/developers/stellar/oft/overview#receiving-tokens) for details. **Receive Events:** | Event | Topics | Data | | ------------------------ | ----------------------- | -------------------- | | `PacketDelivered` | `origin`, `receiver` | -- | | `OFTReceived` (OFT only) | `guid`, `src_eid`, `to` | `amount_received_ld` | #### Step 4: Compose (Optional) If the message includes a compose payload, the Endpoint stores it and the Executor triggers the compose call in a separate transaction: 1. OApp calls `endpoint.send_compose(from, to, guid, index, compose_msg)` 2. Executor calls `composer.lz_compose(executor, from, guid, index, message, extra_data, value)` on the target composer contract 3. Composer processes the compose message (e.g., swap, stake, etc.) **Compose Events:** | Event | Topics | Data | | ------------- | ----------------------------- | --------- | | `ComposeSent` | `from`, `to`, `guid`, `index` | `message` | ### Recovery Operations If a message fails to deliver or gets stuck, several recovery operations are available. The `caller` must be the receiving OApp itself or its registered delegate. #### Skip Skip the **next expected** inbound nonce without processing the message. The provided nonce must equal `inbound_nonce + 1` -- you cannot skip arbitrary nonces. Useful for ordered messaging when a message should be bypassed: ```rust wrap theme={null} endpoint.skip(&caller, &receiver, &src_eid, &sender, &nonce); ``` #### Clear Remove a verified payload that hasn't been delivered. The nonce in `origin` must be at or below `inbound_nonce`, and the hash derived from `guid` and `message` must match the stored payload hash: ```rust wrap theme={null} endpoint.clear(&caller, &origin, &receiver, &guid, &message); ``` #### Nilify Mark a message as nil (void), preventing execution until re-verified. The supplied `payload_hash` must exactly match the currently stored value. For a verified, unexecuted nonce, pass its stored hash. To pre-emptively nilify a future nonce, the nonce must be greater than `inbound_nonce` but no greater than `inbound_nonce + 256`; pass `None` when no hash is currently stored. A nonce at or below `inbound_nonce` can be nilified only while it still has a stored payload hash: ```rust wrap theme={null} // Use Some(payload_hash) for an existing verified hash. // For a future nonce with no stored hash, set expected_payload_hash to None instead. let expected_payload_hash = Some(payload_hash); endpoint.nilify(&caller, &receiver, &src_eid, &sender, &nonce, &expected_payload_hash); ``` #### Burn Permanently burn a nonce. The nonce must be at or below `inbound_nonce`, and `payload_hash` must match a hash currently stored for that nonce. Unlike nilify, the removed hash cannot be restored by re-verification: ```rust wrap theme={null} endpoint.burn(&caller, &receiver, &src_eid, &sender, &nonce, &payload_hash); ``` **Recovery Operations Summary:** | Operation | When to Use | Key Preconditions | Reversible? | | --------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | | `skip` | Skip the next nonce in ordered delivery | `nonce == inbound_nonce + 1` | No | | `clear` | Remove an undelivered verified payload | `nonce <= inbound_nonce`; the supplied payload must match the stored hash | No | | `nilify` | Void a verified message or pre-emptively block a future nonce | The supplied optional hash must match the stored value; either `nonce > inbound_nonce` (and within the 256-nonce pending window) or a hash must already exist | Yes (re-verify) | | `burn` | Permanently destroy a nonce | `nonce <= inbound_nonce`; a matching stored hash must exist | No | *** ## Contract Development Model ### Trait Composition with Proc Macros Soroban contracts use Rust's proc macros instead of inheritance: | Macro | Purpose | | ----------------------------- | --------------------------------------------------------------------------- | | `#[oapp]` | Generates OApp trait implementations (Core, Sender, Receiver, OptionsType3) | | `#[oapp(custom = [...])]` | Skip generating specific traits for custom implementations | | `#[storage]` | Generates typed storage accessors from an enum | | `#[contract_impl]` | Extends contract functions with automatic TTL management | | `#[only_auth]` | Auth-gated function attribute | | `#[only_role(account, ROLE)]` | Role-based access control check | | `#[ownable]` | Generates ownership management (single-step and 2-step transfer) | | `#[upgradeable]` | Contract upgrade + migration support | | `#[lz_contract]` | Combines `#[contract]` + `#[ownable]` + TTL management | | `#[ttl_configurable]` | Allows adjusting TTL threshold and extension parameters | | `#[ttl_extendable]` | Automatically extends TTL on storage access | **Example:** ```rust wrap theme={null} #[oapp] pub struct MyOFT; // The #[oapp] macro generates: // - OAppCore (endpoint, peer, set_peer, set_delegate) // - OAppSenderInternal (__quote, __lz_send) // - OAppReceiver (lz_receive, allow_initialize_path, next_nonce) // - OAppOptionsType3 (enforced_options, set_enforced_options, combine_options) ``` ### Storage Model Soroban contracts use a typed enum with the `#[storage]` macro to define storage layout: ```rust wrap theme={null} #[storage] enum OFTStorage { #[instance(u32)] DecimalsDiff, // Immutable after construction #[instance(Address)] Token, // Immutable after construction #[instance(Address)] MsgInspector, // Optional, mutable } ``` The macro generates typed `get`, `set`, `has`, and `remove` methods for each variant. #### Storage Tiers | Tier | Annotation | TTL | Use in LayerZero | | -------------- | ------------------ | ---------------- | ----------------------------------------- | | **Instance** | `#[instance(T)]` | Tied to contract | Endpoint address, token address, decimals | | **Persistent** | `#[persistent(T)]` | Must be extended | Peer mappings, rate limit state | | **Temporary** | `#[temporary(T)]` | Short-lived | Caches, intermediate computation | #### TTL Management All Soroban storage entries have a time-to-live. LayerZero contracts use the `#[ttl_configurable]` and `#[ttl_extendable]` macros to manage TTL automatically: * **Instance storage**: TTL extended automatically on every contract call via `#[contract_impl]` * **Persistent storage**: TTL extended automatically when entries are read or written * **Temporary storage**: Short-lived, no automatic extension If a persistent entry's TTL expires, it becomes **archived**. It still exists on the ledger but must be restored (with a fee) before it can be read. Ensure your operational processes monitor and extend TTLs for critical state. #### Manual TTL Configuration Contracts with `#[ttl_configurable]` allow adjusting TTL thresholds: ```bash wrap theme={null} # Set TTL extension parameters (owner/admin only) # Values are in ledgers (~5 seconds each) stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ set_ttl_configs \ --instance '{"threshold": 1000, "extend_to": 5000}' \ --persistent '{"threshold": 1000, "extend_to": 5000}' ``` #### Monitoring TTL For operational monitoring, check the TTL of critical entries: ```bash wrap theme={null} stellar contract read \ --id \ --network testnet \ --key \ --durability persistent ``` Monitor persistent storage TTLs in production. If a peer mapping entry expires and gets archived, the OApp won't be able to validate inbound messages from that chain until the entry is restored. ### Contract Upgrades LayerZero Soroban contracts support native upgrades via the `#[upgradeable]` macro: ```rust wrap theme={null} // Upgrade to a new WASM implementation contract.upgrade(&new_wasm_hash); // Must call migrate after upgrade to clear the migration flag contract.migrate(&migration_data); ``` **Upgrade flow:** 1. Upload the new WASM to the network (but don't create a contract instance) 2. Call `upgrade` on the existing contract with the new WASM hash 3. The contract code is replaced atomically 4. Call `migrate` to clear the migration flag and optionally adjust state Always test upgrades on testnet first. Verify that storage layout is compatible -- adding new storage variants is safe, but removing or reordering existing ones is not. *** ## Operations & Security ### Ownership LayerZero Soroban contracts support both single-step and **2-step ownership transfer**: * **Single-step**: `transfer_ownership(new_owner)` -- immediate, irreversible (will fail if a 2-step transfer is already pending) * **2-step (recommended)**: 1. Current owner calls `begin_ownership_transfer(new_owner, ttl)` -- proposes transfer with a TTL window (in ledgers, \~5s each) 2. New owner calls `accept_ownership()` -- confirms transfer within the TTL 3. If not accepted within the TTL, the proposal expires and the original owner retains ownership This prevents accidental ownership loss from typos or incorrect addresses. ```bash wrap theme={null} # Step 1: Begin transfer (2-step) # TTL is in ledgers (~5s each). 120960 ledgers ≈ 1 week. stellar contract invoke \ --id \ --network testnet \ --source current-owner \ -- \ begin_ownership_transfer \ --new_owner \ --ttl 120960 ``` ```bash wrap theme={null} # Step 2: Accept transfer (from new owner) stellar contract invoke \ --id \ --network testnet \ --source new-owner \ -- \ accept_ownership ``` ### Delegate The delegate can manage endpoint configuration (message libraries, DVN config) without owner-level access: ```bash wrap theme={null} stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ set_delegate \ --delegate \ --operator ``` Set to `None` to remove the delegate. ### Role-Based Access Control `RoleBasedAccessControl` adds OpenZeppelin-style role membership so administrative actions can be delegated without surrendering ownership. A vanilla `#[lz_contract] #[oapp]` contract exposes both layers automatically. Some contracts (e.g., SAC Manager) use RBAC with role-gated functions: ```rust wrap theme={null} #[only_role(operator, "MINTER_ROLE")] fn mint(env: &Env, to: &Address, amount: i128, operator: &Address) { // Only accounts with MINTER_ROLE can call this } ``` *** ## Technical Constraints ### Pending Inbound Nonce Cap LayerZero's Stellar Endpoint uses an eager inbound nonce model with a bounded pending nonce list: * Maximum **256 pending nonces** (`PENDING_INBOUND_NONCE_MAX_LEN`) * Nonces beyond `inbound_nonce + 256` cannot be verified * Pending nonces drain automatically as they become consecutive ### Reentrancy Prohibition Soroban prohibits reentrancy -- contracts cannot call themselves (directly or indirectly) in the same transaction. This is why LayerZero uses **pull mode** for message delivery: the executor calls `OApp.lz_receive()` directly, and the OApp calls `endpoint.clear()` internally to verify and clear the payload. This avoids the Endpoint -> OApp -> Endpoint reentrancy that would occur if the Endpoint delivered messages to the OApp directly. ## Next Steps * **[Build an OApp](/v2/developers/stellar/oapp/overview)**: Create your first Omnichain Application. * **[Build an OFT](/v2/developers/stellar/oft/overview)**: Deploy an Omnichain Fungible Token. * **[DVN & Executor Config](/v2/developers/stellar/configuration/dvn-executor-config)**: Configure your security stack. * **[Troubleshooting](/v2/developers/stellar/troubleshooting/common-errors)**: Common errors and how to resolve them. # Common Errors on Stellar Source: https://docs.layerzero.network/v2/developers/stellar/troubleshooting/common-errors Troubleshoot common errors when developing and deploying LayerZero contracts on Stellar Soroban. Includes error codes, causes, and solutions. ## Build Errors ### WASM Compilation Failure **Error Message:** ```bash wrap theme={null} error[E0463]: can't find crate for `std` ``` **Cause:** Soroban contracts target `wasm32v1-none` which is a `no_std` environment. Using standard library features that aren't available. **Solution:** Ensure your contract crate uses `#![no_std]`: ```rust wrap theme={null} #![no_std] use soroban_sdk::{contract, Env}; ``` And verify the WASM target is installed: ```bash wrap theme={null} rustup target add wasm32v1-none ``` ### Missing WASM Target **Error Message:** ```bash wrap theme={null} error: target `wasm32v1-none` is not installed ``` **Cause:** The WASM compilation target is not installed in your Rust toolchain. **Solution:** ```bash wrap theme={null} rustup target add wasm32v1-none ``` ## Deployment Errors ### Insufficient Funds **Error Message:** ```bash wrap theme={null} Error: transaction simulation failed: insufficient funds ``` **Cause:** Your account doesn't have enough XLM to cover the deployment transaction fees and contract storage rent. **Solution:** Fund your testnet account: ```bash wrap theme={null} stellar keys fund my-account --network testnet ``` For mainnet, ensure sufficient XLM balance for the deployment plus storage rent deposit. ### Contract Already Exists **Error Message:** ```bash wrap theme={null} Error: contract already exists at address ``` **Cause:** A contract with the same deployer/source account and salt already exists at the computed address. **Solution:** Use a different salt or deployer account. Contract addresses are deterministic based on the deployer and salt. ### Constructor Failed **Error Message:** ```bash wrap theme={null} Error: HostError: Error(Contract, #) ``` **Cause:** The constructor arguments are invalid or a dependency (e.g., endpoint address) is incorrect. **Solution:** Verify constructor arguments match the contract type: **OApp:** `owner`, `endpoint`, `delegate` * `endpoint` must be a valid, deployed Endpoint V2 contract * `owner` and `delegate` must be valid addresses **OFT:** `token`, `shared_decimals`, `oft_type`, `endpoint`, `delegate` * `token` must be a valid SEP-41 token contract * `shared_decimals` must be ≤ the token's local decimals * `endpoint` must be a valid, deployed Endpoint V2 contract * `delegate` must be a valid address (serves as the initial owner) ## Configuration Errors ### No Peer Set **Error Code:** `2001` (`NoPeer`) **Cause:** Attempted to send a message to a destination that has no configured peer. **Solution:** ```bash wrap theme={null} stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ set_peer \ --eid \ --peer \ --operator ``` ### Only Peer **Error Code:** `2002` (`OnlyPeer`) **Cause:** Received a message from an address that doesn't match the configured peer for the source chain. **Solution:** Verify that the peer address set on this chain matches the actual contract address on the remote chain. Peer addresses must be in 32-byte format (left-padded with zeros for EVM addresses). ### Invalid Options **Error Code:** `2000` (`InvalidOptions`) **Cause:** Enforced options or caller options are malformed or not Type 3 format. **Solution:** Ensure options follow Type 3 format. Only Type 3 options can be combined. Check that enforced options are set correctly for the destination and message type. ## OFT Errors ### Slippage Exceeded **Error Code:** `3005` (`SlippageExceeded`) **Cause:** The received amount after fees and dust removal is below `min_amount_ld`. **Solution:** Decrease the `min_amount_ld` value in your `SendParam` to allow more slippage tolerance, or reduce OFT fees. Use `quote_oft` to preview the expected receipt before sending: ```rust wrap theme={null} let (limit, fees, receipt) = oft.quote_oft(&from, &send_param); // receipt.amount_received_ld shows what the recipient will get ``` ### Invalid Amount **Error Code:** `3001` (`InvalidAmount`) **Cause:** `amount_ld` or `min_amount_ld` is negative. **Solution:** Ensure both `amount_ld` and `min_amount_ld` in `SendParam` are non-negative. ### Paused **Error Code:** `3110` (`Paused`) **Cause:** The OFT is paused. All send, receive, and quote operations are blocked. **Solution:** Call `unpause` from an account that has `UNPAUSER_ROLE`: ```bash wrap theme={null} stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ unpause \ --operator ``` ### Rate Limit Exceeded **Error Code:** `3120` (`ExceededRateLimit`) **Cause:** The transfer amount exceeds the configured rate limit capacity. **Solution:** Wait for capacity to replenish (tokens decay linearly over the configured window), or adjust the rate limit: ```bash wrap theme={null} # Check current capacity stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ rate_limit_capacity \ --direction Outbound \ --eid ``` ## Endpoint Errors ### Unauthorized **Error Code:** `22` (`Unauthorized`) **Cause:** The caller is not authorized to perform the action. Could be: not the owner, not the delegate, not the registered library, or `require_auth()` was not satisfied. **Solution:** Verify you're calling from the correct account (owner or delegate) and that your transaction includes the proper authorization. ### Path Not Initializable **Error Code:** `18` (`PathNotInitializable`) **Cause:** The OApp's `allow_initialize_path` returned `false` for the given origin. This means the sender is not a configured peer. **Solution:** Set the peer on the receiving OApp for the source chain's endpoint ID. ### Payload Hash Not Found **Error Code:** `20` (`PayloadHashNotFound`) **Cause:** Attempting to `clear` a message that hasn't been verified yet, or the payload hash doesn't match. **Solution:** Wait for DVN verification to complete before attempting delivery. Use `uln302.verifiable(packet_header, payload_hash)` to check whether DVNs have reached quorum. If the message is already verifiable but `clear` still fails, verify that the `guid` and `message` exactly match the stored payload, because `endpoint.verifiable(origin, receiver)` only confirms path and nonce state and does not validate the payload hash used by `clear`. ## ULN-302 Errors ### Invalid Config **Error Code:** `6` (`InvalidConfig`) **Cause:** ULN configuration is malformed (e.g., no DVNs specified, invalid threshold). **Solution:** Ensure at least one DVN is configured (required or optional). The `optional_dvn_threshold` must be ≤ the number of optional DVNs. ### Duplicate DVNs **Error Code:** `5` (`DuplicateRequiredDVNs`) or `4` (`DuplicateOptionalDVNs`) **Cause:** The same DVN address appears multiple times in the configuration. **Solution:** Remove duplicate DVN addresses from your configuration. ## Common Gotchas ### TTL Extension During lz\_receive TTL extension during `lz_receive` introduces additional fees. Ensure the executor gas budget accounts for TTL extension overhead. ### Reentrancy Prohibition Soroban prohibits reentrancy -- contracts cannot call themselves (directly or indirectly) in the same transaction. This is why LayerZero uses **pull mode** for message delivery: the executor calls `OApp.lz_receive()` directly, and the OApp calls `endpoint.clear()` internally to verify and clear the payload. This avoids the Endpoint -> OApp -> Endpoint reentrancy that would occur if the Endpoint delivered messages to the OApp directly. ## Error Code Reference ### Endpoint (1–25) | Code | Name | Description | | ---- | ------------------------------ | --------------------------------------------------------------------- | | 1 | `AlreadyRegistered` | Message library already registered | | 2 | `ComposeExists` | Compose message already exists for this guid/index | | 3 | `ComposeNotFound` | No compose message found for this guid/index | | 4 | `DefaultReceiveLibUnavailable` | No default receive library set for this endpoint ID | | 5 | `DefaultSendLibUnavailable` | No default send library set for this endpoint ID | | 6 | `InsufficientNativeFee` | Native fee provided is below the required amount | | 7 | `InsufficientZroFee` | ZRO fee provided is below the required amount | | 8 | `InvalidExpiry` | Library timeout expiry is invalid | | 9 | `InvalidAmount` | Invalid amount parameter | | 10 | `InvalidIndex` | Compose index is invalid | | 11 | `InvalidNonce` | Nonce does not match expected value | | 12 | `InvalidPayloadHash` | Payload hash does not match the verified hash | | 13 | `InvalidReceiveLibrary` | Receive library is not valid for this OApp/endpoint | | 14 | `OnlyNonDefaultLib` | Operation only allowed on non-default libraries | | 15 | `OnlyReceiveLib` | Library is not registered as a receive library | | 16 | `OnlyRegisteredLib` | Library is not registered | | 17 | `OnlySendLib` | Library is not registered as a send library | | 18 | `PathNotInitializable` | OApp rejected the origin (sender is not a configured peer) | | 19 | `PathNotVerifiable` | Path cannot be verified in its current state | | 20 | `PayloadHashNotFound` | Message not yet verified or payload hash mismatch | | 21 | `SameValue` | Setting the same value that's already configured | | 22 | `Unauthorized` | Caller is not authorized (not owner, delegate, or registered library) | | 23 | `UnsupportedEid` | Endpoint ID is not supported | | 24 | `ZeroZroFee` | ZRO fee cannot be zero when ZRO payment is selected | | 25 | `ZroUnavailable` | ZRO token is not configured | ### OApp (2000–2003) | Code | Name | Description | | ---- | --------------------- | ----------------------------------------------------- | | 2000 | `InvalidOptions` | Options are malformed or not Type 3 format | | 2001 | `NoPeer` | No peer configured for the destination chain | | 2002 | `OnlyPeer` | Inbound message sender does not match configured peer | | 2003 | `ZroTokenUnavailable` | ZRO token not available for fee payment | ### OFT Core (3000–3005) | Code | Name | Description | | ---- | ---------------------- | ------------------------------------------------------------------------- | | 3000 | `InvalidAddress` | Invalid address format | | 3001 | `InvalidAmount` | Send amount is zero, negative, or rounds to zero after decimal conversion | | 3002 | `InvalidLocalDecimals` | Local decimals \< shared decimals | | 3003 | `NotInitialized` | OFT not properly initialized | | 3004 | `Overflow` | Arithmetic overflow | | 3005 | `SlippageExceeded` | Received amount below `min_amount_ld` | ### OFT Fee Extension (3100–3102) | Code | Name | Description | | ---- | -------------------------- | ---------------------------------------------------- | | 3100 | `InvalidFeeBps` | Fee basis points are outside the allowed range | | 3101 | `InvalidFeeDepositAddress` | Invalid fee deposit address | | 3102 | `SameValue` | Setting the same fee value that's already configured | ### OFT Pausable Extension (3110–3111) | Code | Name | Description | | ---- | ---------------------- | ---------------------------------- | | 3110 | `Paused` | OFT is paused | | 3111 | `PauseStatusUnchanged` | Setting pause to its current value | ### OFT Rate Limiter Extension (3120–3124) | Code | Name | Description | | ---- | ------------------- | ------------------------------------ | | 3120 | `ExceededRateLimit` | Transfer exceeds rate limit capacity | | 3121 | `InvalidAmount` | Rate limiter amount invalid | | 3122 | `InvalidTimestamp` | Rate limiter timestamp invalid | | 3123 | `InvalidConfig` | Rate limiter config invalid | | 3124 | `SameValue` | Rate limiter same value already set | ### ULN-302 (1–21) | Code | Name | Description | | ---- | --------------------------------- | ----------------------------------------------------------------- | | 1 | `DefaultExecutorConfigNotFound` | No default executor config for this endpoint ID | | 2 | `DefaultReceiveUlnConfigNotFound` | No default receive ULN config for this endpoint ID | | 3 | `DefaultSendUlnConfigNotFound` | No default send ULN config for this endpoint ID | | 4 | `DuplicateOptionalDVNs` | Same DVN address appears multiple times in optional list | | 5 | `DuplicateRequiredDVNs` | Same DVN address appears multiple times in required list | | 6 | `InvalidConfig` | ULN configuration is malformed (e.g., no DVNs, invalid threshold) | | 7 | `InvalidConfigType` | Unrecognized config type parameter | | 8 | `InvalidConfirmations` | Block confirmations value is invalid | | 9 | `InvalidEID` | Endpoint ID is invalid | | 10 | `InvalidFee` | Fee calculation returned an invalid result | | 11 | `InvalidMessageSize` | Message exceeds maximum allowed size | | 12 | `InvalidOptionalDVNCount` | Number of optional DVNs exceeds the maximum | | 13 | `InvalidOptionalDVNs` | Optional DVN list is invalid | | 14 | `InvalidOptionalDVNThreshold` | Optional DVN threshold exceeds the number of optional DVNs | | 15 | `InvalidRequiredDVNCount` | Number of required DVNs exceeds the maximum | | 16 | `InvalidRequiredDVNs` | Required DVN list is invalid | | 17 | `InvalidSenderAddress` | Sender address format is invalid | | 18 | `UlnAtLeastOneDVN` | At least one DVN (required or optional) must be configured | | 19 | `UnsupportedEid` | Endpoint ID is not supported | | 20 | `Verifying` | Message is still being verified by DVNs | | 21 | `ZeroMessageSize` | Message size cannot be zero | ## Debugging Tips 1. **Check error codes**: Soroban errors include a numeric code. Match it against the error code tables in this page or the contract source. 2. **Simulate first**: Use `stellar contract invoke` with `--send=no` to simulate a transaction without submitting it. This reveals errors before spending fees. 3. **Check events**: Use `stellar events` or the RPC's `getEvents` method to inspect emitted events for debugging. 4. **Verify peers**: Most "message rejected" errors come from misconfigured peers. Always verify peers are set on both sides. 5. **Check TTLs**: For "entry not found" errors on entries that should exist, the entry may have been archived due to TTL expiry. ## Next Steps * **[FAQ](/v2/developers/stellar/troubleshooting/faq)**: Frequently asked questions. * **[Technical Overview](/v2/developers/stellar/technical-overview)**: Protocol architecture and Soroban fundamentals. * **[Getting Started](/v2/developers/stellar/getting-started)**: Key differences between EVM and Stellar. # Debugging Messages Source: https://docs.layerzero.network/v2/developers/stellar/troubleshooting/debugging-messages Intervene on inbound LayerZero messages on Stellar using the Endpoint's skip, nilify, burn, and clear methods -- preconditions, authorization, and SDK examples. LayerZero V2 processes messages in two distinct phases: **`Verified`**: the destination chain has received verification from all configured [DVNs](../../../concepts/modular-security/security-stack-dvns) and the message nonce has been committed to the [Endpoint](../../../concepts/protocol/layerzero-endpoint)'s messaging channel. **`Delivered`**: the message has been successfully executed by the [Executor](../../../concepts/permissionless-execution/executors). Unlike EVM, where the Endpoint pushes a verified message to the OApp, Stellar uses **pull mode**: the Executor calls `OApp.lz_receive()` directly, and the OApp calls `endpoint.clear()` internally to verify and clear the payload. See [Reentrancy Prohibition](../technical-overview#reentrancy-prohibition) for why Soroban requires this model. Because verification and execution are independent, the receiving OApp or its delegate can intervene when an inbound message is stuck, has been verified but not executed, or must be abandoned. The Endpoint provides four methods for handling these cases: * **`skip`** -- skip the next expected inbound nonce without verifying it. * **`nilify`** -- set an existing or future nonce's payload hash to NIL, blocking execution while still allowing recovery. * **`burn`** -- permanently mark a nonce as unexecutable and un-verifiable. * **`clear`** -- clear a verified-but-unexecuted message's payload without executing it. For the general debugging workflow, see [Debugging Messages](../../../concepts/troubleshooting/debugging-messages). ## Authorization All four methods run the same on-chain authorization check (`require_oapp_auth`): the `caller` must be the receiver OApp itself **or** the OApp's registered delegate, and it must authorize the call. When you call these methods **off-chain with the SDK** (as shown below), the signer is a keypair, so `caller` must be a **registered delegate** -- a keypair-backed account that can sign the transaction. The OApp-as-`caller` path only applies when the OApp **contract itself** invokes the Endpoint on-chain; it does not apply to a plain SDK call, because a contract address cannot sign an off-chain transaction. See [Change Delegate](/v2/developers/stellar/oapp/overview#step-4-change-delegate-optional) to register a delegate. ## Setup Every example below reuses a single `endpointClient`. Because these are off-chain SDK calls, the signer (`caller`) must be a keypair registered as the OApp's delegate. ```typescript wrap theme={null} import { endpoint } from '@layerzerolabs/lz-v2-stellar-sdk'; import { Keypair, Networks } from '@stellar/stellar-sdk'; import { basicNodeSigner } from '@stellar/stellar-sdk/contract'; const keypair = Keypair.fromSecret('S...'); const endpointClient = new endpoint.Client({ contractId: 'C...ENDPOINT_CONTRACT_ID', networkPassphrase: Networks.TESTNET, rpcUrl: 'https://soroban-testnet.stellar.org', publicKey: keypair.publicKey(), ...basicNodeSigner(keypair, Networks.TESTNET), }); ``` ## skip `endpointClient.skip({ caller, receiver, src_eid, sender, nonce })` **When to use:** Skip the next expected inbound nonce without verifying it, to unblock subsequent messages when a message is stuck, invalid, or must be abandoned. **Preconditions:** * `nonce` must equal `inbound_nonce + 1` (the next expected nonce); otherwise the call reverts with `InvalidNonce`. * `caller` must be the receiver OApp or its delegate. ```typescript wrap theme={null} const tx = await endpointClient.skip({ caller: keypair.publicKey(), // registered delegate keypair (signs the call) receiver: 'C...RECEIVER_OAPP', // the receiving OApp on Stellar src_eid: 40161, // source endpoint ID sender: senderBytes32, // 32-byte source sender (Buffer) nonce: 3n, // must equal inbound_nonce + 1 }); await tx.signAndSend(); ``` A skipped nonce counts as verified and can never be delivered. Once skipped, the payload cannot be recovered. ## nilify `endpointClient.nilify({ caller, receiver, src_eid, sender, nonce, payload_hash })` **When to use:** Set an inbound nonce's payload hash to NIL, which blocks execution while still allowing recovery -- the packet can be re-verified through the MessageLib. It is a precautionary measure against a malicious DVN. There are two cases: * **Existing hash:** for a nonce whose payload was verified but not executed, pass the current on-chain payload hash. * **Future nonce:** for a `nonce` greater than `inbound_nonce` that has no stored hash yet, read the current value with `inbound_payload_hash` and pass the returned `null` value unchanged to nilify it pre-emptively. **Preconditions:** * `payload_hash` represents an `Option` and must exactly equal the payload hash currently stored on-chain for that nonce; otherwise the call reverts with `PayloadHashNotFound`. Although generated TypeScript declarations may model the empty option as `undefined`, the current Stellar SDK decodes the on-chain empty option as `null`. Read the value with `inbound_payload_hash` and pass it through unchanged. * `nonce > inbound_nonce`, or the current payload hash exists; otherwise the call reverts with `InvalidNonce`. A future nonce must also be no greater than `inbound_nonce + 256`, the maximum pending-nonce window. * `caller` must be the receiver OApp or its delegate. * Effect: the payload hash for that nonce is set to NIL (`0xff...ff`); the packet cannot execute until it is re-verified. If `nonce > inbound_nonce`, the nonce is added to the pending list and consecutive nonces may advance `inbound_nonce`. ```typescript wrap theme={null} // Read the current on-chain hash (the current SDK returns null if nothing is stored yet) const { result: currentPayloadHash } = await endpointClient.inbound_payload_hash({ receiver: 'C...RECEIVER_OAPP', src_eid: 40161, sender: senderBytes32, nonce: 3n, }); const tx = await endpointClient.nilify({ caller: keypair.publicKey(), receiver: 'C...RECEIVER_OAPP', src_eid: 40161, sender: senderBytes32, nonce: 3n, payload_hash: currentPayloadHash, // must match exactly; null nilifies a future nonce }); await tx.signAndSend(); ``` ## burn `endpointClient.burn({ caller, receiver, src_eid, sender, nonce, payload_hash })` **When to use:** Permanently mark a verified nonce as unexecutable and un-verifiable -- it can never be verified or executed again. Unlike `clear`, `burn` needs only the nonce's on-chain payload hash, not the full message, so it can eject a nonce even when the original message is unavailable (for example, withheld by a malicious DVN). It still requires that a matching payload hash is currently stored for that nonce. **Preconditions:** * A payload hash must currently be stored for that nonce, and your `payload_hash` (a required `Buffer`) must exactly equal it; otherwise the call reverts with `PayloadHashNotFound`. * `nonce <= inbound_nonce`; otherwise the call reverts with `InvalidNonce`. * `caller` must be the receiver OApp or its delegate. * Effect: the stored payload hash is removed permanently. Because the nonce is at or below `inbound_nonce` and no longer has a stored hash, it can never be re-verified. ```typescript wrap theme={null} const { result: currentPayloadHash } = await endpointClient.inbound_payload_hash({ receiver: 'C...RECEIVER_OAPP', src_eid: 40161, sender: senderBytes32, nonce: 2n, }); if (currentPayloadHash == null) { throw new Error('No payload hash is stored for this nonce'); } const tx = await endpointClient.burn({ caller: keypair.publicKey(), receiver: 'C...RECEIVER_OAPP', src_eid: 40161, sender: senderBytes32, nonce: 2n, // must be <= inbound_nonce payload_hash: currentPayloadHash, // narrowed to Buffer; must match the stored hash exactly }); await tx.signAndSend(); ``` Burning a nonce is irreversible. The message can never be recovered, verified, or executed. ## clear `endpointClient.clear({ caller, origin, receiver, guid, message })` **When to use:** PULL-mode manual acknowledgement -- settle an already-verified message from the Endpoint without push execution. Use it to eject a message that cannot or should not be executed. This is the Stellar equivalent of the EVM `clear`. **Preconditions:** * Provide the full `origin` (`{ nonce, sender, src_eid }`), the `guid`, and the exact `message`. The Endpoint rebuilds the payload from `guid` + `message` and checks it against what was verified; a mismatch reverts with `PayloadHashNotFound`. * `nonce <= inbound_nonce`; otherwise the call reverts with `InvalidNonce`. * `caller` must be the receiver OApp or its delegate. * Effect: removes the stored payload hash for that nonce and emits `PacketDelivered`. It does **not** advance `inbound_nonce` -- on Stellar the inbound nonce is advanced only by verification, `skip`, or `nilify`. ```typescript wrap theme={null} const tx = await endpointClient.clear({ caller: keypair.publicKey(), origin: { nonce: 3n, sender: senderBytes32, // 32-byte source sender (Buffer) src_eid: 40161, }, receiver: 'C...RECEIVER_OAPP', guid: guidBytes32, // 32-byte message GUID (Buffer) message: messageBytes, // the exact message payload (Buffer) }); await tx.signAndSend(); ``` # Stellar FAQ Source: https://docs.layerzero.network/v2/developers/stellar/troubleshooting/faq Frequently asked questions about building LayerZero applications on Stellar Soroban. ## General Stellar uses [Soroban](https://soroban.stellar.org/), a smart contract platform where contracts are written in **Rust** and compiled to **WebAssembly (WASM)**. The primary SDK is `soroban-sdk` (v25.1.1 for LayerZero contracts). Key differences include: * **Authorization**: Uses `require_auth()` instead of `msg.sender` * **Token standard**: SEP-41 instead of ERC-20 * **Contract composition**: Trait composition with proc macros instead of inheritance * **Storage**: Three-tiered storage (instance, persistent, temporary) with TTL management * **Fee payment**: Explicit SEP-41 token transfer instead of `msg.value` * **Address format**: 32 bytes (contract or account) instead of 20 bytes See the [Technical Overview](/v2/developers/stellar/technical-overview) for a detailed comparison. No. LayerZero deploys and maintains the Endpoint V2 contract on Stellar. You only need to: 1. Deploy your OApp or OFT contract, passing the Endpoint address in the constructor 2. Set peers for each remote chain 3. Configure enforced options and security stack (DVNs) See [OApp Overview](/v2/developers/stellar/oapp/overview) for the deployment flow. ## Development Use Rust 1.90.0 or later with the `wasm32v1-none` target. Pin your version with a `rust-toolchain.toml` file: ```toml wrap theme={null} [toolchain] channel = "1.90.0" targets = ["wasm32v1-none"] ``` The `#[oapp]` macro generates implementations for `OAppCore`, `OAppSenderInternal`, `OAppReceiver`, and `OAppOptionsType3`. Apply it to your contract struct together with `#[lz_contract]`: ```rust wrap theme={null} #[lz_contract] #[oapp] pub struct MyOApp; ``` You must always implement `LzReceiveInternal` for your receive logic. To customize specific traits, use `#[oapp(custom = [receiver])]`. * **LockUnlock**: Tokens are locked in the OFT contract on send and unlocked on receive. Use this when you don't control the token's minting capability. * **MintBurn**: Tokens are burned on send and minted on receive via an external `Mintable` contract. Use this when you control the token supply. Both modes are configured at deployment via the `OftType` enum in the constructor. Stellar has contract addresses (C-addresses) and account addresses (G-addresses), both 32 bytes. LayerZero's `resolve_address` utility handles disambiguation automatically by checking if a contract exists at the address. You typically don't need to handle this yourself -- the OFT contract resolves addresses when crediting tokens to recipients. OFTs use **shared decimals** for crosschain consistency: * **Local decimals**: Token's native decimals on the chain (e.g., 7 for Stellar) * **Shared decimals**: Crosschain precision (typically 6) * **Conversion rate**: `10^(local - shared)` When sending, amounts are divided by the conversion rate, removing dust. The truncated amount is sent crosschain, and the destination multiplies by its own conversion rate. For example, with local decimals = 7 and shared decimals = 6, the conversion rate is 10. An amount of `1,000,015` becomes `1,000,010` (the trailing 5 is dust). `shared_decimals` is set at deployment and **cannot be changed**. Use 6 for compatibility with most chains. See [OFT Overview - Decimal Precision](/v2/developers/stellar/oft/overview#decimal-precision) for details. ## Gas and Fees Unlike EVM where fees are paid via `msg.value`, Stellar requires explicit SEP-41 token transfers. The OApp base contract handles this internally -- it transfers XLM (native token) to the Endpoint before calling `send`. As a developer, you just pass the fee from the quote function (`__quote` for OApp, `quote_send` for OFT) to the send function. Soroban uses resource-based fees rather than gas. You're charged for: * **CPU instructions**: Computation cost * **Memory**: Runtime memory allocation * **Storage I/O**: Read/write bytes and entries * **Transaction size**: Byte size of the transaction Transactions declare resource limits upfront. The Stellar CLI estimates these automatically during simulation. Soroban storage operations are priced to reflect the real cost of maintaining ledger state. Each storage entry requires rent to maintain its TTL. Writing to persistent storage is more expensive than temporary storage. To minimize costs, keep your storage footprint small and use temporary storage where possible. ## Configuration DVN configuration is done through the ULN-302 message library via the Endpoint's `set_config` function. You provide an XDR-encoded `OAppUlnConfig` with config type `2` (send) or `3` (receive). See [DVN & Executor Config](/v2/developers/stellar/configuration/dvn-executor-config) for detailed instructions. Enforced options define the **minimum execution parameters** (gas limit, native value) for messages sent to each destination. Without them, messages may fail on the destination chain due to insufficient gas. Always set enforced options for each destination and message type. ```bash wrap theme={null} stellar contract invoke \ --id \ --network testnet \ --source my-account \ -- \ set_enforced_options \ --options '[{"eid": , "msg_type": 1, "options": ""}]' \ --operator ``` Where EVM uses ABI encoding, Soroban uses XDR (External Data Representation) for config encoding. This means ULN configs, executor configs, and other message library settings are XDR-encoded. The TypeScript SDK handles this automatically, but if you're using CLI directly, you'll need to provide XDR-encoded config values. Yes. The contract **owner** can modify: * **Peers**: `set_peer` to add or update remote chain connections * **Enforced options**: `set_enforced_options` to adjust gas limits per destination * **Delegate**: `set_delegate` to assign or change the operational delegate The **delegate** can configure: * **Message libraries**: Send and receive library selection * **DVN and Executor settings**: Via the Endpoint's `set_config` function See [DVN & Executor Config](/v2/developers/stellar/configuration/dvn-executor-config) for configuration details. ## Troubleshooting 1. **Check LayerZero Scan**: Look up the message GUID on [LayerZero Scan](https://layerzeroscan.com/) to see its status. 2. **Verify peers**: Ensure peers are set on both the source and destination chains. 3. **Check DVN status**: The message may be waiting for DVN verification. Use the receive library's `verifiable` (e.g. on ULN-302) to check whether DVNs have reached quorum. 4. **Check executor**: The executor may not have delivered yet. Check `endpoint.inbound_payload_hash(receiver, src_eid, sender, nonce)` to see if it's been verified but not delivered. 5. **Check enforced options**: Insufficient gas on the destination can cause delivery to fail silently. If a message is verified but delivery fails, you have several recovery options: * **Retry delivery**: The executor will typically retry automatically. * **Skip**: Skip the nonce if using ordered delivery: `endpoint.skip()` * **Nilify**: Void the message (can be re-verified later): `endpoint.nilify()` * **Burn**: Permanently destroy a nonce (irreversible): `endpoint.burn()` * **Clear**: Remove the payload: `endpoint.clear()` See [Technical Overview - Recovery Operations](/v2/developers/stellar/technical-overview#recovery-operations) for details. Soroban persistent storage entries have a TTL. If the TTL expires, the entry is archived (not deleted, but inaccessible until restored). This can happen to peer mappings, rate limit state, or other persistent data if the contract hasn't been called for an extended period. **Solution:** Restore the entry with `stellar contract restore` or call any function that accesses the entry (TTL is automatically extended on access). ## Security To prevent accidental ownership loss, use the 2-step flow: 1. Current owner calls `begin_ownership_transfer(new_owner, ttl)` -- proposes transfer with a TTL window 2. New owner calls `accept_ownership()` -- confirms within the TTL 3. If not accepted in time, the proposal expires and the original owner retains ownership A single-step `transfer_ownership` is also available but is immediate and irreversible. This ensures the new owner can actually receive and operate the contract. DVNs on Stellar are implemented as Soroban custom account contracts with multisig verification. They use secp256k1 signatures (compatible with EVM signers) for the multisig quorum, plus an optional Ed25519 admin key. Transaction data is hashed with keccak256, enabling crosschain signer reuse. ## Next Steps * **[Common Errors](/v2/developers/stellar/troubleshooting/common-errors)**: Detailed error codes and solutions. * **[Getting Started](/v2/developers/stellar/getting-started)**: Key differences between EVM and Stellar development. * **[Technical Overview](/v2/developers/stellar/technical-overview)**: Deep dive into Soroban architecture. # DVN and Executor Configuration Source: https://docs.layerzero.network/v2/developers/sui/configuration/dvn-executor-config Step-by-step guide to dvn and executor configuration using LayerZero V2. Build and deploy omnichain applications with crosschain messaging. Follow step-by-s... This guide explains how to configure Decentralized Verifier Networks (DVNs), Executors, and message libraries for your Sui OApp or OFT using the SDK. **Production deployments should use multiple required DVNs from independent operators.** A single-DVN configuration means a compromise of that one verifier results in unrestricted forged messages on the pathway. The examples on this page show the LayerZero Labs DVN with a `` placeholder so the snippet does not silently model a single-DVN production setup. Replace `` with a real non-LayerZero-Labs provider — see [DVN Addresses](/v2/deployments/dvn-addresses) for available providers per chain. See the [Integration Checklist](/v2/tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway) for production DVN guidance. ## Overview Configuration is done through the OApp SDK instance. All examples use `sdk.getOApp(packageId)` to get the OApp instance, then call SDK methods for configuration. **Configuration flow**: 1. Set message libraries (optional) 2. Configure DVNs for send/receive (optional but recommended) 3. Set enforced options (optional) 4. Set peer addresses (required - opens pathway, call last!) ## SDK Setup ```typescript wrap theme={null} import {SDK} from '@layerzerolabs/lz-sui-sdk-v2'; import {Stage} from '@layerzerolabs/lz-definitions'; const sdk = new SDK({client, stage: Stage.MAINNET}); const oapp = sdk.getOApp(yourPackageId); // Always use SDK factory ``` ## Configuration Methods ### Set Peer ```typescript wrap theme={null} // Configure peer for destination chain await oapp.setPeerMoveCall(tx, dstEid, peerBytes32); ``` **Address format**: Use package ID for Sui peers, 32-byte address for other chains. ### Set Message Libraries ```typescript wrap theme={null} // Set custom send library await oapp.setSendLibraryMoveCall(tx, dstEid, libraryAddress); // Set custom receive library await oapp.setReceiveLibraryMoveCall(tx, srcEid, libraryAddress, gracePeriod); ``` **Default**: Uses Endpoint defaults if not configured. ## DVN Configuration ### Configure Receive DVN (Inbound) ```typescript wrap theme={null} import { SDK, OAppUlnConfigBcs, PACKAGE_ULN_302_ADDRESS, OBJECT_ULN_302_ADDRESS, PACKAGE_DVN_LAYERZERO_ADDRESS, } from '@layerzerolabs/lz-sui-sdk-v2'; import {Stage} from '@layerzerolabs/lz-definitions'; const sdk = new SDK({client, stage: Stage.MAINNET}); const oapp = sdk.getOApp(yourPackageId); // Encode configuration const config = OAppUlnConfigBcs.serialize({ use_default_confirmations: false, use_default_required_dvns: false, use_default_optional_dvns: true, uln_config: { confirmations: 15, // Replace with a non-LayerZero-Labs DVN; see /v2/deployments/dvn-addresses required_dvns: [ PACKAGE_DVN_LAYERZERO_ADDRESS[Stage.MAINNET], PACKAGE_DVN__ADDRESS[Stage.MAINNET], ], optional_dvns: [], optional_dvn_threshold: 0, }, }).toBytes(); // Two-step Call pattern const tx = new Transaction(); const configCall = await oapp.setConfigMoveCall( tx, PACKAGE_ULN_302_ADDRESS[Stage.MAINNET], 30184, // Remote EID 3, // CONFIG_TYPE_RECEIVE_ULN config, ); tx.moveCall({ target: `${PACKAGE_ULN_302_ADDRESS[Stage.MAINNET]}::uln_302::set_config`, arguments: [tx.object(OBJECT_ULN_302_ADDRESS[Stage.MAINNET]), configCall], }); await client.signAndExecuteTransaction({transaction: tx, signer: keypair}); ``` ### Configure Send DVN (Outbound) ```typescript wrap theme={null} // Same pattern, use CONFIG_TYPE_SEND_ULN = 2 const configCall = await oapp.setConfigMoveCall( tx, PACKAGE_ULN_302_ADDRESS[Stage.MAINNET], 30184, 2, // CONFIG_TYPE_SEND_ULN config, ); ``` **Config types**: `1` = Executor, `2` = Send ULN, `3` = Receive ULN **DVN addresses**: Use `PACKAGE_DVN_LAYERZERO_ADDRESS[Stage.MAINNET]` or see [Deployed Contracts](/v2/deployments/chains/sui). ### Set Enforced Options ```typescript wrap theme={null} import {Options} from '@layerzerolabs/lz-v2-utilities'; const options = Options.newOptions() .addExecutorLzReceiveOption(60000, 0) // Gas for destination .toBytes(); await oapp.setEnforcedOptionsMoveCall(tx, dstEid, msgType, options); ``` ## Gas Limit Recommendations Based on gas profiling: ### OApp/OFT Operations | Operation | Gas Used (MIST) | Recommended Budget | Notes | | ------------ | --------------- | ------------------ | --------------------------------------------------------------------- | | `lz_receive` | 2,000-4,172 | 3,500-5,000 | For OApps and custom business logic, this needs independent profiling | | `oft_send` | 4,728,620 | 6,700,000 | Includes endpoint + ULN | | `dvn_verify` | 5,684,108 | 7,700,000 | Verification submission | | `dvn_commit` | 517,248 | 2,500,000 | Commit verification | ### Enforced Options Examples For EVM destinations: ```typescript wrap theme={null} import {Options} from '@layerzerolabs/lz-v2-utilities'; // Standard OApp message const options = Options.newOptions() .addExecutorLzReceiveOption(60000, 0) // 60k gas, no msg.value .toBytes(); // OFT with compose const optionsCompose = Options.newOptions() .addExecutorLzReceiveOption(200000, 0) // Higher for compose .toBytes(); ``` For Sui destinations: ```typescript wrap theme={null} const optionsForSui = Options.newOptions() .addExecutorLzReceiveOption(5000, 0) // 5k gas units, no msg.value .toBytes(); ``` **Note**: Based on gas profiling, Sui `lz_receive` uses 2,000-5,000 gas units. No msg.value needed - Sui handles storage internally. ## Common Issues **Errors**: * `InvalidBCSBytes` → Use `OAppUlnConfigBcs.serialize()` for DVN config * `oapp_registry::get_messaging_channel abort code: 1` → Used object ID instead of package ID as peer * Channel not initialized → Registration creates MessagingChannel automatically (no manual init needed) ## Next Steps * [OApp Overview](/v2/developers/sui/oapp/overview) - Base messaging standard * [OFT Overview](/v2/developers/sui/oft/overview) - Token standard and deployment * [OFT SDK](/v2/developers/sui/oft/sdk) - Complete SDK methods and examples * [Technical Overview](/v2/developers/sui/technical-overview) - Sui fundamentals and architecture * [Protocol Overview](/v2/developers/sui/protocol-overview) - Complete message workflows * [Troubleshooting](/v2/developers/sui/troubleshooting/common-errors) - Common configuration issues # Getting Started with LayerZero V2 on Sui Source: https://docs.layerzero.network/v2/developers/sui/getting-started Get started with Getting Started with on Sui. Step-by-step tutorial for building omnichain applications on LayerZero V2. LayerZero enables secure... Any data, whether it's a fungible token transfer, an NFT, or some other smart contract input can be encoded onchain as bytes and delivered to a destination chain to trigger some action using LayerZero. Because of this, any blockchain that broadly supports state propagation and events can be connected to LayerZero, including **Sui**. If you're new to LayerZero, we recommend reviewing [**"What is LayerZero?"**](/v2/concepts/getting-started/what-is-layerzero) before continuing.
LayerZero provides **Sui Move Packages** that can communicate with the equivalent [Solidity Contract Libraries](/v2/developers/evm/overview) and [Solana Programs](/v2/developers/solana/overview) deployed on other chains. These packages, like their Solidity and Rust counterparts, simplify calling the [LayerZero Endpoint](../../concepts/protocol/layerzero-endpoint), provide message handling, interfaces for protocol configurations, and other utilities for interoperability: * **Omnichain Fungible Token (OFT)**: extends OApp with functionality for handling omnichain token transfers using Sui's coin framework. * **Omnichain Application (OApp)**: the base package utilities for omnichain messaging and configuration. Each of these package standards implements common functions for **sending** and **receiving** omnichain messages. ## Differences from the Ethereum Virtual Machine The full differences between Solidity/EVM and Sui/Move are significant. For comprehensive guides, see: * [Sui Documentation](https://docs.sui.io/) * [Move Book](https://move-book.com/) * [Sui Compared to Other Blockchains](https://docs.sui.io/sui-compared) Skip this section if you already feel comfortable working with the Sui blockchain and its object model. ### Object Model vs Account Model The most fundamental difference is how state is organized: **EVM (Account Model)**: ```rust wrap theme={null} Account { address: 0x123... balance: 100 ETH storage: { slot_0: value_0, slot_1: value_1, ... } code: bytecode } ``` All state lives in storage slots within the account. Functions modify these slots. **Sui (Object Model)**: ```rust wrap theme={null} Object { id: UID (globally unique) owner: Address | Shared | Immutable type: Module::StructName fields: { field_1: value_1, field_2: value_2, ... } } ``` State lives in individual objects. Functions take objects as parameters and modify them. ### Writing Smart Contracts on Sui To create a new ERC20 token on an EVM-compatible blockchain, a developer inherits and redeploys the ERC20 contract: ```solidity wrap theme={null} // EVM: Inherit and deploy contract MyToken is ERC20 { constructor() ERC20("MyToken", "MTK") {} } ``` **Sui is different.** Instead of inheritance, Sui uses: 1. **[Packages](https://docs.sui.io/concepts/sui-move-concepts/packages)**: Published Move code (immutable) 2. **[Objects](https://docs.sui.io/concepts/object-model)**: State containers with unique IDs 3. **[Capabilities](https://move-book.com/programmability/capability/)**: Authorization objects Rather than deploying a new contract, you publish a package once, then create object instances. **One-Time Witness Pattern**: Sui uses the [one-time witness (OTW)](https://examples.sui.io/patterns/witness.html) pattern to prove code runs exactly once during package initialization: ```rust wrap theme={null} /// Sui: One-time witness pattern /// Struct name must match module name in ALL_CAPS public struct MY_TOKEN has drop {} // Only `drop` ability /// Called once when package is published fun init(otw: MY_TOKEN, ctx: &mut TxContext) { // Create coin with metadata // The `otw` parameter can only be created once by the runtime let (treasury_cap, coin_metadata) = coin::create_currency( otw, // Proves this is the first/only call 9, // Decimals b"MTK", // Symbol b"MyToken", // Name b"My token", // Description option::none(), ctx ); // CoinMetadata automatically frozen (immutable) // TreasuryCap transferred to deployer (can mint/burn) transfer::public_transfer(treasury_cap, ctx.sender()); } ``` **Key Differences**: * **No redeploy**: Package is published once, objects created many times * **No inheritance**: Use composition and capabilities instead * **Object ownership**: State has explicit ownership (address, shared, immutable) * **Type safety**: Move's type system prevents many runtime errors ### Object Ownership Types Sui's ownership model determines who can access and modify objects. Understanding these types is essential for building LayerZero applications: | Ownership | Access | Example | LayerZero Usage | | ------------------------------------------------------------------------ | -------------------- | --------------------- | ---------------------- | | [**Owned**](https://docs.sui.io/concepts/object-ownership/address-owned) | Only owner can use | `AdminCap`, `CallCap` | Authorization objects | | [**Shared**](https://docs.sui.io/concepts/object-ownership/shared) | Anyone can reference | `OApp`, `EndpointV2` | Protocol state objects | | [**Immutable**](https://docs.sui.io/concepts/object-ownership/immutable) | Anyone can read | `CoinMetadata` | Published packages | **OApp on Sui**: ```rust wrap theme={null} /// Shared OApp configuration object /// Contains peer configuration and enforced options for crosschain messaging /// The delegate (authorized by the OApp owner) can update these settings public struct OApp has key { id: UID, oapp_cap: CallCap, // Embedded capability for authentication admin_cap: address, // Reference to owned AdminCap for admin operations peer: Peer, // Embedded peer config (trusted remote OApp addresses) // ... } // Create and share let oapp = OApp { /* ... */ }; transfer::share_object(oapp); // Now accessible to everyone /// Owned object - only owner can use public struct AdminCap has key, store { id: UID, } // Transfer to admin transfer::public_transfer(admin_cap, admin_address); ``` ### Capabilities vs msg.sender **What are Capabilities?** [Capabilities](https://move-book.com/programmability/capability/) are special owned objects that grant specific permissions. Owning a capability object proves you have authorization to perform certain operations. **EVM Authorization** uses `msg.sender`: ```solidity wrap theme={null} // EVM: Check caller modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; } function setConfig() external onlyOwner { // only owner can call } ``` **Sui Authorization** uses capability objects: ```rust wrap theme={null} // Sui: Require capability object public fun set_config( oapp: &mut OApp, admin_cap: &AdminCap, // Must own this object to call config: Config, ) { // Owning AdminCap proves authorization // No need to check msg.sender oapp.config = config; } ``` **Benefits**: * **Transferable**: Can give capabilities to other addresses * **Composable**: Capabilities can be stored in other objects * **Type-safe**: Different capabilities for different permissions * **No spoofing**: Can't fake capability ownership ### Programmable Transaction Blocks While EVM executes one function call per transaction, Sui enables complex multi-step workflows in a single atomic transaction: ```solidity wrap theme={null} // EVM: Separate transactions tx1: token.approve(spender, amount); tx2: spender.transferFrom(user, recipient, amount); tx3: recipient.stake(amount); ``` **Sui uses [Programmable Transaction Blocks (PTBs)](https://docs.sui.io/concepts/transactions/prog-txn-blocks)** - up to [1,024 commands](https://docs.sui.io/concepts/transactions/prog-txn-blocks#transaction-type) in one atomic transaction: ```typescript wrap theme={null} const tx = new Transaction(); // All in one atomic transaction: tx.moveCall({ target: `${pkg}::token::approve`, ... }); tx.moveCall({ target: `${pkg}::spender::transfer_from`, ... }); tx.moveCall({ target: `${pkg}::staking::stake`, ... }); await client.signAndExecuteTransaction({ transaction: tx }); ``` **For LayerZero**: * Quote fees * Send message * Route through Endpoint/ULN/Workers * Confirm and extract receipt * All in one PTB, atomically ### No Dynamic Dispatch (Call Pattern) EVM can dynamically call contracts: ```solidity wrap theme={null} // EVM: delegatecall allows dynamic invocation contract Endpoint { function lzReceive(address oapp, ...) { // Call back into OApp without knowing it at compile time (bool success, ) = oapp.delegatecall( abi.encodeWithSignature("_lzReceive(...)", ...) ); } } ``` **Sui has no dynamic dispatch.** Instead, LayerZero uses the **Call pattern**: ```rust wrap theme={null} /// Call object (hot potato - must be consumed) public struct Call { // Has NO drop or store ability // Must be explicitly destroyed } // Endpoint creates Call targeting OApp public fun lz_receive(...): Call { call::create(executor_cap, oapp_address, true, param, ctx) } // OApp must destroy Call to process public fun lz_receive(oapp: &mut OApp, call: Call) { let (callee, param, _) = call.destroy(&oapp.oapp_cap); // Validate and process... } ``` The `Call` object has **no abilities at all**: * No `drop` ability → Can't be ignored (must be consumed) * No `store` ability → Can't be saved in structs * No `copy` ability → Can't be forged or copied (prevents reentrancy) * No `key` ability → Can't be stored globally in the ledger This lack of abilities enforces the hot potato pattern - the `Call` must be explicitly destroyed before the transaction ends, routing through the PTB to the destination module. This achieves similar functionality to dynamic dispatch while maintaining type safety and preventing reentrancy attacks. ## Prerequisites Before you start building, you'll need to set up your development environment. ### Install Sui CLI Using [suiup](https://github.com/MystenLabs/suiup) (recommended): ```bash wrap theme={null} # Install suiup installer curl -sSf https://raw.githubusercontent.com/MystenLabs/suiup/main/install.sh | sh # Install Sui CLI for testnet suiup install testnet ``` Verify installation: ```bash wrap theme={null} sui --version # sui 1.54.1-... or later ``` Alternatively, install via cargo: ```bash wrap theme={null} cargo install --locked --git https://github.com/MystenLabs/sui.git --branch mainnet sui ``` ### Install Node.js and TypeScript SDK For PTB construction and SDK usage in your OApp/OFT project: ```bash wrap theme={null} # Install as project dependencies (not global) npm install @mysten/sui.js @layerzerolabs/lz-sui-sdk-v2 @layerzerolabs/lz-sui-oft-sdk-v2 ``` These packages are required for building PTBs, configuring your OApp, and interacting with deployed contracts. ### Set Up Sui Wallet Create or import a wallet: ```bash wrap theme={null} # Create new wallet sui client new-address ed25519 # Or import existing sui client import ``` ### Get Testnet SUI For testing on Sui testnet, see [Sui Faucet documentation](https://docs.sui.io/guides/developer/getting-started/get-coins): ```bash wrap theme={null} # Switch to testnet sui client switch --env testnet # Get SUI from faucet curl --location --request POST 'https://faucet.testnet.sui.io/gas' \ --header 'Content-Type: application/json' \ --data-raw '{ "FixedAmountRequest": { "recipient": "" } }' ``` ## Understanding Package IDs vs Object IDs One of the most important concepts for LayerZero on Sui is the distinction between package IDs and object IDs: | Type | What It Is | When to Use | Example | | -------------- | ------------------------------------- | ------------------------------------- | --------------- | | **Package ID** | Address of published code (immutable) | Move call targets, **peer addresses** | `0x061a47bf...` | | **Object ID** | Address of object instance (state) | Function arguments via `tx.object()` | `0xf1ab4be...` | **Finding Package ID**: ```bash wrap theme={null} # From object type field sui client object --json | jq '.data.type' # Returns: "0xPACKAGE_ID::module::StructName" ``` **Critical for LayerZero**: * **Peer addresses = Package ID** (where code is deployed) * **Not Object ID** (instance of OApp/OFT) See [Peer Address Configuration](/v2/developers/sui/oapp/overview#step-7-set-peer-address) for details. For general peer concepts, see [Peer in Glossary](/v2/concepts/glossary#peer). ### Understanding the Registry System When you deploy and register an OApp with the LayerZero Endpoint, understanding the registry architecture is crucial: **What happens during registration**: 1. **Endpoint stores your package ID** in its registry (not object ID) 2. **MessagingChannel.oapp field** = your package ID 3. **Remote chains send messages** to your package ID 4. **Endpoint looks up package ID** → finds your MessagingChannel → routes message **Example deployment flow**: ```bash wrap theme={null} # 1. Deploy your OApp package sui client publish --gas-budget 1000000000 # Output includes: # - Package ID: 0x061a47bf... (your code location) # - OApp Object ID: 0x242952... (instance of OApp) # 2. Register with Endpoint # Endpoint stores: registry[0x061a47bf...] = MessagingChannel # 3. Remote chain configuration # Remote chain must use: peer = 0x061a47bf... (package ID) ``` **This is why peers must be package IDs.** ### CallCap and Package Identity LayerZero OApps use **Package CallCaps** (not Individual CallCaps): ```rust wrap theme={null} // From call_cap module public enum CapType { Individual, // ID = object's UID address Package(address), // ID = package address ← OApps use this } // When OApp calls callCap.id(): // Returns the package address, not the object UID! ``` **Impact on LayerZero**: * `callCap.id()` returns your package address * Registry keys by this package address * All lookups expect package address * Remote chains must use this as peer address **Finding your package ID from an object**: ```bash wrap theme={null} # Method 1: From object type sui client object 0x242952... --json | jq '.data.type' # Output: "0x061a47bf...::oapp::OApp" # ^^^^^^^^^^^^ # This is your package ID # Method 2: From publish output # Look for "packageId" in the transaction result ``` **Common errors when using wrong ID**: | Error | Cause | Fix | | ---------------------------------------------------- | -------------------------- | ---------------------------- | | `oapp_registry::get_messaging_channel abort code: 1` | Used object ID as peer | Use package ID instead | | `oapp_registry::get_oapp_info abort code: 1` | Registry lookup failed | Ensure OApp is registered | | Message delivery fails | Peer not found in registry | Verify package ID is correct | ## Next Steps Choose your path: ### Build an OApp For custom crosschain logic: * [OApp Overview](/v2/developers/sui/oapp/overview) - Architecture and patterns * [OApp Protocol Details](/v2/developers/sui/protocol-overview) - Deep technical dive * [Technical Overview](/v2/developers/sui/technical-overview) - Sui fundamentals ### Build an OFT For crosschain tokens: * [OFT Overview](/v2/developers/sui/oft/overview) - Token architecture * [OFT SDK](/v2/developers/sui/oft/sdk) - TypeScript SDK integration and methods * [Configuration Guide](/v2/developers/sui/configuration/dvn-executor-config) - Security and DVN setup ### Understand the Protocol For protocol-level understanding: * [Technical Overview](/v2/developers/sui/technical-overview) - VM architecture and Call pattern * [Protocol Overview](/v2/developers/sui/protocol-overview) - Complete message workflows * [OFT SDK](/v2/developers/sui/oft/sdk) - Available SDK methods and patterns ### Get Help * [Troubleshooting](/v2/developers/sui/troubleshooting/common-errors) - Common issues * [FAQ](/v2/developers/sui/troubleshooting/faq) - Frequently asked questions * [Discord](https://discord.com/invite/ktbvm8Nkcr) - Community support # LayerZero Sui OApp Source: https://docs.layerzero.network/v2/developers/sui/oapp/overview Overview of Sui OApp on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure crosschain messaging. The OApp Standard provides developers with a generic message passing interface to send and receive arbitrary pieces of data between contracts existing on different blockchain networks. How the data is interpreted and what actions it triggers depend on the specific OApp implementation. ## What is an OApp on Sui? An OApp on Sui is a [Move package](https://docs.sui.io/concepts/sui-move-concepts/packages) that integrates with the LayerZero protocol to enable crosschain messaging. Unlike EVM OApps that inherit base contracts, Sui OApps use [shared objects](https://docs.sui.io/concepts/object-ownership/shared) and explicit function calls within [Programmable Transaction Blocks (PTBs)](https://docs.sui.io/concepts/transactions/prog-txn-blocks). ### Differences from EVM OApps | Aspect | EVM | Sui | | ---------------------- | --------------------------------------------- | ---------------------------------------------- | | **Code Organization** | Solidity contracts | Move packages containing modules | | **Integration Method** | Inherit from `OApp` base contract | Use `oapp` package and `Call` pattern | | **Receive Flow** | Endpoint calls `lzReceive` via `delegatecall` | Endpoint creates `Call` object for OApp module | | **Validation** | Implicit via inheritance | Explicit via `CallCap` validation | | **State Model** | Contract storage slots | Shared objects with struct fields | | **OApp Identity** | Contract address | Shared `OApp` object ID | | **Authorization** | `msg.sender` and modifiers | Capability objects (`CallCap`, `AdminCap`) | ## Installation ### Prerequisites * [Sui CLI](https://docs.sui.io/references/cli) installed (version 1.54.1 or later) * Basic understanding of [Move programming](https://docs.sui.io/concepts/sui-move-concepts) * Familiarity with [Sui's object model](https://docs.sui.io/concepts/object-model) ### Create a New Project Create a new Sui Move package: ```bash wrap theme={null} mkdir my-oapp cd my-oapp sui move new my_oapp ``` This creates: ``` my_oapp/ ├── Move.toml ├── sources/ └── tests/ ``` ### Configure Move.toml ### Git Dependencies Not Supported Git dependencies for LayerZero packages currently do not work due to missing Move.toml manifests in subdirectories. Use **local dependencies** instead. **Clone LayerZero Repository**: ```bash wrap theme={null} cd .. git clone https://github.com/LayerZero-Labs/LayerZero-v2.git cd my-oapp ``` **Update `Move.toml` with local paths**: ```toml wrap theme={null} [package] name = "my_oapp" version = "0.0.1" edition = "2024.beta" [dependencies] Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "mainnet" } # LayerZero packages - use local paths OApp = { local = "../LayerZero-v2/packages/layerzero-v2/sui/contracts/oapps/oapp" } EndpointV2 = { local = "../LayerZero-v2/packages/layerzero-v2/sui/contracts/endpoint-v2" } Call = { local = "../LayerZero-v2/packages/layerzero-v2/sui/contracts/dynamic-call/call" } Utils = { local = "../LayerZero-v2/packages/layerzero-v2/sui/contracts/utils" } [addresses] my_oapp = "0x0" ``` **Alternative: Use Published Package Addresses** If LayerZero packages are published onchain, you can reference them by address: ```toml wrap theme={null} [dependencies] Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "mainnet" } # Reference by published address (check deployments page for current addresses) OApp = { address = "0xfdc28afc0110cb2edb94e3e57f2b1ce69b5a99c503b06d15e51cfa212de56e24" } # ... other packages [addresses] my_oapp = "0x0" ``` See [Deployed Contracts](/v2/deployments/chains/sui) for current mainnet package addresses. *** ## Working Example: OFT Implementation The **Omnichain Fungible Token (OFT)** is a complete, production-ready implementation of an OApp that demonstrates all core messaging patterns. OFTs extend OApp functionality to enable crosschain token transfers. **To see a working OApp in action**, review the [OFT Overview](/v2/developers/sui/oft/overview) which shows: * Complete initialization and deployment workflow * Message encoding/decoding patterns * Integration with Sui's object model and coin framework * Production deployment examples with TypeScript SDK **Source Code References**: * [oapp.move](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/sui/contracts/oapps/oapp/sources/oapp.move) - Base OApp implementation * [oft.move](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/sui/contracts/oapps/oft/oft/sources/oft.move) - OFT extending OApp with token logic ## How OApp Messaging Works Understanding how OApps work on Sui requires understanding several key concepts that differ from EVM implementations. ### Initialization: Creating Your OApp When you initialize an OApp on Sui, the `oapp::new()` function creates three critical components using the [one-time witness (OTW) pattern](https://examples.sui.io/patterns/witness.html): 1. **OApp Shared Object**: Contains configuration state (peers, enforced options) that anyone can read but only admins can modify 2. **CallCap** (owned): Proves ownership of the OApp and is required for all messaging operations 3. **AdminCap** (owned): Grants authority for administrative operations like setting peers and configuring options **Key Point**: The OApp object is automatically made shared (via `transfer::share_object()`), while the capabilities are transferred to the deployer. This separation allows secure access control using Sui's capability-based authorization system rather than address-based checks. ### Registration: Connecting to the Endpoint After initialization, your OApp must register with the LayerZero Endpoint by calling `endpoint_v2::register_oapp()`. This creates a dedicated `MessagingChannel` shared object that stores your OApp's message state and nonce tracking for each [pathway](/v2/concepts/glossary#channel--lossless-channel). **What the registry stores**: The Endpoint registry maps your **package ID** (not object ID) to your MessagingChannel. This is critical because Sui OApps use Package CallCaps, which identify by package address. ### Peer Configuration: Establishing Trust [Peers](/v2/concepts/glossary#peer) are trusted OApp addresses on remote chains that are authorized to send messages to your OApp. You configure peers by calling `oapp::set_peer()` with the AdminCap: **Critical: Package ID vs Object ID** On Sui, peers must be configured using **package IDs**, not object IDs: * **Your Sui OApp**: Use the package ID published address as the peer on remote chains * **Remote OApp peers**: Use their contract/package addresses (EVM contract address, Solana program ID, etc.) This is because LayerZero's registry and verification systems key by package address for Sui OApps. Using an object ID will cause the error: `oapp_registry::get_messaging_channel abort code: 1`. ### Sending Messages: The Call Pattern When your OApp sends a message, it creates a **Call object** using `oapp::lz_send()`. This Call object is a "hot potato" - it has no `drop` or `store` abilities, meaning it **must** be consumed before the transaction ends. **The Send Flow**: 1. Your OApp calls `lz_send()` → creates `Call` 2. Call is routed through a PTB to: Endpoint → ULN302 → DVNs & Executor 3. Each component processes and returns the Call 4. Your OApp calls `confirm_lz_send()` to extract the receipt and finalize **Why the Call pattern?** Sui Move lacks dynamic dispatch (like EVM's `delegatecall`). The Call pattern achieves similar routing functionality while maintaining type safety and preventing reentrancy attacks through Move's ability system. **How OFT Implements Custom Send Logic**: ```rust wrap theme={null} // From oft.move - shows how custom business logic wraps OApp messaging public fun send( self: &mut OFT, oapp: &mut OApp, sender: &OFTSender, send_param: &SendParam, coin_provided: &mut Coin, native_coin_fee: Coin, zro_coin_fee: Option>, refund_address: Option
, clock: &Clock, ctx: &mut TxContext, ): (Call, OFTSendContext) { // 1. Custom business logic: Validate state self.assert_upgrade_version(); self.pausable.assert_not_paused(); // 2. Custom business logic: Debit tokens (burn or escrow) let (amount_sent_ld, amount_received_ld) = self.debit( coin_provided, send_param.dst_eid(), send_param.amount_ld(), send_param.min_amount_ld(), ctx, ); // 3. Custom business logic: Apply rate limits self.inbound_rate_limiter.release_rate_limit_capacity(/*...*/); self.outbound_rate_limiter.try_consume_rate_limit_capacity(/*...*/); // 4. Custom business logic: Build OFT-specific message (recipient + amount) let (message, options) = self.build_msg_and_options(/*...*/); // 5. Call base OApp send functionality let ep_call = oapp.lz_send( &self.oft_cap, // Prove OFT owns this OApp send_param.dst_eid(), // Destination chain message, // Encoded OFT message options, // Execution options native_coin_fee, // Fee payment zro_coin_fee, refund_address, ctx, ); // 6. Return Call and context for confirmation (ep_call, send_context) } ``` This pattern shows how your custom OApp would: 1. Add application-specific validation and state changes 2. Encode your business logic into the message payload 3. Call the base `oapp::lz_send()` function 4. Return the Call for PTB routing **Sequential vs Parallel Sends**: * `lz_send()` + `confirm_lz_send()`: Enforces sequential execution (one send at a time) * `lz_send_and_refund()`: Allows parallel sends in the same PTB (messages can be reordered) ### Receiving Messages: Validation and Processing When a message arrives on Sui, the Executor calls `endpoint_v2::lz_receive()`, which creates a `Call` object targeting your OApp. Your OApp's `lz_receive()` function must: 1. **Validate the CallCap**: Ensure the Call belongs to this OApp 2. **Check the Endpoint**: Verify the Call came from the authorized LayerZero Endpoint 3. **Verify the Peer**: Confirm the sender matches your configured peer for that source chain 4. **Process the message**: Decode and execute your custom business logic **How OFT Implements Custom Receive Logic**: ```rust wrap theme={null} // From oft.move - shows validation + custom business logic public fun lz_receive( self: &mut OFT, oapp: &OApp, call: Call, clock: &Clock, ctx: &mut TxContext, ) { // 1. Custom business logic: Pre-receive validation self.assert_upgrade_version(); self.pausable.assert_not_paused(); // 2. Base OApp validation (CallCap, Endpoint, Peer) // Returns validated LzReceiveParam let param = oapp.lz_receive(&self.oft_cap, call); // 3. Custom business logic: Decode OFT message let oft_msg = oft_msg_codec::decode(param.message()); let recipient = oft_msg.send_to(); let amount_received_sd = oft_msg.amount_sd(); // 4. Custom business logic: Convert from shared to local decimals let amount_received_ld = self.sd_to_ld(amount_received_sd); // 5. Custom business logic: Credit tokens (mint or release from escrow) let coin_credited = self.credit(amount_received_ld, ctx); // 6. Custom business logic: Apply rate limits self.inbound_rate_limiter.try_consume_rate_limit_capacity( param.src_eid(), amount_received_ld, clock, ); // 7. Custom business logic: Transfer tokens to recipient transfer::public_transfer(coin_credited, recipient); // 8. Emit event for tracking event::emit(OFTReceivedEvent { /* ... */ }); } ``` This pattern shows how your custom OApp would: 1. Delegate security validation to `oapp.lz_receive()` (returns validated params) 2. Decode the message payload to extract your application data 3. Execute your custom business logic (state updates, token transfers, etc.) 4. Handle any post-processing (events, cleanup) ### How This Differs from EVM | Aspect | EVM | Sui | | ------------------- | --------------------------------------------- | ------------------------------------------------ | | **Message Routing** | Endpoint calls `lzReceive()` via delegatecall | Endpoint creates Call object, PTB routes to OApp | | **Validation** | Implicit via `onlyEndpoint` modifier | Explicit via CallCap and Call pattern | | **Execution Flow** | Single transaction with nested calls | PTB chains multiple function calls atomically | | **Authorization** | Address-based (`msg.sender`) | Capability-based (own the CallCap) | | **Composability** | Vertical (nested calls in one tx) | Horizontal (chained calls in PTB) | *** ## Message Encoding and Business Logic Your OApp is responsible for encoding/decoding message payloads. LayerZero transports raw bytes - how you structure them depends on your application. **Key Principles**: * Use consistent byte order (big-endian recommended for cross-VM compatibility with EVM) * Document your message format clearly * Consider padding for fixed-width fields * Test encoding/decoding on both source and destination chains **Example from OFT**: The [oft\_msg\_codec.move](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/sui/contracts/oapps/oft/oft/sources/codec/oft_msg_codec.move) shows a production codec that encodes recipient address, amount in shared decimals, and optional compose parameters. *** ## Required Components Every OApp on Sui requires four key components: ### 1. OApp Shared Object Contains configuration state including: * **Peer mappings**: Maps endpoint IDs to trusted remote OApp addresses * **Enforced options**: Minimum gas and execution parameters for each destination * **Embedded CallCap**: Used internally for authentication * **Admin tracking**: Reference to the AdminCap owner address The OApp object is created as a [shared object](https://docs.sui.io/concepts/object-ownership/shared), making it publicly accessible for reading configuration while restricting modifications to capability holders. ### 2. Capability Objects **CallCap** (owned): Required for all messaging operations (`quote`, `lz_send`). This proves ownership of the OApp and is validated on every call. Typically stored within your application's module or transferred to a specific address. **AdminCap** (owned): Grants authority for configuration operations like setting peers, configuring DVNs, and updating enforced options. Transferable to enable admin rotation. ### 3. MessagingChannel Created during Endpoint registration, this shared object stores state for your OApp's [communication channel](/v2/concepts/glossary#channel--lossless-channel): * Message nonces for ordering * Payload hashes for verification * Channel initialization state per destination EID The registry maps your **package ID** → MessagingChannel, which is why peers must use package IDs. ### 4. Message Codec A module in your package that defines how to encode/decode your business logic into bytes that LayerZero transports crosschain. This is application-specific - OFT uses `oft_msg_codec`, your custom OApp would define its own format. ## Message Flow ### Send Flow ``` ┌─────────────┐ │ OApp │ 1. User calls send() │ (Your App) │ └──────┬──────┘ │ 2. Create SendParam ▼ ┌─────────────┐ │ Endpoint │ 3. Throws Hot Potato └──────┬──────┘ │ 4. PTB routes to ULN ▼ ┌─────────────┐ │ ULN302 │ 5. Assign jobs to workers └──────┬──────┘ │ 6. Throws Hot Potatoes ▼ ┌─────────────────┐ │ DVNs + Executor │ 7. Process and return results └─────────────────┘ ``` ### Receive Flow ``` ┌─────────────┐ │ Executor │ 1. Calls lzReceive └──────┬──────┘ │ 2. Throws Hot Potato ▼ ┌─────────────┐ │ Endpoint │ 3. Routes to OApp └──────┬──────┘ │ 4. Throws Hot Potato ▼ ┌─────────────┐ │ OApp │ 5. Process message │ (Your App) │ 6. Update state └─────────────┘ ``` ## Core Methods These are the primary functions your OApp will call to send messages and receive them from other chains. ### quote() Estimates the fee required to send a crosschain message without actually sending it. Returns a `Call` that must be routed through the Endpoint in a PTB, then confirmed with `confirm_quote()` to extract the fee amount. **When to use**: Before sending to determine how much SUI to include in the transaction, or to display estimated costs to users. ### lz\_send() Sends a crosschain message to a destination chain. Creates a `Call` that routes through Endpoint → ULN → DVNs/Executor, then must be confirmed with `confirm_lz_send()` to extract the receipt. **Key parameters**: * `dst_eid`: Destination chain endpoint ID * `message`: Your encoded payload (raw bytes) * `options`: Execution parameters (gas limits, msg.value) * `native_token_fee`: SUI payment for crosschain delivery * `refund_address`: Where to send excess fees **Sequential execution**: Uses internal state tracking (`sending_call`) to enforce one send at a time. Must call `confirm_lz_send()` before initiating another send. ### lz\_send\_and\_refund() Alternative send method that allows parallel message sending within the same PTB. Unlike `lz_send()`, this doesn't track state and doesn't require confirmation, making it suitable for batch operations. Requires a refund address (cannot be optional). **When to use**: When sending multiple messages in one transaction and order doesn't matter. ### lz\_receive() Processes incoming messages delivered by the Executor. This function is called with a `Call` created by the Endpoint. It performs three critical validations: 1. **CallCap validation**: Ensures the Call belongs to this OApp 2. **Endpoint check**: Verifies the Call originated from the authorized LayerZero Endpoint 3. **Peer verification**: Confirms the message sender matches the configured peer for the source chain After validation, it returns `LzReceiveParam` containing the decoded message data for your business logic to process. **Your implementation**: You'll wrap `oapp::lz_receive()` in your own function that adds application-specific processing (see OFT's implementation for reference). ## Best Practices ### Always Validate CallCap Every function that accepts a `CallCap` must call `self.assert_oapp_cap(oapp_cap)` to ensure it belongs to this OApp. This prevents unauthorized calls and ensures type safety. ### Verify Message Senders in lz\_receive Always validate that incoming messages come from configured peers. The `oapp::lz_receive()` base function handles this validation, but custom receive logic must preserve these checks. ### Use One-Time Witness for Initialization Use the [one-time witness pattern](https://examples.sui.io/patterns/witness.html) in your module's `init()` function to create the OApp. This guarantees initialization runs exactly once. ### Configure Security Before Setting Peers Set your message libraries and DVN configuration before calling `set_peer()`. Setting a peer opens the pathway for messaging, so security should be configured first. ### Confirm All Call Objects Every `Call` object returned by `quote()`, `lz_send()`, or similar functions must be consumed in a PTB (routed through protocol components) and confirmed to extract results. Unused Call objects will cause transaction failures due to their lack of `drop` ability. ## Configuration Before your OApp can send messages, you must configure: 1. **Initialize Channel**: Create channel state for remote EID 2. **Set Peer**: Define the peer OApp address on the remote chain 3. **Configure DVNs**: Set which DVNs verify your messages (optional, defaults used if not set) 4. **Configure Executor**: Set who executes messages on destination (optional, defaults used if not set) See the [Configuration Guide](/v2/developers/sui/configuration/dvn-executor-config) for details. ## Security Considerations ### Critical Validations * Always validate OApp object in every function * Verify message sender matches configured peer * Check Endpoint address matches stored value * Validate nonce sequence to prevent replay attacks ### Common Pitfalls * Forgetting to call `assert_oapp` * Not validating message sender in `lzReceive` * Incorrect peer address configuration * Missing channel initialization ## Next Steps * [OFT Implementation](/v2/developers/sui/oft/overview) - Token standard built on OApp * [OFT SDK](/v2/developers/sui/oft/sdk) - TypeScript SDK methods and patterns * [Configuration Guide](/v2/developers/sui/configuration/dvn-executor-config) - DVN and executor setup * [Technical Overview](/v2/developers/sui/technical-overview) - Sui fundamentals and Call pattern * [Protocol Overview](/v2/developers/sui/protocol-overview) - Complete message workflows * [Troubleshooting](/v2/developers/sui/troubleshooting/common-errors) - Common issues and solutions # Sui OFT Source: https://docs.layerzero.network/v2/developers/sui/oft/overview Overview of Sui OFT on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure crosschain messaging. 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](/v2/concepts/applications/oft-standard). ## What is an OFT on Sui? An OFT on Sui is a [Move package](https://docs.sui.io/concepts/sui-move-concepts/packages) that extends the OApp functionality to enable crosschain token transfers. It integrates with Sui's native [coin type system](https://docs.sui.io/standards/coin) ([`Coin`](https://docs.sui.io/references/framework/sui-framework/coin), [`Balance`](https://docs.sui.io/references/framework/sui-framework/balance), `TreasuryCap`) while providing LayerZero's omnichain capabilities. This guide will walk you through deploying an OFT on Sui. To understand how OFTs integrate with Sui's coin system and the differences between mint/burn and lock/unlock token management strategies, see [Integration with Sui Coin System](#integration-with-sui-coin-system). ## Deployment OFT deployment on Sui uses a **two-package pattern**: your token + pure [LayerZero OFT source](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/sui/contracts/oapps/oft/oft). ### 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](#choosing-mintburn-vs-lockunlock) for details. **Prerequisites**: * Sui CLI installed (via [suiup](https://github.com/MystenLabs/suiup)) * Node.js and npm for TypeScript SDK * 1-2 SUI for gas fees ### New to Sui? If you haven't used Sui before, start with [Getting Started with Sui](/v2/developers/sui/getting-started) to understand the object model, package structure, and development basics. ### Step 1: Create and Deploy Your Token **Create token package**: ```bash wrap theme={null} mkdir my-token cd my-token sui move new myoft ``` **Implement token** (`sources/myoft.move`): ```rust wrap theme={null} module myoft::myoft; use sui::coin; /// One-time witness for coin creation /// Must be named same as module (MYOFT) and have only `drop` ability public struct MYOFT has drop {} /// Initialize the coin on package publish fun init(otw: MYOFT, ctx: &mut TxContext) { // Create the coin with metadata let (treasury_cap, coin_metadata) = coin::create_currency( otw, // One-time witness 6, // decimals (6 for crosschain compatibility) b"MYOFT", // symbol b"My Omnichain Fungible Token", // name b"A LayerZero OFT on Sui with mint/burn capabilities", // description option::none(), // icon_url (optional) ctx ); // Freeze the metadata object (makes it immutable and shared) transfer::public_freeze_object(coin_metadata); // Transfer treasury cap to deployer transfer::public_transfer(treasury_cap, ctx.sender()); } // That's it! No OFT logic in token package ``` **Deploy your token**: ```bash wrap theme={null} # From your token directory sui client publish --gas-budget 500000000 --json > token_deploy.json # Extract IDs TOKEN_PACKAGE=$(jq -r '.objectChanges[] | select(.type=="published") | .packageId' token_deploy.json) TREASURY_CAP=$(jq -r '.objectChanges[] | select(.objectType | contains("TreasuryCap")) | .objectId' token_deploy.json) COIN_METADATA=$(jq -r '.objectChanges[] | select(.objectType | contains("CoinMetadata")) | .objectId' token_deploy.json) echo "Token Package: $TOKEN_PACKAGE" echo "Treasury Cap: $TREASURY_CAP" echo "Coin Metadata: $COIN_METADATA" ``` ### Step 2: Deploy LayerZero OFT Package Deploy the pure [LayerZero OFT source](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/sui/contracts/oapps/oft/oft) without modifications. **Recommended approach** (git dependencies): ```bash wrap theme={null} # Copy OFT source to your project mkdir oft cd oft ``` **Create `Move.toml`** with git dependencies: ```toml wrap theme={null} [package] name = "OFT" version = "0.0.1" edition = "2024.beta" license = "MIT" [dependencies] OApp = { git = "https://github.com/LayerZero-Labs/LayerZero-v2.git", subdir = "packages/layerzero-v2/sui/contracts/oapps/oapp", rev = "main" } OFTCommon = { git = "https://github.com/LayerZero-Labs/LayerZero-v2.git", subdir = "packages/layerzero-v2/sui/contracts/oapps/oft/oft-common", rev = "main" } PtbMoveCall = { git = "https://github.com/LayerZero-Labs/LayerZero-v2.git", subdir = "packages/layerzero-v2/sui/contracts/ptb-builders/ptb-move-call", rev = "main" } [addresses] oft = "0x0" [dev-dependencies] SimpleMessageLib = { git = "https://github.com/LayerZero-Labs/LayerZero-v2.git", subdir = "packages/layerzero-v2/sui/contracts/message-libs/simple-message-lib", rev = "main" } ``` **Copy OFT source files**: ```bash wrap theme={null} # Clone LayerZero repository (temporary, just to copy sources) git clone https://github.com/LayerZero-Labs/LayerZero-v2.git --depth 1 cp -r LayerZero-v2/packages/layerzero-v2/sui/contracts/oapps/oft/oft/sources ./ rm -rf LayerZero-v2 ``` **Deploy** (dependencies auto-fetched from GitHub): ```bash wrap theme={null} # From your OFT directory sui client publish --gas-budget 1000000000 --json > oft_deploy.json # Extract the package ID (you'll need this for peer configuration!) OFT_PACKAGE=$(jq -r '.objectChanges[] | select(.type=="published") | .packageId' oft_deploy.json) OAPP_OBJECT=$(jq -r '.objectChanges[] | select(.objectType | contains("::oapp::OApp")) | select(.owner.Shared) | .objectId' oft_deploy.json) INIT_TICKET=$(jq -r '.objectChanges[] | select(.objectType | contains("OFTInitTicket")) | .objectId' oft_deploy.json) echo "OFT Package: $OFT_PACKAGE" # ← SAVE THIS! Use as peer on remote chains echo "OApp Object: $OAPP_OBJECT" echo "Init Ticket: $INIT_TICKET" ``` 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 Sui OFT. **Finding package ID from object** (if you didn't save it): ```bash wrap theme={null} sui client object --json | jq -r '.data.type' | cut -d':' -f1 ``` **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](#choosing-mintburn-vs-lockunlock) 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`: ```typescript wrap theme={null} import {SDK} from '@layerzerolabs/lz-sui-sdk-v2'; import {OFT} from '@layerzerolabs/lz-sui-oft-sdk-v2'; import {Stage} from '@layerzerolabs/lz-definitions'; const sdk = new SDK({client, stage: Stage.MAINNET}); const oft = new OFT(sdk, OFT_PKG, undefined, TOKEN_TYPE, OAPP); const initTx = new Transaction(); // Use initOftMoveCall for mint/burn (includes TreasuryCap) const [adminCap, migrationCap] = oft.initOftMoveCall( initTx, TOKEN_TYPE, // "0xTOKEN_PKG::myoft::MYOFT" TICKET, // OFTInitTicket object ID OAPP, // OApp object ID TREASURY, // TreasuryCap object ID (enables mint/burn) METADATA, // CoinMetadata object ID 6, // shared_decimals ); initTx.transferObjects([adminCap, migrationCap], sender); const result = await client.signAndExecuteTransaction({ transaction: initTx, signer: keypair, options: {showObjectChanges: true}, }); // Extract OFT object ID const OFT_OBJECT = result.objectChanges.find( (c) => c.type === 'created' && c.objectType.includes('oft::OFT<'), ).objectId; await client.waitForTransaction({digest: result.digest}); ``` ### 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 Sui Coin System](#integration-with-sui-coin-system) for details. ## Integration with Sui Coin System OFTs integrate seamlessly with Sui's native coin framework, using standard types for token management. ### Sui Coin Type System The Sui framework provides these core types for token functionality: **`Coin`**: Owned coin object with a value ```rust wrap theme={null} public struct Coin has key, store { id: UID, balance: Balance, } ``` **`Balance`**: Storable value (can be held in structs) ```rust wrap theme={null} public struct Balance has store { value: u64, } ``` **`TreasuryCap`**: Authority to mint/burn coins ```rust wrap theme={null} public struct TreasuryCap has key, store { id: UID, total_supply: Supply, } ``` **`CoinMetadata`**: Token information (name, symbol, decimals) ```rust wrap theme={null} public struct CoinMetadata has key, store { id: UID, decimals: u8, name: string::String, symbol: ascii::String, description: string::String, icon_url: Option, } ``` ### OFT Integration The OFT uses these types: ```rust wrap theme={null} public struct OFT has key { // ... treasury: OFTTreasury, // Holds TreasuryCap OR Balance escrow coin_metadata: address, // Reference to CoinMetadata object // ... } ``` **[Phantom Type Parameter](https://move-book.com/move-basics/generics#phantom-type-parameters)**: `` 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 Sui 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: ```rust wrap theme={null} /// Omnichain Fungible Token - enables seamless crosschain token transfers public struct OFT has key { id: UID, upgrade_version: u64, oapp_object: address, // Associated OApp for messaging admin_cap: address, // AdminCap owner address migration_cap: address, // Migration capability oft_cap: CallCap, // Capability for crosschain calls treasury: OFTTreasury, // ← Enum: determines mint/burn vs lock/unlock coin_metadata: address, // Reference to CoinMetadata decimal_conversion_rate: u64, // 10^(local - shared decimals) shared_decimals: u8, // Crosschain precision // Optional features (always present, opt-in to configure) pausable: Pausable, // Starts unpaused (false) fee: OFTFee, // Starts with 0% fees inbound_rate_limiter: RateLimiter, // Starts with no limits outbound_rate_limiter: RateLimiter, // Starts with no limits } ``` 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` enum determines token management strategy: ```rust wrap theme={null} public enum OFTTreasury has store { /// Standard OFT: mints/burns using treasury capability OFT { treasury_cap: TreasuryCap, // Grants mint/burn authority }, /// Adapter OFT: escrows/releases existing tokens OFTAdapter { escrow: Balance, // Token balance pool }, } ``` #### Choosing Mint/Burn vs Lock/Unlock | Model | When to Use | Initialization Method | | --------------- | ------------------------------------- | ----------------------------------------------- | | **Mint/Burn** | You have/control the `TreasuryCap` | `oft.initOftMoveCall()` with TREASURY parameter | | **Lock/Unlock** | You DON'T have the `TreasuryCap` | `oft.initOftAdapterMoveCall()` without TREASURY | #### 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` 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 Sui On Sui, [`TreasuryCap`](https://docs.sui.io/standards/coin#treasury-capability) 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): ```typescript wrap theme={null} // SDK handles internal treasury enum construction const [adminCap, migrationCap] = oft.initOftMoveCall( initTx, TOKEN_TYPE, TICKET, OAPP, TREASURY, // ← Your TreasuryCap transferred to OFT internally METADATA, 6, // shared_decimals ); ``` #### 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 Sui. **When to use**: * You DON'T have access to the `TreasuryCap` (frozen, held by DAO, or inaccessible) * Token supply on Sui 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): ```typescript wrap theme={null} // SDK handles internal treasury enum construction const [adminCap, migrationCap] = oft.initOftAdapterMoveCall( initTx, TOKEN_TYPE, TICKET, OAPP, METADATA, // ← No TreasuryCap needed for adapter 6, // shared_decimals ); ``` 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 Sui 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. ```rust wrap theme={null} public fun send( self: &mut OFT, oapp: &mut OApp, sender: &OFTSender, // Authorization context (from oft_sender module) send_param: &SendParam, // Complete send parameters coin_provided: &mut Coin, // Coin to debit tokens from native_coin_fee: Coin, // Fee payment in SUI zro_coin_fee: Option>, // Optional ZRO payment refund_address: Option
, // Optional refund address clock: &Clock, // Clock for rate limiting ctx: &mut TxContext, ): (Call, OFTSendContext) ``` **Returns**: A tuple containing: 1. `Call` - 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 Sui involves securely processing incoming crosschain messages, validating the source and payload, and minting or unlocking tokens to deliver them to the intended recipient. ```rust wrap theme={null} public fun lz_receive( self: &mut OFT, oapp: &OApp, // Associated OApp for validation call: Call,// Call from Executor via Endpoint clock: &Clock, // Clock for rate limiting ctx: &mut TxContext, ) ``` **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](/v2/concepts/technical-reference/oft-reference#shared-decimals). ### Sui-Specific Constraint: u64 Balance Limit ### u64 Balance Overflow Sui'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**: ``` Maximum supply = (2^64 - 1) / (10^decimals) ``` Choose your decimals carefully during token deployment. ### Recommended Configuration for Sui | Local Decimals | Max Total Supply | Recommendation | | ----------------- | ----------------- | -------------- | | 6 | \~18.4 trillion | ✅ Recommended | | 9 | \~18.4 billion | ✅ Recommended | | 18 (EVM standard) | \~18 whole tokens | ❌ Avoid on Sui | **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](/v2/concepts/technical-reference/oft-reference#shared-decimals). ## Registration with Endpoint After initializing your OFT, you must register it with the LayerZero Endpoint to enable crosschain messaging. ### Using OFT SDK ```typescript wrap theme={null} import {SDK} from '@layerzerolabs/lz-sui-sdk-v2'; import {OFT} from '@layerzerolabs/lz-sui-oft-sdk-v2'; import {Stage} from '@layerzerolabs/lz-definitions'; import {Transaction} from '@mysten/sui/transactions'; const sdk = new SDK({client, stage: Stage.MAINNET}); const oft = new OFT(sdk, OFT_PKG, OFT_OBJECT, TOKEN_TYPE, OAPP); const regTx = new Transaction(); // SDK auto-generates lz_receive_info internally! await oft.registerOAppMoveCall( regTx, TOKEN_TYPE, // "0xTOKEN_PKG::myoft::MYOFT" OFT_OBJECT, // OFT object ID OAPP, // OApp object ID '0xfbece0b75d097c31b9963402a66e49074b0d3a2a64dd0ed666187ca6911a4d12', // OFTComposerManager ); const regResult = await client.signAndExecuteTransaction({ transaction: regTx, signer: keypair, options: {showObjectChanges: true}, }); // Wait for finality await client.waitForTransaction({digest: regResult.digest}); console.log('✅ Registration complete:', regResult.digest); ``` **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: `0xfbece0b75d097c31b9963402a66e49074b0d3a2a64dd0ed666187ca6911a4d12` OFTComposerManager address on testnet: `0x90384f5f6034604f76ac99bbdd25bc3c9c646a6e13a27f14b530733a8e98db99` ## Configuration After registration, configure your OFT to enable crosschain token transfers. ### Using OApp SDK for Configuration on Sui All configuration is done through the base SDK's OApp instance. Configure security settings **before** setting peers to open the pathway. ### Endpoint IDs The examples below use EID `30184` (Base Mainnet). For a complete list of endpoint IDs across all supported chains, see [Deployed Contracts](/v2/deployments/deployed-contracts). ```typescript wrap theme={null} import { SDK, PACKAGE_ULN_302_ADDRESS, OBJECT_ULN_302_ADDRESS, PACKAGE_DVN_LAYERZERO_ADDRESS, OAppUlnConfigBcs, } from '@layerzerolabs/lz-sui-sdk-v2'; import {Stage} from '@layerzerolabs/lz-definitions'; const sdk = new SDK({client, stage: Stage.MAINNET}); const oapp = sdk.getOApp(OFT_PKG); // Use OFT package ID // Step 1: Set Send Library (recommended - custom send message library) const sendLibTx = new Transaction(); await oapp.setSendLibraryMoveCall( sendLibTx, 30184, // Destination EID customSendLibraryAddress, ); await client.signAndExecuteTransaction({transaction: sendLibTx, signer: keypair}); // Step 1: Set Receive Library (recommended - custom receive message library) const receiveLibTx = new Transaction(); await oapp.setReceiveLibraryMoveCall( receiveLibTx, 30184, // Source EID customReceiveLibraryAddress, 0, // Grace period ); await client.signAndExecuteTransaction({transaction: receiveLibTx, signer: keypair}); // Step 2: Configure Receive DVN (recommended - receive verification) const receiveConfig = OAppUlnConfigBcs.serialize({ use_default_confirmations: false, use_default_required_dvns: false, use_default_optional_dvns: true, uln_config: { confirmations: 15, // Replace with a non-LayerZero-Labs DVN; see /v2/deployments/dvn-addresses required_dvns: [ PACKAGE_DVN_LAYERZERO_ADDRESS[Stage.MAINNET], PACKAGE_DVN__ADDRESS[Stage.MAINNET], ], optional_dvns: [], optional_dvn_threshold: 0, }, }).toBytes(); const receiveConfigTx = new Transaction(); const receiveConfigCall = await oapp.setConfigMoveCall( receiveConfigTx, PACKAGE_ULN_302_ADDRESS[Stage.MAINNET], 30184, // Destination EID 3, // CONFIG_TYPE_RECEIVE_ULN receiveConfig, ); receiveConfigTx.moveCall({ target: `${PACKAGE_ULN_302_ADDRESS[Stage.MAINNET]}::uln_302::set_config`, arguments: [receiveConfigTx.object(OBJECT_ULN_302_ADDRESS[Stage.MAINNET]), receiveConfigCall], }); await client.signAndExecuteTransaction({transaction: receiveConfigTx, signer: keypair}); // Step 2: Configure Send DVN (recommended - send verification) const sendConfig = OAppUlnConfigBcs.serialize({ use_default_confirmations: false, use_default_required_dvns: false, use_default_optional_dvns: true, uln_config: { confirmations: 15, // Replace with a non-LayerZero-Labs DVN; see /v2/deployments/dvn-addresses required_dvns: [ PACKAGE_DVN_LAYERZERO_ADDRESS[Stage.MAINNET], PACKAGE_DVN__ADDRESS[Stage.MAINNET], ], optional_dvns: [], optional_dvn_threshold: 0, }, }).toBytes(); const sendConfigTx = new Transaction(); const sendConfigCall = await oapp.setConfigMoveCall( sendConfigTx, PACKAGE_ULN_302_ADDRESS[Stage.MAINNET], 30184, 2, // CONFIG_TYPE_SEND_ULN (outbound messages) sendConfig, ); sendConfigTx.moveCall({ target: `${PACKAGE_ULN_302_ADDRESS[Stage.MAINNET]}::uln_302::set_config`, arguments: [sendConfigTx.object(OBJECT_ULN_302_ADDRESS[Stage.MAINNET]), sendConfigCall], }); await client.signAndExecuteTransaction({transaction: sendConfigTx, signer: keypair}); // Step 3: Set Enforced Options (optional - minimum gas requirements) const options = Options.newOptions() .addExecutorLzReceiveOption(80000, 0) // 80k gas for destination .toBytes(); const optionsTx = new Transaction(); await oapp.setEnforcedOptionsMoveCall( optionsTx, 30184, // Destination EID 1, // Message type (1 = SEND) options, ); await client.signAndExecuteTransaction({transaction: optionsTx, signer: keypair}); // Step 4: Configure OFT Settings (optional - rate limits) const rateLimitTx = new Transaction(); await oft.setRateLimitMoveCall( rateLimitTx, 30184, // Destination EID false, // Outbound 1000000n, // 1M tokens per window 86400n, // 24 hours ); await client.signAndExecuteTransaction({transaction: rateLimitTx, signer: keypair}); // Step 4: Configure OFT Settings (optional - fees) const feeTx = new Transaction(); await oft.setFeeBpsMoveCall(feeTx, 30184, 30); // 0.3% fee await client.signAndExecuteTransaction({transaction: feeTx, signer: keypair}); // Step 5: Set Peer LAST (required - opens pathway for messaging) const peerTx = new Transaction(); await oapp.setPeerMoveCall( peerTx, 30184, // Destination EID (e.g., Base) Buffer.from('0000000000000000000000006D2e17A05B9Ac62b8499f4bF4757e261005c03A5', 'hex'), ); await client.signAndExecuteTransaction({transaction: peerTx, signer: keypair}); ``` **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](/v2/developers/sui/configuration/dvn-executor-config). ### Configuring Remote Chains to Send to Sui When configuring OFTs on other chains (e.g., EVM, Solana) to send tokens **to Sui**, follow standard LayerZero configuration but note these Sui-specific requirements: **1. Use Package ID as Peer**: ```solidity wrap theme={null} // On EVM: Use Sui OFT PACKAGE ID (not object ID!) myOFT.setPeer( 30378, // Sui mainnet EID bytes32(0x061a47bf...) // Your Sui OFT Package ID ); ``` **2. Set Enforced Options for Sui Destination**: Based on gas profiling, configure appropriate gas limits for Sui: ```solidity wrap theme={null} // On EVM: Set enforced options for Sui pathway import { Options } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/ExecutorOptions.sol"; bytes memory options = Options.newOptions() .addExecutorLzReceiveOption(5000, 0); // 5k gas units, no msg.value myOFT.setEnforcedOptions( EnforcedOptionParam({ eid: 30378, // Sui mainnet msgType: SEND, options: options }) ); ``` **Gas requirements**: * Sui's `lz_receive` uses 2,000-5,000 MIST for computation * Use 5,000 gas units for safe buffer * No `msg.value` needed (Sui handles storage internally) **3. Standard DVN Configuration**: DVN configuration on remote chains follows standard LayerZero patterns - no Sui-specific changes needed. See the platform-specific implementation guides for EVM and Solana configuration. ## Example Usage ### Sending Tokens (TypeScript SDK) ```typescript wrap theme={null} import {OFT} from '@layerzerolabs/lz-sui-oft-sdk-v2'; // Quote the fee const {nativeFee} = await oft.quote({ dstEid: 30101, // Ethereum to: recipientBytes32, amountLD: BigInt(1000000), // 1 token (6 decimals) options: optionsBytes, }); // Send tokens const receipt = await oft.send({ dstEid: 30101, to: recipientBytes32, amountLD: BigInt(1000000), minAmountLD: BigInt(950000), // 5% slippage nativeFee, options: optionsBytes, }); ``` For more SDK usage, see [OFT SDK Documentation](/v2/developers/sui/oft/sdk). ## 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 with `Stage.MAINNET` (no hardcoded addresses) **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](/v2/developers/sui/configuration/dvn-executor-config). ## Next Steps * [OFT SDK Documentation](/v2/developers/sui/oft/sdk) - Complete SDK methods and TypeScript integration * [Configuration Guide](/v2/developers/sui/configuration/dvn-executor-config) - DVN, executor, and gas configuration * [OApp Overview](/v2/developers/sui/oapp/overview) - Base messaging standard * [Technical Overview](/v2/developers/sui/technical-overview) - Sui fundamentals and Call pattern * [Protocol Overview](/v2/developers/sui/protocol-overview) - Complete message workflows * [Troubleshooting](/v2/developers/sui/troubleshooting/common-errors) - Common deployment issues # Sui OFT SDK Source: https://docs.layerzero.network/v2/developers/sui/oft/sdk The LayerZero Sui OFT SDK provides TypeScript utilities for interacting with OFT contracts on the Sui blockchain, enabling seamless crosschain token... The LayerZero Sui OFT SDK provides TypeScript utilities for interacting with OFT contracts on the Sui blockchain, enabling seamless crosschain token transfers. ## Installation Install both the core Sui SDK and the OFT-specific SDK: ```bash wrap theme={null} npm install @layerzerolabs/lz-sui-sdk-v2 @layerzerolabs/lz-sui-oft-sdk-v2 ``` Or with yarn: ```bash wrap theme={null} yarn add @layerzerolabs/lz-sui-sdk-v2 @layerzerolabs/lz-sui-oft-sdk-v2 ``` ## Setup ### Initialize the SDKs The recommended pattern uses automatic address fetching from the protocol SDK: ```typescript wrap theme={null} import {SuiClient} from '@mysten/sui/client'; import {SDK} from '@layerzerolabs/lz-sui-sdk-v2'; import {OFT} from '@layerzerolabs/lz-sui-oft-sdk-v2'; import {Stage} from '@layerzerolabs/lz-definitions'; // Setup Sui client const client = new SuiClient({url: 'https://fullnode.mainnet.sui.io:443'}); // Initialize protocol SDK (automatically fetches LayerZero protocol addresses) const sdk = new SDK({client, stage: Stage.MAINNET}); // Initialize OFT SDK with your OFT package ID const oft = new OFT( sdk, // Protocol SDK instance oftPackageId, // Your OFT package ID (NOT OFT CallCap ID!) oftObjectId, // Optional: OFT object ID (set after init) tokenType, // Optional: "0x123::mycoin::MYCOIN" oappObjectId, // Optional: OApp object ID adminCapId, // Optional: Admin cap ID (can query later) ); ``` **Critical Notes**: * **First parameter**: Use your OFT **package ID** (where your code is deployed) * **SDK automatic addresses**: No need to hardcode LayerZero protocol addresses * **Optional parameters**: Can be `undefined` initially and set later * **After initialization**: Update `oft.oftObjectId = newObjectId` **Getting OApp Instance** (for peer/DVN configuration): ```typescript wrap theme={null} // Use SDK to get OApp instance (recommended) const oapp = sdk.getOApp(oftPackageId); // Use package ID // Configure peers and DVNs through OApp await oapp.setPeerMoveCall(tx, dstEid, peerBytes); await oapp.setConfigMoveCall(tx, lib, eid, configType, config); ``` **Do NOT manually instantiate** `new OApp(...)` - this will fail to find your OApp in the registry. Always use `sdk.getOApp(packageId)`. ## SDK Architecture The Sui OFT SDK consists of two complementary SDKs: ### Base SDK (`@layerzerolabs/lz-sui-sdk-v2`) Provides core LayerZero protocol functionality: * **OApp operations**: Peer configuration, messaging, registration * **Endpoint interaction**: Channel initialization, library configuration * **DVN/Executor configuration**: Security stack setup * **Protocol address exports**: All deployed contract addresses **When to use**: For OApp configuration, peer setup, DVN configuration, and general protocol interactions. ### OFT SDK (`@layerzerolabs/lz-sui-oft-sdk-v2`) Extends the base SDK with OFT-specific functionality: * **OFT initialization**: `initOftMoveCall()`, `initOftAdapterMoveCall()` * **Registration**: `registerOAppMoveCall()` (auto-generates lz\_receive\_info) * **Rate limiting**: Per-pathway token flow limits * **Fee management**: Crosschain fee configuration * **Pause control**: Emergency pause functionality **When to use**: For OFT deployment, token-specific operations, and OFT lifecycle management. ### Relationship ```typescript wrap theme={null} // Base SDK provides OApp functionality const sdk = new SDK({ client, stage: Stage.MAINNET }); const oapp = sdk.getOApp(packageId); // OApp configuration // OFT SDK extends with token-specific features const oft = new OFT(sdk, oftPackageId, ...); // OFT operations ``` ## SDK Address Exports The SDK provides address exports for all protocol contracts, eliminating hardcoded values: ```typescript wrap theme={null} import { // Object addresses (shared instances everyone uses) OBJECT_ENDPOINT_V2_ADDRESS, OBJECT_ULN_302_ADDRESS, // Package addresses (where code lives) PACKAGE_OAPP_ADDRESS, PACKAGE_ULN_302_ADDRESS, PACKAGE_DVN_LAYERZERO_ADDRESS, // Helpers OAppUlnConfigBcs, Stage, } from '@layerzerolabs/lz-sui-sdk-v2'; // Get addresses for your network const endpointObj = OBJECT_ENDPOINT_V2_ADDRESS[Stage.MAINNET]; const uln302Obj = OBJECT_ULN_302_ADDRESS[Stage.MAINNET]; const uln302Pkg = PACKAGE_ULN_302_ADDRESS[Stage.MAINNET]; const dvnLayerZero = PACKAGE_DVN_LAYERZERO_ADDRESS[Stage.MAINNET]; ``` **Benefits**: * **Network switching**: Toggle between mainnet/testnet via `Stage` enum * **SDK updates**: Address changes handled automatically * **No magic numbers**: Self-documenting configuration * **Type safety**: TypeScript ensures correct usage **Available exports**: | Export | Description | Usage | | ------------------------------- | ---------------------- | -------------------------- | | `OBJECT_ENDPOINT_V2_ADDRESS` | Endpoint shared object | Pass to Endpoint functions | | `OBJECT_ULN_302_ADDRESS` | ULN302 shared object | Pass to ULN302 functions | | `PACKAGE_OAPP_ADDRESS` | OApp package ID | Reference for OApp code | | `PACKAGE_ULN_302_ADDRESS` | ULN302 package ID | Use in move call targets | | `PACKAGE_DVN_LAYERZERO_ADDRESS` | LayerZero DVN package | DVN configuration | | `OAppUlnConfigBcs` | Config serializer | Encode DVN configuration | ## Complete Working Example ### Reference Implementation All examples on this page are based on proven mainnet deployments. The complete reference implementation demonstrating these patterns is available in the LayerZero test repository at `test-repo/sui-oft-complete/deploy_with_oft_sdk.mjs`. Based on proven mainnet deployment: ```typescript wrap theme={null} import {SuiClient} from '@mysten/sui/client'; import {Transaction} from '@mysten/sui/transactions'; import {Ed25519Keypair} from '@mysten/sui/keypairs/ed25519'; import {SDK} from '@layerzerolabs/lz-sui-sdk-v2'; import {OFT} from '@layerzerolabs/lz-sui-oft-sdk-v2'; import {Stage} from '@layerzerolabs/lz-definitions'; const client = new SuiClient({url: 'https://fullnode.mainnet.sui.io:443'}); const keypair = Ed25519Keypair.fromSecretKey(secretKeyBytes); // Initialize protocol SDK const sdk = new SDK({client, stage: Stage.MAINNET}); // Initialize OFT SDK const oft = new OFT(sdk, oftPackageId, undefined, tokenType, oappObjectId); // Step 1: Initialize OFT const initTx = new Transaction(); const [adminCap, migrationCap] = oft.initOftMoveCall( initTx, tokenType, ticketObjectId, oappObjectId, treasuryCapId, coinMetadataId, 6, // shared_decimals ); initTx.transferObjects([adminCap, migrationCap], sender); const initResult = await client.signAndExecuteTransaction({ transaction: initTx, signer: keypair, options: {showObjectChanges: true}, }); // ✅ CRITICAL: Wait for finality before referencing created objects await client.waitForTransaction({digest: initResult.digest}); const oftObjectId = initResult.objectChanges.find( (c) => c.type === 'created' && c.objectType.includes('oft::OFT<'), ).objectId; // Update OFT SDK with object ID oft.oftObjectId = oftObjectId; // Step 2: Register (SDK auto-generates lz_receive_info) const regTx = new Transaction(); await oft.registerOAppMoveCall( regTx, tokenType, oftObjectId, oappObjectId, '0xfbece0b75d097c31b9963402a66e49074b0d3a2a64dd0ed666187ca6911a4d12', // OFTComposerManager ); const regResult = await client.signAndExecuteTransaction({transaction: regTx, signer: keypair}); // Wait for finality before next operation await client.waitForTransaction({digest: regResult.digest}); // Step 3: Configure via OApp SDK const oapp = sdk.getOApp(oftPackageId); const peerTx = new Transaction(); await oapp.setPeerMoveCall(peerTx, dstEid, peerBytes); await client.signAndExecuteTransaction({transaction: peerTx, signer: keypair}); console.log('Deployment complete!'); ``` **Key SDK Methods Used**: * `oft.initOftMoveCall()` - Initialize OFT with treasury * `oft.registerOAppMoveCall()` - Register and auto-generate lz\_receive\_info * `sdk.getOApp()` - Get OApp instance for configuration * `oapp.setPeerMoveCall()` - Configure peer addresses * `oapp.setConfigMoveCall()` - Configure DVNs/executors *** ## Available SDK Methods ### Base SDK (OApp Operations) The base SDK provides methods for OApp configuration through `sdk.getOApp(packageId)`: ```typescript wrap theme={null} const oapp = sdk.getOApp(packageId); // Peer Configuration await oapp.setPeerMoveCall(tx, eid, peerBytes); // Set peer for destination await oapp.hasPeer(eid); // Check if peer configured await oapp.getPeer(eid); // Get peer address // DVN/Executor Configuration await oapp.setConfigMoveCall(tx, lib, eid, configType, config); // Set DVN/executor config await oapp.getConfig(lib, eid, configType); // Get current config // OApp Registration await oapp.registerOAppMoveCall(tx, oappObjectId, oappInfo); // Register with Endpoint await oapp.setOAppInfoMoveCall(tx, oappInfo); // Update OApp info // Enforced Options await oapp.setEnforcedOptionsMoveCall(tx, eid, msgType, options); // Set minimum execution params await oapp.getEnforcedOptions(eid, msgType); // Get enforced options await oapp.combineOptions(eid, msgType, extraOptions); // Combine with user options // Admin Operations await oapp.setDelegateMoveCall(tx, newDelegate); // Transfer admin rights await oapp.setSendLibraryMoveCall(tx, dstEid, library); // Set custom send library await oapp.setReceiveLibraryMoveCall(tx, srcEid, library, grace); // Set custom receive library // Channel Management await oapp.initChannelMoveCall(tx, remoteEid, remoteOApp); // Initialize messaging channel await oapp.skipMoveCall(tx, srcEid, sender, nonce); // Skip stuck message await oapp.clearMoveCall(tx, srcEid, sender, nonce, guid, msg); // Clear verified message ``` ### OFT SDK Methods The OFT SDK provides token-specific operations: ```typescript wrap theme={null} const oft = new OFT(sdk, oftPackageId, oftObjectId, tokenType, oappObjectId); // Initialization initOftMoveCall(tx, coinType, ticket, oapp, treasury, metadata, sharedDecimals); initOftAdapterMoveCall(tx, coinType, ticket, oapp, metadata, sharedDecimals); // Registration (auto-generates lz_receive_info!) await registerOAppMoveCall(tx, coinType, oftObj, oappObj, composerMgr); // Rate Limiting await setRateLimitMoveCall(tx, eid, inbound, limit, windowSeconds); // Set rate limit await unsetRateLimitMoveCall(tx, eid, inbound); // Remove rate limit await rateLimitConfig(eid, inbound); // Get config await rateLimitCapacity(eid, inbound); // Get remaining capacity await rateLimitInFlight(eid, inbound); // Get current usage // Fee Management await setFeeBpsMoveCall(tx, eid, feeBps); // Set fee for pathway await setDefaultFeeBpsMoveCall(tx, feeBps); // Set default fee await setFeeDepositAddressMoveCall(tx, address); // Set fee recipient await unsetFeeBpsMoveCall(tx, eid); // Remove pathway fee await effectiveFeeBps(eid); // Get effective fee await defaultFeeBps(); // Get default fee await feeDepositAddress(); // Get fee recipient await hasOftFee(eid); // Check if fee configured // Pause Control await setPauseMoveCall(tx, paused); // Pause/unpause OFT await isPaused(); // Check pause status // Queries await sharedDecimals(); // Get shared decimals await decimalConversionRate(); // Get conversion rate await isAdapter(); // Check if adapter mode await adminCap(); // Get AdminCap address await oappObject(); // Get OApp object ID await oftVersion(); // Get OFT version await coinMetadata(); // Get metadata ID ``` *** ## Core Methods ### quote() Get a fee quote for sending tokens crosschain: ```typescript wrap theme={null} const {nativeFee, lzTokenFee} = await oft.quote( client, { payer: keypair.toSuiAddress(), tokenMint: '0x...', // Coin type tokenEscrow: '0x...', // OFT escrow object }, { dstEid: 30101, // Destination endpoint ID (e.g., Ethereum) to: Buffer.from('0x' + '1'.repeat(64), 'hex'), // 32-byte recipient address amountLD: BigInt(1000000), // Amount in local decimals minAmountLD: BigInt(950000), // Minimum amount (slippage) options: Buffer.from([]), // Execution options composeMsg: undefined, // Optional compose message payInLzToken: false, // Pay fee in native or LZ token }, ); console.log(`Native fee: ${nativeFee} wei`); console.log(`LZ token fee: ${lzTokenFee} wei`); ``` ### send() Send tokens crosschain: ```typescript wrap theme={null} const receipt = await oft.send( client, { payer: keypair, // Signer keypair tokenMint: '0x...', // Coin type tokenEscrow: '0x...', // OFT escrow object tokenSource: '0x...', // Source token account }, { dstEid: 30101, to: Buffer.from(recipientBytes32), amountLD: BigInt(1000000), minAmountLD: BigInt(950000), options: Buffer.from([]), composeMsg: undefined, nativeFee: nativeFee, lzTokenFee: BigInt(0), }, ); console.log('Transaction:', receipt.digest); ``` ### getOFTConfig() Read OFT configuration: ```typescript wrap theme={null} const config = await oft.getOFTConfig(client); console.log('Token type:', config.tokenType); console.log('Shared decimals:', config.sharedDecimals); console.log('Endpoint:', config.endpoint); ``` ### getPeer() Get peer OFT address for a specific chain: ```typescript wrap theme={null} const peer = await oft.getPeer(client, 30101); // Ethereum console.log('Peer address:', Buffer.from(peer).toString('hex')); ``` ## Building Execution Options Use the `Options` helper from the core SDK: ```typescript wrap theme={null} import {Options} from '@layerzerolabs/lz-v2-utilities'; // For EVM destination const options = Options.newOptions() .addExecutorLzReceiveOption(60000, 0) // gas limit, msg.value .toBytes(); // For Sui/Solana destination with ATA/object creation const optionsWithValue = Options.newOptions() .addExecutorLzReceiveOption(200000, 2039280) // gas + rent .toBytes(); ``` ### Gas Limits by Destination | Destination | Recommended Gas Limit | Notes | | ----------- | --------------------- | -------------------------------------- | | EVM chains | 60,000 - 200,000 | Higher for complex logic | | Solana | 200,000 | May need msg.value for ATA | | Sui | 200,000+ | May need msg.value for object creation | | Aptos | 100,000 | Adjust based on complexity | ### msg.value Considerations When sending **to Sui**, you may need to include `msg.value` for: * Creating a new coin object for the recipient * Storage rent for the new object Calculate rent based on object size (typically \~0.002 SUI). ## Complete Example ### Sending Tokens from Sui to Ethereum ```typescript wrap theme={null} import {SuiClient, getFullnodeUrl} from '@mysten/sui.js/client'; import {Ed25519Keypair} from '@mysten/sui.js/keypairs/ed25519'; import {OFT} from '@layerzerolabs/lz-sui-oft-sdk-v2'; import {Options} from '@layerzerolabs/lz-v2-utilities'; async function sendTokens() { // Setup const client = new SuiClient({url: getFullnodeUrl('mainnet')}); const keypair = Ed25519Keypair.deriveKeypair(process.env.MNEMONIC!); const oft = new OFT({ client, oftAddress: process.env.OFT_PACKAGE!, oftStoreId: process.env.OFT_STORE!, }); // Prepare params const dstEid = 30101; // Ethereum const recipient = '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'; // Remove 0x, pad to 32 bytes const recipientBytes32 = Buffer.from(recipient.padStart(64, '0'), 'hex'); const amount = BigInt(1_000000); // 1 token (6 decimals) const minAmount = BigInt(950000); // 5% slippage // Build options const options = Options.newOptions().addExecutorLzReceiveOption(60000, 0).toBytes(); // Quote fee console.log('Getting quote...'); const {nativeFee} = await oft.quote( client, { payer: keypair.toSuiAddress(), tokenMint: process.env.TOKEN_TYPE!, tokenEscrow: process.env.OFT_ESCROW!, }, { dstEid, to: recipientBytes32, amountLD: amount, minAmountLD: minAmount, options: Buffer.from(options), composeMsg: undefined, payInLzToken: false, }, ); console.log(`Fee: ${nativeFee / BigInt(1e9)} SUI`); // Send tokens console.log('Sending tokens...'); const receipt = await oft.send( client, { payer: keypair, tokenMint: process.env.TOKEN_TYPE!, tokenEscrow: process.env.OFT_ESCROW!, tokenSource: process.env.TOKEN_ACCOUNT!, }, { dstEid, to: recipientBytes32, amountLD: amount, minAmountLD: minAmount, options: Buffer.from(options), composeMsg: undefined, nativeFee, lzTokenFee: BigInt(0), }, ); console.log('- Sent!'); console.log('Transaction:', receipt.digest); console.log('Track at: https://layerzeroscan.com'); } sendTokens().catch(console.error); ``` ## Reading Token Balances Check OFT token balances using the Sui client: ```typescript wrap theme={null} import {SuiClient} from '@mysten/sui.js/client'; const client = new SuiClient({url: getFullnodeUrl('mainnet')}); // Get all coins of a specific type for an address const coins = await client.getCoins({ owner: '0x...', coinType: '0x...::token::TOKEN', }); const totalBalance = coins.data.reduce((sum, coin) => sum + BigInt(coin.balance), BigInt(0)); console.log('Balance:', totalBalance.toString()); ``` ## Integration with Core SDK The OFT SDK builds on the core Sui SDK: ```typescript wrap theme={null} import {createEndpointClient, createOAppClient} from '@layerzerolabs/lz-sui-sdk-v2'; // For lower-level Endpoint interactions const endpoint = createEndpointClient({ address: '0x...', }); // For OApp functionality const oapp = createOAppClient({ address: '0x...', }); ``` ## Error Handling Common errors and how to handle them: ```typescript wrap theme={null} try { const receipt = await oft.send(/* ... */); } catch (error) { if (error.message.includes('Insufficient funds')) { console.error('Not enough tokens or SUI for gas'); } else if (error.message.includes('Invalid peer')) { console.error('Peer not configured for destination chain'); } else if (error.message.includes('Channel not initialized')) { console.error('Must initialize channel first'); } else { console.error('Unknown error:', error); } } ``` ## Admin Functions The SDK provides admin functions for OFT management (requires `AdminCap`): ### Pause/Unpause ```typescript wrap theme={null} // Pause OFT operations (emergency) await oft.setPauseMoveCall(tx, true); // Unpause await oft.setPauseMoveCall(tx, false); ``` ### Fee Configuration ```typescript wrap theme={null} // Set default fee rate (in basis points, 10000 = 100%) await oft.setDefaultFeeBpsMoveCall(tx, 30); // 0.3% fee // Set fee for specific destination await oft.setFeeBpsMoveCall(tx, 30101, 50); // 0.5% for Ethereum // Set fee deposit address await oft.setFeeDepositAddressMoveCall(tx, feeRecipientAddress); ``` ### Rate Limiting ```typescript wrap theme={null} // Set outbound rate limit await oft.setOutboundRateLimitMoveCall(tx, { dstEid: 30101, limit: BigInt(1000000), // Max tokens per window window: 86400, // 24 hours in seconds }); // Set inbound rate limit await oft.setInboundRateLimitMoveCall(tx, { srcEid: 30101, limit: BigInt(1000000), window: 86400, }); ``` ### Peer Configuration ```typescript wrap theme={null} // Set peer OFT on destination chain await oft.setPeerMoveCall(tx, 30101, peerBytes32); ``` ## Best Practices 1. **Always Quote First**: Get fee estimates before sending 2. **Set Slippage**: Use `minAmountLD` to protect against dust/precision loss 3. **Check Balances**: Verify sufficient tokens and SUI for gas 4. **Use TypeScript**: Leverage type safety for parameter validation 5. **Test on Testnet**: Always test on testnet before mainnet deployments 6. **Monitor Rate Limits**: Configure appropriate limits for production 7. **Secure Admin Cap**: Use multisig or hardware wallet for admin operations ## Next Steps * [OFT Overview](/v2/developers/sui/oft/overview) - OFT architecture and deployment guide * [Configuration Guide](/v2/developers/sui/configuration/dvn-executor-config) - DVN, executor, and gas setup * [OApp Overview](/v2/developers/sui/oapp/overview) - Base messaging standard * [Technical Overview](/v2/developers/sui/technical-overview) - Sui fundamentals and architecture * [Protocol Overview](/v2/developers/sui/protocol-overview) - Complete message workflows * [Troubleshooting](/v2/developers/sui/troubleshooting/common-errors) - Common SDK issues # LayerZero V2 Sui Packages Source: https://docs.layerzero.network/v2/developers/sui/overview Overview of Sui Packages on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure crosschain messaging. The LayerZero Protocol on Sui consists of several Move packages designed to facilitate the secure movement of data, tokens, and digital assets between different blockchain environments. LayerZero provides **Sui Move Packages** that can communicate directly with the equivalent [Solidity Contract Libraries](/v2/developers/evm/overview) and other blockchain implementations deployed across supported chains. ## Sui and LayerZero Sui uses the [Move programming language](https://docs.sui.io/concepts/sui-move-concepts) and employs a unique execution model based on [**Programmable Transaction Blocks (PTBs)**](https://docs.sui.io/concepts/transactions/prog-txn-blocks) and the **Call pattern** (a hot potato implementation using Move's [ability system](https://move-book.com/advanced-topics/abilities.html) where objects without `drop` or `store` must be explicitly consumed) to achieve crosschain functionality without traditional dynamic dispatch. ### Sui Move Packages Learn how the LayerZero V2 Protocol operates on the Sui blockchain. Deep dive into Sui object model, Call pattern, and PTB execution. Build the instructions necessary for sending arbitrary data and external function calls crosschain on Sui. Create and send Omnichain Fungible Tokens (OFTs) on the Sui blockchain. Use the TypeScript SDK to interact with Sui OFTs programmatically. #### Sui Protocol Configurations Configure which decentralized verifier networks (DVNs) secure your messages. Configure who executes your messages on the destination chain. Set the amount of gas to deliver to the destination chain.
You can find all [**LayerZero Sui Packages**](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/sui/contracts) here. ### Tooling and Resources Sui development relies on the [Move programming language](https://docs.sui.io/concepts/sui-move-concepts) and the [Sui CLI](https://docs.sui.io/references/cli). For comprehensive information, see the [Sui Documentation](https://docs.sui.io/). LayerZero provides developer tooling to simplify the package development, testing, and deployment process: [LayerZero Scan](/v2/developers/layerzero-scan-explorer): a comprehensive crosschain explorer, search, API, and analytics platform for tracking and debugging your omnichain transactions. **TypeScript SDKs**: * [`@layerzerolabs/lz-sui-sdk-v2`](https://www.npmjs.com/package/@layerzerolabs/lz-sui-sdk-v2): Core SDK for interacting with LayerZero on Sui * [`@layerzerolabs/lz-sui-oft-sdk-v2`](https://www.npmjs.com/package/@layerzerolabs/lz-sui-oft-sdk-v2): OFT-specific SDK for token operations You can also ask for help or follow development in the [Discord](https://discord.com/invite/ktbvm8Nkcr). # LayerZero V2 Sui Protocol Implementation Source: https://docs.layerzero.network/v2/developers/sui/protocol-overview Overview of Sui Protocol Implementation on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure... This page provides a deep technical dive into the LayerZero V2 protocol implementation on Sui, documenting the complete message lifecycle with actual contract code, function signatures, and transaction analysis. **What you'll find**: * Complete send workflow (7 steps from OApp to MessagingReceipt) * DVN verification and commit process with storage management * Executor delivery and OApp receive handling * Real transaction analysis from mainnet * Event emissions and monitoring * Recovery operations (skip, clear, nilify, burn) **Target audience**: Developers who understand Sui basics and want to deeply understand the protocol implementation. ### Prerequisites Before reading this page, familiarize yourself with Sui fundamentals in [Technical Overview](/v2/developers/sui/technical-overview). For SDK usage and practical implementation, see [OFT SDK](/v2/developers/sui/oft/sdk) or implementation guides for [OApp](/v2/developers/sui/oapp/overview) and [OFT](/v2/developers/sui/oft/overview). *** This page documents the complete message lifecycle with contract-level implementation details: * **Send Workflow:** Message initiation, fee calculation, nonce management, and packet dispatch * **Verification Workflow:** DVN submission, threshold checking, and verification commitment * **Receive Workflow:** Executor delivery, payload clearing, and OApp processing ## Send Overview When a user sends a crosschain message, the following high-level steps occur within a single Programmable Transaction Block (PTB): 1. **OApp Initiates Send:** User calls the OApp module's `send()` function, which creates a `Call` object 2. **Endpoint Processes:** The Endpoint increments the outbound nonce, constructs a packet with GUID, and routes to the send library 3. **ULN302 Assigns Jobs:** The message library creates child `Call` objects for the executor and each DVN 4. **Workers Calculate Fees:** Executor and DVNs estimate their fees and return `FeeRecipient` results 5. **Confirmation Chain:** Results flow back through confirm functions, aggregating fees and emitting events 6. **OApp Finalizes:** The OApp confirms the send call to extract the `MessagingReceipt` ### Endpoint Send The `EndpointV2` module orchestrates message sending through its shared object. #### EndpointV2 Shared Object ```rust wrap theme={null} /// The main endpoint object that coordinates all crosschain messaging operations public struct EndpointV2 has key { id: UID, eid: u32, // This chain's LayerZero endpoint ID call_cap: CallCap, // Capability for creating calls oapp_registry: OAppRegistry, // Registry of all registered OApps composer_registry: ComposerRegistry, // Registry for compose handlers message_lib_manager: MessageLibManager, // Manages send/receive libraries } ``` #### MessagingChannel per OApp Each OApp gets a dedicated `MessagingChannel` [shared object](https://docs.sui.io/concepts/object-ownership/shared) for parallel execution: ```rust wrap theme={null} /// Shared object managing message channels for a specific OApp public struct MessagingChannel has key { id: UID, oapp: address, // OApp owner of this channel channels: Table, // Maps (eid, remote_oapp) → channel state is_sending: bool, // Prevents reentrancy } /// Composite key identifying a specific channel path public struct ChannelKey has copy, drop, store { remote_eid: u32, // Destination endpoint ID remote_oapp: Bytes32, // Remote OApp address (32 bytes) } /// State for a specific channel path public struct Channel has store { outbound_nonce: u64, // Next nonce for sends lazy_inbound_nonce: u64, // Last cleared (executed) nonce inbound_payload_hashes: Table, // Verified messages awaiting execution } ``` #### Step 1: OApp Creates Send Call The OApp module creates a `Call` object targeting the Endpoint: ```rust wrap theme={null} /// From oapp::send() public fun send( self: &mut OApp, oapp_cap: &CallCap, // Proves OApp ownership dst_eid: u32, // Destination endpoint ID message: vector, // Message payload options: vector, // Execution options native_fee: Coin, // Fee payment in SUI lz_token_fee: Option>, // Optional ZRO payment refund_address: address, // Address for refunds ctx: &mut TxContext, ): Call { self.assert_oapp_cap(oapp_cap); // Lookup peer address for destination let receiver = self.peer.get_peer(dst_eid); // Combine enforced options with provided options let final_options = self.enforced_options.combine_options(dst_eid, SEND_MSG_TYPE, options); // Create send parameters let send_param = endpoint_send::create_param( dst_eid, receiver, message, final_options, native_fee, lz_token_fee, refund_address, ); // Create Call object targeting the Endpoint call::create(oapp_cap, endpoint!(), false, send_param, ctx) } ``` **Key Points**: * Returns a `Call` object (hot potato—must be consumed) * The `Call` has no `drop` or `store` abilities * PTB must route this `Call` to the Endpoint module * `oapp_cap` validates ownership via ID comparison #### Step 2: Endpoint Processes Send The Endpoint receives the `Call`, manages state, and delegates to the send library: ```rust wrap theme={null} /// From endpoint_v2::send() public fun send( self: &EndpointV2, messaging_channel: &mut MessagingChannel, call: &mut Call, ctx: &mut TxContext, ): Call { // Validate Call came from the OApp that owns this channel call.assert_caller(messaging_channel.oapp()); // Get the configured send library for this destination let (send_lib, _) = self.message_lib_manager.get_send_library( call.caller(), call.param().dst_eid() ); // Create outbound packet with incremented nonce // This is where the nonce++ happens: let send_param = messaging_channel.send(self.eid(), call.param()); // Create child Call targeting the send library call.create_single_child(&self.call_cap, send_lib, send_param, ctx) } ``` **Inside `messaging_channel.send()`**: ```rust wrap theme={null} /// From messaging_channel::send() public(package) fun send( self: &mut MessagingChannel, src_eid: u32, param: &EndpointSendParam, ): MessageLibSendParam { assert!(!self.is_sending, ESendReentrancy); self.is_sending = true; // Get or create the channel for this destination let channel_key = ChannelKey { remote_eid: param.dst_eid(), remote_oapp: param.receiver() }; if (!self.channels.contains(channel_key)) { // Initialize channel if first send let channel = Channel { outbound_nonce: 0, lazy_inbound_nonce: 0, inbound_payload_hashes: table::new(ctx), }; self.channels.add(channel_key, channel); }; // Increment nonce let channel = &mut self.channels[channel_key]; channel.outbound_nonce = channel.outbound_nonce + 1; // Build packet with GUID let packet = outbound_packet::create( channel.outbound_nonce, src_eid, self.oapp, // Sender address param.dst_eid(), param.receiver(), param.message(), ); // Create send parameters for message library message_lib_send::create_param( packet, param.options(), param.pay_in_zro(), param.native_fee_ref(), param.lz_token_fee_ref(), ) } ``` **GUID Generation**: Uses [keccak256 hashing](https://docs.sui.io/references/framework/sui-framework/hash) with [BCS-encoded](https://github.com/MystenLabs/sui/blob/main/docs/content/concepts/cryptography/system) parameters: ```rust wrap theme={null} /// From outbound_packet module public fun create(...): OutboundPacket { let guid = hash::keccak256!( &vector[ nonce.to_be_bytes(), // Convert to bytes (big-endian) src_eid.to_be_bytes(), sender.to_bytes(), dst_eid.to_be_bytes(), receiver.data(), message, ] ); OutboundPacket { nonce, src_eid, sender, dst_eid, receiver, guid, message } } ``` #### Step 3: ULN302 Assigns Jobs to Workers The ULN302 message library creates child `Call` objects for the executor and DVNs: ```rust wrap theme={null} /// From uln_302::send() public fun send( self: &Uln302, call: &mut Call, ctx: &mut TxContext, ): (Call, MultiCall) { call.assert_caller(endpoint!()); assert!(self.is_supported_eid(call.param().base().packet().dst_eid()), EUnsupportedEid); // Get executor and DVN parameters from SendUln let (executor, executor_param, dvns, dvn_params) = self.send_uln.send(call.param()); // Create a new child batch (capacity: 1 executor + N DVNs) call.new_child_batch(&self.call_cap, 1); // Create child Call for each DVN let dvn_calls = dvns.zip_map!(dvn_params, |dvn, param| call.create_child(&self.call_cap, dvn, param, false, ctx) ); // Create child Call for executor (marked as last child) let executor_call = call.create_child(&self.call_cap, executor, executor_param, true, ctx); // Return executor call and MultiCall wrapper for DVN calls (executor_call, multi_call::create(&self.call_cap, dvn_calls)) } ``` **Inside `send_uln::send()`**: ```rust wrap theme={null} /// From send_uln::send() - prepares worker parameters public(package) fun send( self: &SendUln, param: &MessageLibSendParam, ): (address, ExecutorAssignJobParam, vector
, vector) { let packet = param.base().packet(); let sender = packet.sender(); let dst_eid = packet.dst_eid(); // Get effective executor configuration (OApp-specific or default) let executor_config = self.get_executor_config(sender, dst_eid); // Validate message size assert!(packet.message().length() <= executor_config.max_message_size(), EInvalidMessageSize); // Split options into executor and DVN options let (executor_options, dvn_options) = worker_options::split_worker_options(param.base().options()); // Create executor job parameters let executor_param = executor_assign_job::create_param( packet.guid(), packet.dst_eid(), sender, packet.message().length(), executor_options, ); // Get effective ULN configuration (OApp-specific or default) let uln_config = self.get_uln_config(sender, dst_eid); // Encode packet header for DVN verification let packet_header = packet_v1_codec::encode_packet_header(packet); let payload_hash = packet_v1_codec::payload_hash(packet); // Create DVN job parameters for each configured DVN let (dvns, dvn_params) = self.create_dvn_params( packet.guid(), packet.dst_eid(), sender, packet_header, payload_hash, uln_config, dvn_options, ); (executor_config.executor(), executor_param, dvns, dvn_params) } ``` #### Step 4: Workers Process Job Assignments **Executor Assignment**: ```rust wrap theme={null} /// From executor::assign_job() public fun assign_job( self: &Executor, call: &mut Call, ctx: &mut TxContext, ): Call { // Extract parameters let param = *call.param().base(); // Create child call to fee library for fee calculation self.create_feelib_get_fee_call(call, param, ctx) } /// Executor confirms fee calculation public fun confirm_assign_job( self: &Executor, executor_call: &mut Call, feelib_call: Call, ) { // Destroy fee library call and extract fee let (_, _, fee) = executor_call.destroy_child(self.worker.worker_cap(), feelib_call); // Complete executor call with FeeRecipient executor_call.complete( self.worker.worker_cap(), fee_recipient::create(fee, self.worker.deposit_address()) ); } ``` **DVN Assignment** (similar pattern): ```rust wrap theme={null} /// From dvn::assign_job() public fun assign_job( self: &DVN, call: &mut Call, ctx: &mut TxContext, ): Call { let param = *call.param().base(); self.create_feelib_get_fee_call(call, param, ctx) } ``` #### Step 5: ULN302 Confirms Send The ULN302 destroys worker `Call` objects and aggregates fees: ```rust wrap theme={null} /// From uln_302::confirm_send() public fun confirm_send( self: &Uln302, endpoint: &EndpointV2, treasury: &Treasury, messaging_channel: &mut MessagingChannel, endpoint_call: &mut Call, mut send_library_call: Call, executor_call: Call, dvn_multi_call: MultiCall, ctx: &mut TxContext, ) { send_library_call.assert_caller(endpoint!()); // Destroy DVN calls and collect fee recipients let (mut dvns, mut dvn_recipients) = (vector[], vector[]); dvn_multi_call.destroy(&self.call_cap).do!(|dvn_call| { let (dvn, _, dvn_recipient) = send_library_call.destroy_child(&self.call_cap, dvn_call); dvns.push_back(dvn); dvn_recipients.push_back(dvn_recipient); }); // Destroy executor call and collect fee recipient let (executor, _, executor_recipient) = send_library_call.destroy_child(&self.call_cap, executor_call); // Calculate total fees and encoded packet let send_result = send_uln::confirm_send( send_library_call.param(), executor, executor_recipient, dvns, dvn_recipients, treasury, ); send_library_call.complete(&self.call_cap, send_result); // Call endpoint for final confirmation let (native_token, zro_token) = endpoint.confirm_send( &self.call_cap, messaging_channel, endpoint_call, send_library_call, ctx, ); // Distribute fees to workers send_uln::handle_fees(treasury, executor_recipient, dvn_recipients, native_token, zro_token, ctx); } ``` **Inside `send_uln::confirm_send()`**: ```rust wrap theme={null} public(package) fun confirm_send( param: &MessageLibSendParam, executor: address, executor_recipient: FeeRecipient, dvns: vector
, dvn_recipients: vector, treasury: &Treasury, ): MessageLibSendResult { let packet = param.base().packet(); // Aggregate worker fees let mut native_recipients = vector[executor_recipient]; native_recipients.append(dvn_recipients); let total_native_fee = native_recipients.fold!(0, |acc, r| acc + r.fee()); // Calculate treasury fee let (treasury_recipient, zro_recipient) = treasury.quote_treasury_fee( packet.sender(), packet.dst_eid(), total_native_fee, param.base().pay_in_zro() ); if (treasury_recipient.fee() > 0) { native_recipients.push_back(treasury_recipient); }; let zro_recipients = if (zro_recipient.fee() > 0) { vector[zro_recipient] } else { vector[] }; // Encode packet for event emission let encoded_packet = packet_v1_codec::encode_packet(packet); // Emit events event::emit(ExecutorFeePaidEvent { guid: packet.guid(), executor, fee: executor_recipient }); event::emit(DVNFeePaidEvent { guid: packet.guid(), dvns, fees: dvn_recipients }); // Return result with fee recipients message_lib_send::create_result(encoded_packet, native_recipients, zro_recipients) } ``` #### Step 6: Endpoint Finalizes Send ```rust wrap theme={null} /// From endpoint_v2::confirm_send() public fun confirm_send( self: &EndpointV2, send_library: &CallCap, // Library's capability (static call) messaging_channel: &mut MessagingChannel, endpoint_call: &mut Call, send_library_call: Call, ctx: &mut TxContext, ): (Coin, Coin) { messaging_channel.assert_ownership(endpoint_call.caller()); // Destroy library call and extract results let (send_lib, param, result) = endpoint_call.destroy_child(&self.call_cap, send_library_call); assert!(send_lib == send_library.id(), EUnauthorizedSendLibrary); // Process fee payment and emit events let (receipt, paid_native_token, paid_zro_token) = messaging_channel.confirm_send( send_lib, endpoint_call.param_mut(&self.call_cap), param, result, ctx, ); // Complete the Call with MessagingReceipt endpoint_call.complete(&self.call_cap, receipt); // Return collected fees for distribution (paid_native_token, paid_zro_token) } ``` **Inside `messaging_channel.confirm_send()`**: ```rust wrap theme={null} public(package) fun confirm_send( self: &mut MessagingChannel, send_library: address, endpoint_param: &mut EndpointSendParam, message_lib_param: MessageLibSendParam, result: MessageLibSendResult, ctx: &mut TxContext, ): (MessagingReceipt, Coin, Coin) { // Extract fee recipients let (encoded_packet, native_recipients, zro_recipients) = result.destroy(); // Calculate total fees required let total_native_fee = native_recipients.fold!(0, |acc, r| acc + r.fee()); let total_zro_fee = zro_recipients.fold!(0, |acc, r| acc + r.fee()); // Split coins from endpoint_param let paid_native = coin::split(endpoint_param.native_fee_mut(), total_native_fee, ctx); let paid_zro = if (total_zro_fee > 0) { coin::split(endpoint_param.lz_token_fee_mut().borrow_mut(), total_zro_fee, ctx) } else { coin::zero(ctx) }; // Emit PacketSentEvent event::emit(PacketSentEvent { encoded_packet, options: *message_lib_param.base().options(), send_library, }); // Create receipt let packet = message_lib_param.base().packet(); let receipt = messaging_receipt::create(packet.guid(), packet.nonce(), total_native_fee, total_zro_fee); // Reset sending flag self.is_sending = false; (receipt, paid_native, paid_zro) } ``` #### Step 7: OApp Extracts Receipt The OApp confirms the send call to extract the receipt: ```rust wrap theme={null} /// From oapp::confirm_lz_send() public fun confirm_lz_send( self: &OApp, oapp_cap: &CallCap, call: Call, ): (SendParam, MessagingReceipt) { self.assert_oapp_cap(oapp_cap); // Destroy the Call and extract results let (endpoint, param, receipt) = call.destroy(oapp_cap); assert!(endpoint == endpoint!(), EOnlyEndpoint); (param, receipt) } ``` ### Example PTB for Send (from actual transaction) Based on transaction `HXZqH1RdANEkstz3MTFGMuLQ74CfgAkwQCq1YW8TMHHH`: ```javascript wrap theme={null} // PTB commands in order: 1. SplitCoins - Split fee from sender's SUI 2. MoveCall - bytes32::from_bytes (convert recipient to Bytes32) 3. MoveCall - send_param::create (create SendParam struct) 4. MoveCall - oft_sender::tx_sender (create OFTSender context) 5. SplitCoins - Split token amount from sender's coin 6. MoveCall - oft::send (initiate OFT send, returns Call) 7. MoveCall - endpoint::send (route Call to endpoint) 8. MoveCall - uln_302::send (create worker calls) 9. MoveCall - executor::assign_job (executor processes) 10. MoveCall - dvn::assign_job (each DVN processes) 11. MoveCall - executor::confirm_assign_job 12. MoveCall - dvn::confirm_assign_job (for each DVN) 13. MoveCall - uln_302::confirm_send 14. MoveCall - oft::confirm_send (extract receipt) // Events emitted: - ExecutorFeePaidEvent - DVNFeePaidEvent - PacketSentEvent - OFTSentEvent ``` ### Send Limitations #### Max Message Size The `maxMessageSize` is configured per executor and OApp: ```rust wrap theme={null} /// From ExecutorConfig struct public struct ExecutorConfig has copy, drop, store { executor: address, // Executor address max_message_size: u64, // Maximum message bytes (default varies by network) } ``` Default is typically 10,000 bytes, but OApps can configure custom limits. #### Fee Payment Model Unlike EVM's direct fee transfer, Sui uses `Coin` object splitting: ```rust wrap theme={null} // Split exact fee amount from provided coins let paid_native = coin::split(native_fee_coin, required_fee, ctx); // Transfer to fee recipient transfer::public_transfer(paid_native, recipient_address); // Refund excess transfer::public_transfer(remaining_coin, refund_address); ``` *** ## Verification Workflow After the `PacketSentEvent` is emitted on the source chain, DVNs independently verify the message on the destination chain. ### DVN Verification Process #### Step 1: DVN Monitors Source Chain DVNs watch for `PacketSentEvent` and wait for the configured number of block confirmations (finality). #### Step 2: DVN Submits Verification Each DVN calls the `verify()` function on the ULN302: ```rust wrap theme={null} /// From uln_302::verify() public fun verify( self: &Uln302, verification: &mut Verification, // Shared verification object call: Call, ) { let dvn = call.caller(); // DVN's CallCap proves identity let param = call.complete_and_destroy(&self.call_cap); // Store verification in the Verification shared object receive_uln::verify( verification, dvn, *param.packet_header(), param.payload_hash(), param.confirmations() ) } ``` **Inside `receive_uln::verify()`**: ```rust wrap theme={null} /// From receive_uln::verify() public(package) fun verify( self: &mut Verification, dvn: address, packet_header: vector, payload_hash: Bytes32, confirmations: u64, ) { // Create confirmation key let key = ConfirmationKey { header_hash: hash::keccak256!(&packet_header), payload_hash, dvn, }; // Store confirmations in Table table_ext::upsert!(&mut self.confirmations, key, confirmations); // Emit event for monitoring event::emit(PayloadVerifiedEvent { dvn, header: packet_header, confirmations, proof_hash: payload_hash, }); } ``` **Verification Storage**: ```rust wrap theme={null} /// Shared object storing all DVN confirmations public struct Verification has key { id: UID, // Maps (header_hash, payload_hash, dvn) → confirmation_count confirmations: Table, } ``` ### Commit Verification After sufficient DVNs have verified (meeting the X of Y of N threshold), anyone can call `commit_verification()`: ```rust wrap theme={null} /// From uln_302::commit_verification() public fun commit_verification( self: &Uln302, endpoint: &EndpointV2, verification: &mut Verification, messaging_channel: &mut MessagingChannel, packet_header: vector, payload_hash: Bytes32, clock: &Clock, ) { // Verify and reclaim storage from Verification object let header = self.receive_uln.verify_and_reclaim_storage( verification, endpoint.eid(), packet_header, payload_hash, ); // Call endpoint to insert verified payload hash endpoint.verify( &self.call_cap, // Library capability messaging_channel, // Destination OApp's channel header.src_eid(), // Source endpoint ID header.sender(), // Source OApp address header.nonce(), // Message nonce payload_hash, // Payload hash clock, // For timeout validation ); } ``` **Inside `receive_uln::verify_and_reclaim_storage()`**: ```rust wrap theme={null} public(package) fun verify_and_reclaim_storage( self: &ReceiveUln, verification: &mut Verification, local_eid: u32, encoded_packet_header: vector, payload_hash: Bytes32, ): PacketHeader { // Decode and validate packet header let header = packet_v1_codec::decode_header(encoded_packet_header); assert!(header.dst_eid() == local_eid, EInvalidEid); let header_hash = hash::keccak256!(&encoded_packet_header); let receiver = header.receiver(); let src_eid = header.src_eid(); // Get effective ULN configuration let uln_config = self.get_uln_config(receiver, src_eid); // Check all required DVNs have verified let mut verified_count = 0; uln_config.required_dvns().do!(|dvn| { let key = ConfirmationKey { header_hash, payload_hash, dvn: *dvn }; assert!(verification.confirmations.contains(key), EVerifying); // Remove confirmation (reclaim storage) verification.confirmations.remove(key); verified_count = verified_count + 1; }); // Check optional DVN threshold is met if (uln_config.optional_dvn_count() > 0) { let mut optional_verified = 0; uln_config.optional_dvns().do!(|dvn| { let key = ConfirmationKey { header_hash, payload_hash, dvn: *dvn }; if (verification.confirmations.contains(key)) { verification.confirmations.remove(key); optional_verified = optional_verified + 1; }; }); assert!(optional_verified >= uln_config.optional_dvn_threshold(), EVerifying); }; header } ``` ### Endpoint Verify The Endpoint inserts the verified payload hash into the messaging channel: ```rust wrap theme={null} /// From endpoint_v2::verify() public fun verify( self: &EndpointV2, receive_library: &CallCap, // Library's capability messaging_channel: &mut MessagingChannel, src_eid: u32, sender: Bytes32, nonce: u64, payload_hash: Bytes32, clock: &Clock, ) { // Validate receive library is authorized self.message_lib_manager.assert_receive_library( messaging_channel.oapp(), src_eid, receive_library.id(), clock ); // Insert payload hash into messaging channel messaging_channel.verify(src_eid, sender, nonce, payload_hash); } ``` **Inside `messaging_channel::verify()`**: ```rust wrap theme={null} public(package) fun verify( self: &mut MessagingChannel, src_eid: u32, sender: Bytes32, nonce: u64, payload_hash: Bytes32, ) { assert!(payload_hash != EMPTY_PAYLOAD_HASH, EInvalidPayloadHash); // Get or create channel for this pathway let channel_key = ChannelKey { remote_eid: src_eid, remote_oapp: sender }; if (!self.channels.contains(channel_key)) { self.init_channel(src_eid, sender); }; // Insert payload hash into the channel let channel = &mut self.channels[channel_key]; channel.inbound_payload_hashes.add(nonce, payload_hash); // Emit verification event event::emit(PacketVerifiedEvent { src_eid, sender, nonce, receiver: self.oapp, payload_hash, }); } ``` **Message State Transition**: ``` Send → PacketSentEvent emitted on source chain ↓ DVNs monitor and verify (off-chain) ↓ DVNs call verify() (onchain submission) ↓ Verification confirmations stored in Verification object ↓ commit_verification() checks threshold ↓ Payload hash inserted into MessagingChannel ↓ Message ready for execution ``` *** ## Receive Workflow After verification is committed, the Executor can deliver the message to the destination OApp. ### Executor Delivery The Executor initiates message delivery by constructing a PTB with all required objects. #### Step 1: Executor Queries OApp Metadata The Executor queries the OApp's execution metadata to determine which objects are needed: ```rust wrap theme={null} // OApp implements this to provide execution metadata public fun get_oapp_info(oapp: &OApp): vector { // Returns encoded OAppInfoV1 containing required Move calls } ``` #### Step 2: Executor Creates PTB Based on the transaction `9fqmkJYFQyQs6u1vVmMSuqhZyobpSW7P4i7MaNVzbSFg`, the PTB contains: ```javascript wrap theme={null} 1. MoveCall - bytes32::from_bytes (decode sender) 2. MoveCall - bytes32::from_bytes (decode receiver) 3. MoveCall - option::none> (no value transfer) 4. MoveCall - executor_worker::execute_lz_receive (entry point) 5. MoveCall - counter::lz_receive (OApp business logic) Objects passed: - Executor shared object (immutable reference) - Executor capability (owned object) - EndpointV2 shared object (immutable reference) - MessagingChannel shared object (mutable reference) - Clock object (for validation) - Counter OApp shared object (mutable reference) - Counter Peer object (immutable reference) ``` #### Step 3: Executor Calls execute\_lz\_receive ```rust wrap theme={null} /// From executor::execute_lz_receive() public fun execute_lz_receive( self: &Executor, endpoint: &EndpointV2, messaging_channel: &mut MessagingChannel, src_eid: u32, sender: Bytes32, nonce: u64, guid: Bytes32, message: vector, extra_data: vector, value: Option>, ctx: &mut TxContext, ): Call { // Create lz_receive call via endpoint endpoint.lz_receive( &self.worker.worker_cap(), // Executor's capability messaging_channel, src_eid, sender, nonce, guid, message, extra_data, value, ctx, ) } ``` #### Step 4: Endpoint Creates lz\_receive Call ```rust wrap theme={null} /// From endpoint_v2::lz_receive() public fun lz_receive( self: &EndpointV2, executor: &CallCap, // Executor's capability messaging_channel: &mut MessagingChannel, src_eid: u32, sender: Bytes32, nonce: u64, guid: Bytes32, message: vector, extra_data: vector, value: Option>, ctx: &mut TxContext, ): Call { // Clear the payload first (prevents reentrancy) messaging_channel.clear(src_eid, sender, nonce, guid, &message); // Create lz_receive parameters let lz_receive_param = lz_receive::create_param( src_eid, sender, nonce, guid, message, extra_data, value, ); // Create Call object targeting the OApp call::create( executor, // Executor creates the Call messaging_channel.oapp(), // Target: OApp address true, // One-way call (no result expected) lz_receive_param, ctx, ) } ``` **Inside `messaging_channel::clear()`**: ```rust wrap theme={null} public(package) fun clear( self: &mut MessagingChannel, src_eid: u32, sender: Bytes32, nonce: u64, guid: Bytes32, message: &vector, ) { let channel_key = ChannelKey { remote_eid: src_eid, remote_oapp: sender }; let channel = &mut self.channels[channel_key]; // Lazy nonce update: clear all messages up to this nonce if (nonce > channel.lazy_inbound_nonce) { let mut i = channel.lazy_inbound_nonce + 1; while (i <= nonce) { assert!(channel.inbound_payload_hashes.contains(i), EInvalidNonce); i = i + 1; }; channel.lazy_inbound_nonce = nonce; }; // Verify payload hash matches verified hash let expected_hash = channel.inbound_payload_hashes[nonce]; let actual_hash = hash::keccak256!(&vector[guid.data(), *message]); assert!(expected_hash == actual_hash, EPayloadHashNotFound); // Remove from storage (prevents double execution) channel.inbound_payload_hashes.remove(nonce); // Emit delivery event event::emit(PacketDeliveredEvent { src_eid, sender, receiver: self.oapp, nonce, }); } ``` **Key Security Features**: 1. **Lazy nonce validation**: Ensures all prior messages have been verified 2. **Payload hash verification**: Confirms executor provided the correct message 3. **Storage cleanup**: Removes hash to prevent double execution 4. **Event emission**: Signals successful delivery #### Step 5: OApp Processes Message The OApp's `lz_receive()` function is invoked via the `Call` object: ```rust wrap theme={null} /// Example from counter OApp public fun lz_receive( self: &mut Counter, peer: &Peer, call: Call, ) { // Validate Call came from Endpoint let (callee, param, _) = call.complete_and_destroy(&self.call_cap); assert!(callee == endpoint_address(), EOnlyEndpoint); // Validate sender is configured peer assert!(param.sender() == peer.address, EOnlyPeer); // Process message (application-specific logic) self.count = self.count + 1; // Note: No need to manually call clear() - already done by Endpoint } ``` **OApp Responsibilities**: * * Validate `Call` came from authorized Endpoint * * Validate sender matches configured peer * * Process message and update state * * No need to call `clear()` (done by Endpoint before Call creation) ### Example PTB for Receive (from actual transaction) Based on transaction `9fqmkJYFQyQs6u1vVmMSuqhZyobpSW7P4i7MaNVzbSFg`: ```javascript wrap theme={null} // PTB commands in order: 1. MoveCall - bytes32::from_bytes (decode sender parameter) 2. MoveCall - bytes32::from_bytes (decode guid parameter) 3. MoveCall - option::none> (no native token transfer) 4. MoveCall - executor_worker::execute_lz_receive - Passes: Executor, Endpoint, MessagingChannel, src_eid, sender, nonce, guid, message - Returns: Call 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 Sui-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 | Sui | | ------------------------ | --------------------- | ------------------------------ | --------------------------------- | | **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>, ``` **Sui**: ```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); ``` **Sui**: ```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 | Sui | | --------------------- | ---------------------- | ------------------------ | ------------------------------ | | **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 | Sui | | ------------------ | --------------------- | ----------------------- | ------------------------- | | **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 | Sui | | ------------------- | ----------------------------------- | ---------------------------------- | ------------------------------------ | | **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 Sui'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::sui::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}); ``` *** ## Sui-Specific Considerations ### Object Abilities Sui's [ability system](https://move-book.com/advanced-topics/abilities.html) 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 Sui 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 Sui uses [`Table`](https://docs.sui.io/references/framework/sui-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 Sui 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/sui/oapp/overview) - Build custom crosschain applications * [OFT Implementation](/v2/developers/sui/oft/overview) - Deploy crosschain tokens * [OFT SDK](/v2/developers/sui/oft/sdk) - Complete SDK methods and patterns * [Configuration Guide](/v2/developers/sui/configuration/dvn-executor-config) - DVN, executor, and gas configuration * [Technical Overview](/v2/developers/sui/technical-overview) - Sui fundamentals and architecture # Sui Fundamentals for LayerZero Developers Source: https://docs.layerzero.network/v2/developers/sui/technical-overview Overview of Sui Fundamentals for Developers on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure... This page introduces the Sui-specific concepts you need to understand before building LayerZero applications. If you're coming from EVM or Solana, this guide explains how Sui differs and why LayerZero's implementation works the way it does. **What you'll learn**: * Sui'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/sui/protocol-overview). For hands-on implementation, see [OApp](/v2/developers/sui/oapp/overview) or [OFT](/v2/developers/sui/oft/overview) guides. ## VM Architecture Sui uses the Move programming language and employs an [object-based model](https://docs.sui.io/concepts/object-model) rather than the account-based model used by EVM chains. This fundamental difference requires different patterns for implementing crosschain functionality. ### Sui Object Model Sui organizes state into [**objects**](https://docs.sui.io/concepts/object-model) with different [ownership types](https://docs.sui.io/concepts/object-ownership). For an introduction to Sui's object model, see [Getting Started](/v2/developers/sui/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.sui.io/concepts/object-model)): Globally unique identifier * [**Abilities**](https://move-book.com/advanced-topics/abilities.html): 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`, **Sui 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." To achieve dynamic routing, LayerZero uses the **Call pattern**—a capability-based hot potato implementation. 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) Sui's execution model centers around [**Programmable Transaction Blocks**](https://docs.sui.io/concepts/transactions/prog-txn-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 Sui 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/sui/protocol-overview). ## Transaction Execution Model Sui 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/sui/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 Sui 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.sui.io/concepts/object-ownership/shared): Created with `transfer::share_object()`, accessible to all transactions * [**Owned objects**](https://docs.sui.io/concepts/object-ownership/address-owned): Created with `transfer::transfer()`, belong to specific addresses * **Embedded structs**: Fields within objects (e.g., `Peer`, `EnforcedOptions`) * [**Tables**](https://docs.sui.io/references/framework/sui-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 Sui'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, Sui functions require capability objects 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 Sui peer addresses are package IDs, not object IDs. ### Why This Matters for Configuration When you deploy an OApp/OFT and configure peers: **On Sui side**: ```typescript wrap theme={null} import {SDK} from '@layerzerolabs/lz-sui-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 Sui PACKAGE ID as peer myOApp.setPeer( 30378, // Sui mainnet EID bytes32(0x061a47bf...) // Your Sui PACKAGE ID ); ``` **What happens when message arrives**: 1. Remote chain sends to your package ID 2. Sui 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 Sui'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: * [Sui Gas Pricing](https://docs.sui.io/concepts/tokenomics/gas-pricing) * [Sui Gas in Sui](https://docs.sui.io/concepts/tokenomics/gas-in-sui) ## Key Sui Concepts for LayerZero Sui provides system-level objects and features that LayerZero leverages for crosschain messaging. ### Clock Object The [Clock](https://docs.sui.io/references/framework/sui-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 Sui [events](https://docs.sui.io/guides/developer/sui-101/using-events) are emitted and indexed for off-chain monitoring: ```rust wrap theme={null} use sui::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**: Sui 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/sui/oapp/overview) - Build custom crosschain applications * [OFT Implementation Guide](/v2/developers/sui/oft/overview) - Deploy crosschain tokens * [OFT SDK](/v2/developers/sui/oft/sdk) - TypeScript SDK methods and patterns * [Configuration Guide](/v2/developers/sui/configuration/dvn-executor-config) - DVN and executor configuration * [Protocol Overview](/v2/developers/sui/protocol-overview) - Complete protocol workflows * [Sui Development Guidance](/v2/developers/sui/technical-reference/sui-guidance) - Best practices # Sui Development Guidance Source: https://docs.layerzero.network/v2/developers/sui/technical-reference/sui-guidance Technical reference for Sui Development Guidance. Complete API documentation with functions, parameters, and usage examples. LayerZero enables secure... This page provides development guidance for building LayerZero applications on Sui, covering toolchain setup, operational practices, and technical constraints. ## Toolchain Setup ### Sui CLI **Tested Version**: `sui@v1.54.1` Install the [Sui CLI](https://docs.sui.io/references/cli): ```bash wrap theme={null} cargo install --locked --git https://github.com/MystenLabs/sui.git --branch mainnet sui ``` Verify installation: ```bash wrap theme={null} sui --version # sui 1.54.1-... ``` ### Project Structure Typical Sui 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.sui.io/concepts/sui-move-concepts/packages) details: ```toml wrap theme={null} [package] name = "my_oapp" version = "0.0.1" [dependencies] Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "mainnet" } LayerZeroEndpoint = { git = "https://github.com/LayerZero-Labs/LayerZero-v2.git", subdir = "packages/layerzero-v2/sui/contracts/endpoint-v2", rev = "main" } LayerZeroOApp = { git = "https://github.com/LayerZero-Labs/LayerZero-v2.git", subdir = "packages/layerzero-v2/sui/contracts/oapps/oapp", rev = "main" } [addresses] my_oapp = "0x0" sui = "0x2" ``` ## Package Verification Sui supports package verification via [SuiScan](https://suiscan.xyz): ### Verification Methods 1. **Source Upload**: Upload source code directly to SuiScan 2. **API Verification**: Use SuiScan's API for programmatic verification ### Verification via SuiScan 1. Navigate to [https://suiscan.xyz/mainnet/package-verification](https://suiscan.xyz/mainnet/package-verification) 2. Enter your package address 3. Upload source files 4. Wait for verification ### Verification via API ```bash wrap theme={null} curl -X POST https://suiscan.xyz/api/verify \ -H "Content-Type: application/json" \ -d '{ "packageId": "0x...", "sourceCode": "...", "network": "mainnet" }' ``` ## Development Environment ### Building Contracts ```bash wrap theme={null} sui move build ``` ### Running Tests ```bash wrap theme={null} sui move test ``` ### Local Development Start a local Sui network: ```bash wrap theme={null} sui start ``` ## Deployment ### Deploying to Testnet ```bash wrap theme={null} sui client publish \ --gas-budget 100000000 \ --json ``` ### Deploying to Mainnet ```bash wrap theme={null} sui client switch --env mainnet sui client publish \ --gas-budget 100000000 \ --json ``` ### Deployment Script Example ```bash wrap theme={null} #!/bin/bash # Build echo "Building..." sui move build # Deploy echo "Deploying..." RESULT=$(sui 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 Sui packages are immutable by default but can be made [upgradeable](https://docs.sui.io/concepts/sui-move-concepts/packages/upgrade). **[`UpgradeCap`](https://docs.sui.io/concepts/sui-move-concepts/packages/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} sui client transfer \ --to \ --object-id \ --gas-budget 10000000 ``` **Upgrade a Package**: ```bash wrap theme={null} sui client upgrade \ --upgrade-capability \ --gas-budget 200000000 ``` **Key Point**: Upgraded packages maintain compatibility with objects created by previous versions, provided you follow Sui'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**: Sui 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. **Sui 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**: Sui Wallet multisig, protocol-specific multisig **Example using address derivation**: ```bash wrap theme={null} # Create multisig address with multiple public keys sui keygen multi-sig \ --pks \ --weights 1 1 1 \ --threshold 2 ``` ## Resource & Fee Models See [Sui Gas Model](https://docs.sui.io/concepts/tokenomics/gas-in-sui) 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 [Sui Transaction Limits](https://move-book.com/guides/building-against-limits/): * Max objects per transaction: 256 * Max events per transaction: 1024 * Max argument size: 128 KB ### Compute Limits Gas limits vary by network: * **Testnet**: Lower limits * **Mainnet**: Higher limits For LayerZero operations, budget at least: * **Simple send**: 5,000,000 gas * **Complex send**: 20,000,000 gas * **Receive**: 10,000,000 gas ### Network Resource Limits Monitor these limits: * **Object count per address**: Unlimited, but impacts query performance * **Storage per address**: Unlimited, but costs scale linearly * **Transaction throughput**: \~5,000 TPS (network-wide) ## Network Considerations ### Finality Sui 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.sui.io:443` * Testnet: `https://fullnode.testnet.sui.io:443` * Devnet: `https://fullnode.devnet.sui.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 Sui CLI has limitations for querying state. Use TypeScript SDKs instead: ```typescript wrap theme={null} import {SuiClient} from '@mysten/sui.js/client'; import {OApp} from '@layerzerolabs/lz-sui-sdk-v2'; const client = new SuiClient({url: 'https://fullnode.mainnet.sui.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); ``` ### Sui CLI Limitations The Sui 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 sui move test # Run integration tests on testnet sui client call --package $PKG ... --json ``` ### 2. Monitor Gas Usage ```bash wrap theme={null} # Use --gas-budget appropriately sui 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 * [Sui Documentation](https://docs.sui.io/) * [Sui Move Book](https://move-book.com/) * [Sui GitHub](https://github.com/MystenLabs/sui) * [LayerZero Sui Contracts](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/sui) ## Next Steps * [OApp Overview](/v2/developers/sui/oapp/overview) * [OFT Overview](/v2/developers/sui/oft/overview) * [Configuration Guide](/v2/developers/sui/configuration/dvn-executor-config) * [Troubleshooting](/v2/developers/sui/troubleshooting/common-errors) # Common Errors Source: https://docs.layerzero.network/v2/developers/sui/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 Sui, along with their causes and solutions. ## Deployment Issues ### Git Dependencies Failed **Error Message**: ``` Error: Package dependency does not specify published address Error: Failed to resolve dependencies ``` **Cause**: Git dependencies for LayerZero packages don't work due to missing Move.toml manifests in subdirectories. **Solution**: Use local dependencies instead: ```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/sui/contracts/oapps/oapp" } EndpointV2 = { local = "../LayerZero-v2/packages/layerzero-v2/sui/contracts/endpoint-v2" } # ... other packages ``` Or use published package addresses (see [Deployed Contracts](/v2/deployments/chains/sui)). ### 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} sui client publish --with-unpublished-dependencies --gas-budget 200000000 ``` ### Package Size Exceeded **Error Message**: ``` Error: Package size (260 KB) exceeds maximum allowed size (250 KB) ``` **Cause**: Your package exceeds Sui's 250 KiB limit per package object. See [Sui transaction limits](https://move-book.com/guides/building-against-limits.html). **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} sui client publish --gas-budget 200000000 ``` Deployment typically requires: * Simple packages: 50-100M gas * Complex packages: 100-200M gas * With dependencies: 200M+ gas ### Upgrade Authority Issues **Error Message**: ``` Error: UpgradeCap not found or not owned by signer ``` **Cause**: The signer doesn't own the `UpgradeCap` for the package. **Solution**: 1. Verify you're using the correct account 2. Check UpgradeCap ownership: ```bash wrap theme={null} sui 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} sui client call \ --package $PACKAGE \ --module oapp \ --function initialize_channel \ --args $OAPP_OBJECT $ADMIN_CAP 30101 \ --gas-budget 10000000 ``` ### Peer Not Set **Error Message**: ``` Error: Peer address not configured for endpoint ID 30101 ``` **Cause**: No peer OApp address configured for the destination chain. **Solution**: Set peer address: ```bash wrap theme={null} sui client call \ --package $PACKAGE \ --module oapp \ --function set_peer \ --args $OAPP_OBJECT $ADMIN_CAP 30101 $PEER_ADDRESS_BYTES \ --gas-budget 10000000 ``` **Address Format**: Ensure peer address is 32 bytes (pad EVM addresses). ### Library Configuration Missing **Error Message**: ``` Error: Send library not configured for endpoint ID 30101 ``` **Cause**: Custom library set but not properly configured. **Solution**: Either: 1. Use default libraries (don't set custom) 2. Or properly configure custom library: ```bash wrap theme={null} sui client call \ --package $PACKAGE \ --module oapp \ --function set_send_library \ --args $OAPP_OBJECT $ADMIN_CAP 30101 $LIBRARY_ADDRESS \ --gas-budget 10000000 ``` ## Configuration Errors ### Peer Address Error (oapp\_registry abort) **Error Message**: ``` Error: oapp_registry::get_messaging_channel abort code: 1 Error: MoveAbort in module oapp_registry ``` **Cause**: Remote chain is using wrong receiver address - likely using **object ID** instead of **package ID**. **Solution**: On Sui, peer addresses must be **package IDs**, not object IDs. **Find Your Correct Package ID**: ```bash wrap theme={null} # Query your OApp/OFT object sui 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 Sui package ID: await oapp.setPeer( 30230, // Sui mainnet EID '0x061a47bffa630b8cd3735f8479edf7ab7897863fb3b796e77ebb8786af6f1bfc', // Package ID ); ``` **Why Package ID?** * Sui 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 sui client publish --json # objectChanges array shows: # - "published" type = package ID # - "created" type = object IDs ``` ### Invalid BCS Bytes Error **Error Message**: ``` Error: InvalidBCSBytes Error: Unable to deserialize config Error: Failed to deserialize argument at index 6 ``` **Cause**: Using `tx.pure()` instead of SDK's `asBytes()` helper for byte array parameters. **Solution**: Use the SDK's `asBytes()` helper: ```typescript wrap theme={null} import {SDK, OAppUlnConfigBcs} from '@layerzerolabs/lz-sui-sdk-v2'; // CRITICAL: Import asBytes helper const {asBytes} = await import('@layerzerolabs/lz-sui-sdk-v2'); // Encode configuration const config = OAppUlnConfigBcs.serialize({ use_default_confirmations: false, use_default_required_dvns: false, use_default_optional_dvns: true, uln_config: { confirmations: 15, required_dvns: [dvnAddress], optional_dvns: [], optional_dvn_threshold: 0, }, }).toBytes(); const tx = new Transaction(); // - WRONG: Using tx.pure() causes InvalidBCSBytes tx.pure(config, 'vector'); // - 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('0xfbece0b75d097c31b9963402a66e49074b0d3a2a64dd0ed666187ca6911a4d12'), // OFTComposerManager 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-sui-sdk-v2'; const tx = new Transaction(); tx.moveCall({ target: `${oappPackage}::endpoint_calls::set_oapp_info`, arguments: [ tx.object(oappObjectId), tx.object(adminCapId), tx.object(endpointObjectId), asBytes(tx, oappInfoBytes), // Includes lz_receive_info ], }); await client.signAndExecuteTransaction({transaction: tx}); ``` **Prevention**: Always provide `lz_receive_info` during initial OFT registration (see [OFT Overview](/v2/developers/sui/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 Sui, peer addresses must be **package IDs**: ```bash wrap theme={null} # Find your package ID sui 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**: Sui uses Package CallCaps where `callCap.id()` returns the package address. ## Runtime Errors ### Call Object Not Consumed **Error Message**: ``` Error: unused value without 'drop' ability Error: unused value of type 'call::call::Call<...>' ``` **Cause**: A `Call` object was not properly consumed before the transaction ended. **Root Causes**: 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 sui client call ... --json --dry-run ``` ## Transaction Issues ### PTB Construction Failures **Error Message**: ``` Error: Invalid PTB: missing required call ``` **Cause**: Programmable Transaction Block doesn't include all required calls. **Solution**: Verify PTB structure: ```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} sui 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.sui.io/concepts/object-ownership/address-owned) (`AdminCap`, `CallCap`): Must be owned by signer * [**Shared objects**](https://docs.sui.io/concepts/object-ownership/shared) (`OApp`, `EndpointV2`): Accessible by anyone, use references (`&` or `&mut`) * [**Immutable objects**](https://docs.sui.io/concepts/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 SuiClient({ url: 'https://fullnode.mainnet.sui.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::sui::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} # Sui CLI with verbose output sui 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 sui client object $OBJECT_ID --json # View all objects for an address sui client objects --json ``` ### Use Sui Explorer Navigate to [SuiScan](https://suiscan.xyz/) 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 sui client switch --env devnet # Test your calls sui client call ... --gas-budget 20000000 ``` ## Getting Help If you continue to experience issues: 1. **Check Documentation**: Review [Sui Documentation](https://docs.sui.io/) 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/sui/troubleshooting/faq) * [Sui Guidance](/v2/developers/sui/technical-reference/sui-guidance) * [Configuration Guide](/v2/developers/sui/configuration/dvn-executor-config) * [Technical Overview](/v2/developers/sui/technical-overview) # Sui FAQ Source: https://docs.layerzero.network/v2/developers/sui/troubleshooting/faq Common issues and solutions for Sui FAQ. Troubleshoot problems and find answers to frequently asked questions. LayerZero enables secure crosschain messaging. Frequently asked questions about developing LayerZero applications on Sui. ## General Questions Sui 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 Sui's architecture, see the [Sui documentation on PTBs](https://docs.sui.io/concepts/transactions/prog-txn-blocks). The key difference is that Sui uses `Call` objects and PTBs instead of `delegatecall`. In EVM, the relayer calls `Endpoint.lzReceive()` which delegates to the OApp. In Sui, 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/sui/technical-overview) and [Protocol Overview](/v2/developers/sui/protocol-overview). ## Development Questions Use [SuiScan](https://suiscan.xyz/mainnet/package-verification): **Method 1 - Web Interface**: 1. Navigate to SuiScan verification page 2. Enter package address 3. Upload source files 4. Wait for verification **Method 2 - API**: ```bash wrap theme={null} curl -X POST https://suiscan.xyz/api/verify \ -d '{"packageId": "0x...", "source": "..."}' ``` See [Sui Guidance](/v2/developers/sui/technical-reference/sui-guidance#contract-verification) for details. No. LayerZero deploys and maintains the `EndpointV2` shared object on Sui. 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/sui/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 Sui 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/sui/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 Sui CLI cannot easily parse complex return values. Yes, if you retain the `AdminCap`: ```bash wrap theme={null} # Update peer sui client call \ --function set_peer \ --args $OAPP $ADMIN_CAP $NEW_EID $NEW_PEER \ ... # Update DVNs sui 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-sui-sdk-v2](https://www.npmjs.com/package/@layerzerolabs/lz-sui-sdk-v2)** * Core Endpoint interactions * OApp functionality * Configuration management 2. **[@layerzerolabs/lz-sui-oft-sdk-v2](https://www.npmjs.com/package/@layerzerolabs/lz-sui-oft-sdk-v2)** * OFT-specific operations * Token transfers * Balance queries See [OFT SDK](/v2/developers/sui/oft/sdk) for usage examples. The Sui 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 {SuiClient} from '@mysten/sui.js/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-sui-sdk-v2'; const peer = await oapp.getPeer(client, remoteEid); ``` Not currently. Package publication and configuration require: 1. **Publish packages**: Using `sui client publish` 2. **Call entry functions**: Invoke configuration functions via `sui client call` or SDK 3. **Custom scripts**: Write TypeScript scripts for automated workflows See [Configuration Guide](/v2/developers/sui/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 sui client call \ --package \ --module endpoint_v2 \ --function init_channel \ --args \ --gas-budget 10000000 ``` The Endpoint creates a dedicated `MessagingChannel` shared object for each OApp. Use recovery entry functions on the Endpoint (requires `AdminCap`): **Skip a message** (increment nonce without execution): ```bash wrap theme={null} sui client call \ --package \ --module endpoint_v2 \ --function skip \ --args \ --gas-budget 10000000 ``` **Clear a message** (mark as delivered without execution): ```bash wrap theme={null} sui client call \ --function clear \ --args \ --gas-budget 10000000 ``` See [Common Errors](/v2/developers/sui/troubleshooting/common-errors) for more recovery options. ## Security Questions Follow these capability-based security practices: 1. **Validate CallCap in All Functions**: ```rust wrap theme={null} public fun send( self: &OApp, oapp_cap: &CallCap, // - Require capability ... ) { self.assert_oapp_cap(oapp_cap); // - Validate ownership // ... } fun assert_oapp_cap(self: &OApp, cap: &CallCap) { assert!(self.oapp_cap.id() == cap.id(), EInvalidOAppCap); } ``` 2. **Validate Call Objects**: ```rust wrap theme={null} public fun lz_receive(self: &mut OApp, call: Call) { // - Validate Call came from authorized Endpoint let (callee, param, _) = call.destroy(&self.oapp_cap); assert!(callee == endpoint_address(), EOnlyEndpoint); // - Validate sender is configured peer let peer = self.peer.get_peer(param.src_eid); assert!(param.sender == peer, EOnlyPeer); } ``` 3. **Secure Capability Objects**: * Store `CallCap` in package module storage (not transferred) * Use multisig or hardware wallet for `AdminCap` * Never expose capabilities publicly * Transfer `AdminCap` carefully (use `transfer::public_transfer`) 4. **Protect UpgradeCap**: * Keep upgrade authority secure * Consider freezing upgrades after deployment (`package::make_immutable`) * Use multisig for mainnet upgrade authority * * Missing `CallCap` validation in functions * * Not validating `Call` object source (callee address) * * Skipping peer validation in `lz_receive` * * Losing capability objects (no recovery possible) * * Wrong peer addresses configured * * Exposing `AdminCap` or `CallCap` publicly See [OApp Best Practices](/v2/developers/sui/oapp/overview#best-practices) for details. ## Next Steps * [Common Errors](/v2/developers/sui/troubleshooting/common-errors) * [Technical Overview](/v2/developers/sui/technical-overview) * [Configuration Guide](/v2/developers/sui/configuration/dvn-executor-config) * [Sui Guidance](/v2/developers/sui/technical-reference/sui-guidance) # Support OFTs and OApps on Tempo Source: https://docs.layerzero.network/v2/developers/tempo/how-to/support-ofts-and-oapps How to adapt OFT and OApp deployments for Tempo: fee payment with LZD, OFTAlt contracts, and the TempoOFTWrapper flow. ## Does my existing OFT/OApp work on Tempo? Yes. Source-chain contracts remain unchanged, **but** the Tempo side requires Alt contract variants and pays fees in LZD instead of `msg.value`. The message format, pathway configuration, and security model are the same. Source-chain contracts may need updated `enforcedOptions` to account for Tempo's higher gas costs. See [what changes on Tempo](#what-changes-on-tempo) below. ## What changes on Tempo See the [overview](/v2/developers/tempo/overview) for how each standard LayerZero component maps to its Tempo equivalent. The key changes relevant to OFT/OApp integrations: * **OFT/OApp contracts** are replaced by their Alt variants: [OFTAlt, OFTAdapterAlt](/v2/developers/tempo/reference/oft-vs-oftalt#choosing-the-right-contract), [OFTBurnSelfMintAlt](/v2/developers/tempo/reference/tip-20-token-standard#integration-with-oftalt), or [OAppAlt](/v2/developers/evm/evm-variants/evm-compatible-variants) depending on your use case * **Fees** are paid in [LZD](/v2/developers/tempo/reference/lz-endpoint-dollar) instead of `msg.value` (this is because Tempo has no native gas token). You must wrap a stablecoin into LZD and approve the OFT before calling `send{value: 0}()` * **Native drop** is not supported (`addExecutorNativeDropOption` reverts) * **Gas costs are higher** on Tempo for certain operations (state creation, new accounts). Set custom `enforcedOptions` with higher gas limits for `lzReceive` when Tempo is the destination. See [TIP-1010](https://docs.tempo.xyz/protocol/tips/tip-1010) for the full gas schedule ## Sending from other chains to Tempo When sending **to** Tempo from another chain, the normal fee model applies on the source chain: * Pay fees in the source chain's native token (ETH, MATIC, etc.) as usual * No LZD is needed on the source chain * The receiver on Tempo does not pay anything to receive the message No changes are required to your existing OFT/OApp contracts on the source chain. ## Sending from Tempo to other chains When sending **from** Tempo, you pay the LayerZero fee in LZD. There are two approaches: ### Direct flow (1 view call + 5 transactions) ```solidity wrap theme={null} // 1. Quote the fee (view call) MessagingFee memory fee = oft.quoteSend(sendParam, false); // 2. Approve the asset token to the OFT (bridge amount) IERC20(usdce).approve(address(oft), sendParam.amountLD); // 3. Approve the fee token to LZD (fee amount) IERC20(usdce).approve(address(lzd), fee.nativeFee); // 4. Wrap stablecoin into LZD (token must be whitelisted by LZD) lzd.wrap(usdce, msg.sender, fee.nativeFee); // 5. Approve LZD to the OFT (fee amount) IERC20(lzd).approve(address(oft), fee.nativeFee); // 6. Send with msg.value = 0 oft.send{value: 0}(sendParam, fee, refundAddress); ``` ### Wrapper flow (1 view call + 2 transactions) The TempoOFTWrapper simplifies this by handling wrapping, approvals, and sending in a single transaction: ```solidity wrap theme={null} // 1. Quote the fee MessagingFee memory fee = oft.quoteSend(sendParam, false); // 2. Approve the fee token to the wrapper (bridge amount + messaging fee) IERC20(usdce).approve(address(wrapper), sendParam.amountLD + fee.nativeFee); // 3. Call sendOFT: wrapper handles wrap, approve, and send atomically wrapper.sendOFT( address(oft), // OFT contract usdce, // fee token (whitelisted stablecoin) sendParam, // standard send parameters fee.nativeFee // max acceptable fee (reverts if re-quote exceeds this) ); ``` The wrapper: 1. Pulls tokens from the caller: * **Same token** (e.g., USDC.e for both bridging and fees): pulls `amountLD + nativeFee` in a single transfer * **Different tokens** (e.g., bridging EURC.e, fees in USDC.e): pulls `amountLD` and `nativeFee` separately, requiring two approvals 2. Wraps the fee portion into LZD 3. Approves LZD to the OFT 4. Calls `oft.send()` with the correct fee 5. Reverts the entire transaction if the re-quoted fee exceeds `maxNativeFee` You can preflight with an `eth_call` to `sendOFT` to obtain `oftReceipt.amountSentLD` before sending the live transaction, and avoid dust. If the bridged token differs from the fee token (e.g., bridging EURC.e but paying fees in USDC.e), you will need to approve the asset token and fee token to the wrapper separately before calling `sendOFT`. ### With LZMulticall (frontend pattern) The Stargate frontend uses LZMulticall to bundle wrapping, approvals, and sending into a single transaction. On Tempo, it bundles the LZD wrap and approval steps alongside the OFT send. | Contract | Address | | ---------------- | -------------------------------------------- | | LZMulticall | `0x4683ce822272cd66cea73f5f1f9f5cbcaef4f066` | | TransferDelegate | `0x3c18440268a78d651a3847653692fc82c31731c9` | | TempoOFTWrapper | `0xbb95daf376cd63f258d7c37a4efe57c10055e8e0` | | LZEndpointDollar | `0x0ceb237e109ee22374a567c6b09f373c73fa4cbb` | ## Common pitfalls * **LZD is not available on DEXes.** LZD is an ERC-20, not a TIP-20 token, so it cannot be traded on the Tempo DEX. Always use `LZD.wrap()` to obtain fee tokens. * **Wrapper becomes `msg.sender`.** When using the TempoOFTWrapper, the wrapper is the sender for the OFT call. Do not use it for compose messages where the composer refunds the original sender. The refund goes to the wrapper, not the user, and the funds will be permanently lost. * **Fee token must be a whitelisted stablecoin, not LZD.** The `feeToken` passed to the wrapper must be a 6-decimal stablecoin whitelisted by LZD (pathUSD, USDC.e, or USDT0). Passing LZD itself reverts. See the [LZD reference failure modes table](/v2/developers/tempo/reference/lz-endpoint-dollar#failure-modes) for a complete list of revert reasons. # LayerZero on Tempo Source: https://docs.layerzero.network/v2/developers/tempo/overview Overview of LayerZero on Tempo, an EVM-compatible chain with no native gas token, using LZEndpointDollar for all messaging fees. Tempo is an EVM-compatible chain with **no native gas token**. All transaction fees are paid in USD stablecoins. LayerZero deploys [EndpointV2Alt](/v2/concepts/protocol/layerzero-endpoint-alt) on Tempo instead of the standard `EndpointV2`, with [LZEndpointDollar (LZD)](/v2/developers/tempo/reference/lz-endpoint-dollar) as the fee token. LayerZero fees are paid through ERC-20 transfers instead of `msg.value`. #### What changes on Tempo | Standard EVM | Tempo | What changes | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | EndpointV2 | [EndpointV2Alt](/v2/concepts/protocol/layerzero-endpoint-alt) | accepts ERC-20 fee payments instead of native value | | Native token fees | [LZD](/v2/developers/tempo/reference/lz-endpoint-dollar) | canonical USD fee token wrapping whitelisted stablecoins | | OApp | [OAppAlt](/v2/developers/tempo/reference/oft-vs-oftalt) | OApp variant for chains without a native gas token; fees are paid in LZD instead of `msg.value` | | OFT / OFTAdapter | [OFTAlt](/v2/developers/tempo/reference/oft-vs-oftalt) / [OFTAdapterAlt](/v2/developers/tempo/reference/oft-vs-oftalt#choosing-the-right-contract) / [OFTBurnSelfMintAlt](/v2/developers/tempo/reference/tip-20-token-standard#integration-with-oftalt) | OFT variants for chains without a native gas token; use `OFTBurnSelfMintAlt` for TIP-20 tokens | | ERC-20 | ERC-20 and [TIP-20](/v2/developers/tempo/reference/tip-20-token-standard) | Tempo's native token standard with role-based administration | **Developing on Tempo**: Tempo's EVM has higher opcode costs than standard EVMs, and fork testing requires [Tempo's Foundry fork](https://docs.tempo.xyz/sdk/foundry). See the [Tempo developer docs](https://docs.tempo.xyz) for more info. Adapt your OFT and OApp deployments for Tempo: fee payment, OFTAlt contracts, and the wrapper flow. API reference for the canonical fee token: wrapping, unwrapping, whitelisting, and contract addresses. Why Tempo needs OFTAlt and OAppAlt, how the ERC-20 fee path works, and comparison with standard contracts. How Tempo's native token standard works with LayerZero: roles, ERC-20 compatibility, and OFTAlt integration. Consumer-side UX differences when using Stargate on Tempo: approvals, supported tokens, and gas estimation.
Tempo uses EndpointV2Alt, a variant of the LayerZero Endpoint designed for chains without a native gas token. See the [EndpointV2Alt concept page](/v2/concepts/protocol/layerzero-endpoint-alt) for protocol-level details. For questions, join the [LayerZero Discord](https://discord.com/invite/ktbvm8Nkcr). # LZEndpointDollar (LZD) Source: https://docs.layerzero.network/v2/developers/tempo/reference/lz-endpoint-dollar Reference for LZEndpointDollar, the canonical USD-denominated fee token used by LayerZero on Tempo. ## What is LZD LZEndpointDollar (LZD) is the canonical USD-denominated fee token for LayerZero on Tempo. Because Tempo has no native gas token, `EndpointV2Alt` uses LZD as the ERC-20 token for all LayerZero messaging fees. The endpoint could accept a single TIP-20 token directly, but that would force users to hold one specific stablecoin. LZD is an ERC-20 wrapper around multiple whitelisted 6-decimal stablecoins (pathUSD, USDC.e, or USDT0), so users can pay fees with whichever USD stablecoin they have. Users wrap a supported stablecoin into LZD, then approve the endpoint or OFT to spend it. ## Endpoint mode detection On any EVM chain, you can check the endpoint type by calling `nativeToken()`. On Tempo, it returns the LZD address: ```solidity wrap theme={null} address feeToken = ILayerZeroEndpointV2(endpoint).nativeToken(); if (feeToken == address(0)) { // Standard Endpoint: pay fees with native token (msg.value) } else { // Endpoint Alt: pay fees with ERC20 at feeToken address // On Tempo, feeToken = LZD address } ``` ## Fee payment flow Paying LayerZero fees on Tempo requires wrapping a whitelisted stablecoin into LZD before sending: 1. **Quote**: call `quoteSend(sendParam, _payInLzToken)` to receive both `nativeFee` and `lzTokenFee`. LayerZero lets you choose the fee token via `_payInLzToken`. At the time of writing, `_payInLzToken` is not enabled on Tempo, so set it to `false` and use `nativeFee` (LZD). 2. **Approve stablecoin**: approve the LZD contract to spend your stablecoin. 3. **Wrap**: call `LZD.wrap(feeToken, recipient, amount)` to convert your stablecoin into LZD. 4. **Approve LZD**: approve the OFT contract to spend your LZD. 5. **Send**: call `send()` with `msg.value = 0` and `MessagingFee(nativeFee, 0)`. ```solidity wrap theme={null} // 1. Quote the fee MessagingFee memory fee = oft.quoteSend(sendParam, false); // 2. Approve stablecoin to LZD IERC20(usdce).approve(address(lzd), fee.nativeFee); // 3. Wrap USDC.e into LZD lzd.wrap(usdce, msg.sender, fee.nativeFee); // 4. Approve LZD to the OFT IERC20(address(lzd)).approve(address(oft), fee.nativeFee); // 5. Send with msg.value = 0 oft.send{value: 0}(sendParam, fee, refundAddress); ``` At the time of writing, `_payInLzToken` is not enabled on Tempo, so set it to `false`. ## API reference ### wrap ```solidity wrap theme={null} function wrap(address _token, address _to, uint256 _amount) public nonReentrant onlyWhitelistedToken(_token) ``` Wraps a whitelisted stablecoin into LZD. Transfers the underlying token from `msg.sender` and mints LZD to `_to`. #### Parameters | Name | Type | Description | | -------- | ------- | ------------------------------ | | \_token | address | whitelisted stablecoin to wrap | | \_to | address | address to receive minted LZD | | \_amount | uint256 | amount to wrap (6-decimal) | ### unwrap ```solidity wrap theme={null} function unwrap(address _token, address _to, uint256 _amount) public nonReentrant onlyWhitelistedToken(_token) ``` Unwraps LZD back into a whitelisted stablecoin. Burns LZD from `msg.sender` and transfers the underlying token to `_to`. #### Parameters | Name | Type | Description | | -------- | ------- | ------------------------------------ | | \_token | address | whitelisted stablecoin to receive | | \_to | address | address to receive underlying tokens | | \_amount | uint256 | amount to unwrap (6-decimal) | ### whitelistToken ```solidity wrap theme={null} function whitelistToken(address _token) public onlyOwner ``` Adds a token to the whitelist. This is an owner-only admin function, so new fee tokens can only be added by the LZD contract owner. The token must have exactly 6 decimals and must not already be whitelisted. #### Parameters | Name | Type | Description | | ------- | ------- | -------------------- | | \_token | address | token address to add | ### unwhitelistToken ```solidity wrap theme={null} function unwhitelistToken(address _token) public onlyOwner ``` Removes a token from the whitelist. #### Parameters | Name | Type | Description | | ------- | ------- | ----------------------- | | \_token | address | token address to remove | ### isWhitelistedToken ```solidity wrap theme={null} function isWhitelistedToken(address _token) public view returns (bool isWhitelisted) ``` Returns whether a token is whitelisted. #### Parameters | Name | Type | Description | | ------- | ------- | -------------- | | \_token | address | token to check | ### getWhitelistedTokens ```solidity wrap theme={null} function getWhitelistedTokens() public view returns (address[] memory tokens) ``` Returns all whitelisted token addresses. ### getTokenBalance ```solidity wrap theme={null} function getTokenBalance(address _token) public view returns (uint256 balance) ``` Returns the contract's balance of a specific underlying token. #### Parameters | Name | Type | Description | | ------- | ------- | -------------- | | \_token | address | token to check | ### decimals ```solidity wrap theme={null} function decimals() public view returns (uint8) ``` Returns `6`. LZD uses 6 decimals to match its underlying whitelisted stablecoins. ### Events #### TokenWrapped ```solidity wrap theme={null} event TokenWrapped(address indexed token, address indexed from, address indexed to, uint256 amount) ``` Emitted when a whitelisted token is wrapped into LZD. #### TokenUnwrapped ```solidity wrap theme={null} event TokenUnwrapped(address indexed token, address indexed from, address indexed to, uint256 amount) ``` Emitted when LZD is unwrapped back into a whitelisted token. #### TokenWhitelisted ```solidity wrap theme={null} event TokenWhitelisted(address indexed token, bool whitelisted) ``` Emitted when a token is added to or removed from the whitelist. ### Errors | Error | Cause | | --------------------------------------- | -------------------------------------------- | | `NotWhitelisted(token)` | token is not on the whitelist | | `Whitelisted(token)` | token is already whitelisted (duplicate add) | | `InvalidToken(token)` | token is zero address or the LZD contract | | `InvalidTokenDecimals(token, decimals)` | token does not have 6 decimals | ## Quoting fees `quoteSend()` returns a `MessagingFee` with both `nativeFee` and `lzTokenFee`. LayerZero lets applications choose the payment token via `_payInLzToken`. At the time of writing, `_payInLzToken` is not enabled on Tempo, so set it to `false` and read the fee from `nativeFee` (LZD): ```solidity wrap theme={null} // false = quote in native fee token (LZD on Tempo) MessagingFee memory fee = oft.quoteSend(sendParam, false); // fee.nativeFee = amount of LZD required // fee.lzTokenFee = 0 on Tempo while _payInLzToken is disabled ``` ## msg.value behavior Tempo's `EndpointV2Alt` rejects any transaction that sends native value: * `msg.value > 0` reverts with `LZ_OnlyAltToken` * There is no native drop. `addExecutorNativeDropOption` is not supported Always set `msg.value = 0` when calling `send()` on Tempo. Do not attach any native value to LayerZero transactions. ## Failure modes | Revert reason | Cause | Mitigation | | -------------------------------- | -------------------------------------------------- | -------------------------------------------- | | `OFTAltCore__msg_value_not_zero` | `msg.value > 0` when calling `send()` on an OFTAlt | set `msg.value = 0` | | `LZ_OnlyAltToken` | `msg.value > 0` reaching the endpoint directly | set `msg.value = 0` | | `LZ_LzTokenUnavailable` | `quoteSend(_, true)` called | use `quoteSend(_, false)` | | `NotWhitelisted(token)` | fee token not whitelisted by LZD | use pathUSD, USDC.e, or USDT0 | | `InvalidTokenDecimals(token, d)` | token does not have 6 decimals | use a 6-decimal stablecoin | | ERC-20 transfer failure | insufficient LZD balance or missing approval | wrap stablecoin into LZD and approve the OFT | ## Contract addresses | Contract | Address | | ---------------- | -------------------------------------------- | | LZEndpointDollar | `0x0ceb237e109ee22374a567c6b09f373c73fa4cbb` | # OFT and OApp Alt Variants on Tempo Source: https://docs.layerzero.network/v2/developers/tempo/reference/oft-vs-oftalt Why Tempo requires OFTAlt and OAppAlt instead of standard contracts, and how the ERC-20 fee path works with EndpointV2Alt. ## Why Alt variants on Tempo Tempo has no native gas token. Sending `msg.value > 0` reverts, which means the standard fee payment path (fees via `msg.value` to the endpoint) does not work. Tempo uses [EndpointV2Alt](/v2/concepts/protocol/layerzero-endpoint-alt) instead of the standard `EndpointV2`. EndpointV2Alt replaces native token fee payment with ERC-20 token transfers using [LZEndpointDollar (LZD)](/v2/developers/tempo/reference/lz-endpoint-dollar). OFTAlt and OAppAlt are the contract variants designed to work with this ERC-20 fee path. **Building an OApp?** `OAppAlt` changes only the fee payment path: `_payNative` transfers LZD to the endpoint instead of using `msg.value`. The receive side is identical to `OAppReceiver`. Everything else (messaging, peer configuration, security) works the same way. Import from `@layerzerolabs/oapp-alt-evm/contracts/oapp/OAppAlt.sol`. ## How standard OFT fee payment works On a typical EVM chain, OFT pays LayerZero messaging fees by attaching native value: ```solidity wrap theme={null} // Standard OFT on Ethereum, Arbitrum, etc. MessagingFee memory fee = oft.quoteSend(sendParam, false); oft.send{value: fee.nativeFee}(sendParam, fee, refundAddress); ``` The endpoint's `_payNative` function reads `msg.value` and forwards native tokens to the message library. The endpoint refunds excess native value to the sender. ## EndpointV2Alt and the ERC-20 fee path On Tempo, `EndpointV2Alt` overrides `_payNative` to delegate to `_payToken`, which: 1. **Rejects native value**: reverts with `LZ_OnlyAltToken` if `msg.value > 0` 2. **Reads ERC-20 balance**: `_suppliedNative()` returns `IERC20(nativeErc20).balanceOf(address(this))` instead of `msg.value` 3. **Exposes the fee token**: `nativeToken()` returns the LZD address instead of `address(0)` OFTAlt contracts use this ERC-20 fee path. Before calling `send()`, the caller must approve and transfer LZD to the endpoint. ## Comparison table | Aspect | OFT | OFTAlt | OFTAdapter | OFTAdapterAlt | | --------------- | ------------------------------------------ | ------------------------------------------------- | ------------------------------------------------- | -------------------------------------------------------- | | **Fee payment** | `msg.value` (native) | ERC-20 `transferFrom` (LZD) | `msg.value` (native) | ERC-20 `transferFrom` (LZD) | | **Endpoint** | EndpointV2 | EndpointV2Alt | EndpointV2 | EndpointV2Alt | | **msg.value** | required for fees | must be 0 | required for fees | must be 0 | | **Native drop** | supported | not supported | supported | not supported | | **Token model** | new omnichain token | new omnichain token | wraps existing token | wraps existing token | | **Import path** | `@layerzerolabs/oft-evm/contracts/OFT.sol` | `@layerzerolabs/oft-alt-evm/contracts/OFTAlt.sol` | `@layerzerolabs/oft-evm/contracts/OFTAdapter.sol` | `@layerzerolabs/oft-alt-evm/contracts/OFTAdapterAlt.sol` | ## Contract interface OFTAlt extends the standard OFT interface but targets EndpointV2Alt: ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { OFTAlt } from "@layerzerolabs/oft-alt-evm/contracts/OFTAlt.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; contract MyOFTAlt is OFTAlt { constructor( string memory _name, string memory _symbol, address _lzEndpoint, // EndpointV2Alt address on Tempo address _delegate ) OFTAlt(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {} } ``` For adapting an existing ERC-20 token on Tempo, use `OFTAdapterAlt`. This uses lock/unlock: it locks tokens in the adapter on send and unlocks on receive. ```solidity wrap theme={null} import { OFTAdapterAlt } from "@layerzerolabs/oft-alt-evm/contracts/OFTAdapterAlt.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; contract MyOFTAdapterAlt is OFTAdapterAlt { constructor( address _token, // existing ERC-20 on Tempo address _lzEndpoint, // EndpointV2Alt address address _delegate ) OFTAdapterAlt(_token, _lzEndpoint, _delegate) Ownable(_delegate) {} } ``` **Do not use `OFTAdapterAlt` for TIP-20 tokens.** TIP-20 tokens only support `burn(amount)` (burning from `msg.sender`), not `burn(from, amount)`. Use [`OFTBurnSelfMintAlt`](/v2/developers/tempo/reference/tip-20-token-standard#integration-with-oftalt) instead — it transfers tokens to itself first, then burns from the adapter address. ## Fee payment differences ### Standard OFT (other chains) ```solidity wrap theme={null} // Fees paid via msg.value MessagingFee memory fee = oft.quoteSend(sendParam, false); oft.send{value: fee.nativeFee}(sendParam, fee, refundAddress); ``` ### OFTAlt (Tempo) ```solidity wrap theme={null} // Fees paid via ERC-20 transfer MessagingFee memory fee = oft.quoteSend(sendParam, false); // Approve the OFT to spend LZD for the fee IERC20(lzd).approve(address(oft), fee.nativeFee); // Send with msg.value = 0 oft.send{value: 0}(sendParam, fee, refundAddress); ``` On Tempo, you must wrap a whitelisted stablecoin into LZD and approve the OFT **before** calling `send()`. See the [LZD reference](/v2/developers/tempo/reference/lz-endpoint-dollar) for the wrap flow. ## Interoperability OFTAlt on Tempo communicates with standard OFT deployments on other chains without issue. The LayerZero protocol handles translation between fee models: * **Sending from Tempo**: the sender pays fees in LZD via OFTAlt. The destination chain receives the message as usual. * **Receiving on Tempo**: the sender on the source chain pays fees in the source chain's native token using standard OFT. The Tempo-side OFTAlt receives the message without requiring LZD from the receiver. No changes are needed to existing OFT contracts on other chains. The cross-chain message format is the same. ## Choosing the right contract | Scenario | Tempo contract | Other chains | | ---------------------------------------------- | -------------------- | --------------------- | | New ERC-20, minted on Tempo | `OFTAlt` | `OFT` | | Existing ERC-20 on Tempo, lock/unlock bridging | `OFTAdapterAlt` | `OFT` or `OFTAdapter` | | Existing TIP-20 on Tempo, mint/burn bridging | `OFTBurnSelfMintAlt` | `OFT` or `OFTAdapter` | **OFTAlt** is for new omnichain tokens where Tempo is a mint chain. It deploys a fresh ERC-20 on Tempo and handles cross-chain mint/burn. **OFTAdapterAlt** wraps an existing ERC-20 that already lives on Tempo. It locks tokens in the adapter on send and unlocks on receive. This is the Alt equivalent of [OFTAdapter](/v2/developers/evm/oft/quickstart#oft-adapter), targeting EndpointV2Alt instead of EndpointV2. **OFTBurnSelfMintAlt** is the [`OFTBurnSelfMint`](/v2/developers/evm/stablecoin-oft/ofts#oftburnselfmint) variant for chains using `EndpointV2Alt`. It bridges [TIP-20 tokens](/v2/developers/tempo/reference/tip-20-token-standard) using a transfer-then-burn pattern: on send, it transfers tokens to itself first, then burns from the adapter address. This is required because TIP-20 tokens only support `burn(amount)` (burning from `msg.sender`), not `burn(from, amount)`. On receive, it mints tokens directly to the recipient. The contract must hold `ISSUER_ROLE` on the TIP-20 token. On other chains, keep using standard OFT or OFTAdapter. No changes needed. The Tempo-side adapter connects to EndpointV2Alt while the other chains continue using EndpointV2. LayerZero routes messages between them without extra configuration. # Stargate UX on Tempo Source: https://docs.layerzero.network/v2/developers/tempo/reference/stargate-ux Consumer-side UX differences when using Stargate on Tempo: fee payment, approvals, and supported tokens. ## What changes for consumers Stargate on Tempo works the same way as on other chains, with these UX differences: * **Fees are paid in LZD** (an ERC-20) instead of via `msg.value` * **Additional approvals required**: asset token, stablecoin to LZD for wrapping, and LZD to Stargate * **No native drop**: you cannot send native tokens to the destination * **Bus mode disabled**: all sends are quoted and executed on-chain ## Fee payment On standard chains, `send()` accepts fees via `msg.value`. On Tempo, `send()` requires an LZD approval instead: ```solidity wrap theme={null} // Standard chain: fees via msg.value MessagingFee memory fee = stargate.quoteSend(sendParam, false); stargate.send{value: fee.nativeFee}(sendParam, fee, refundAddress); // Tempo: fees via LZD approval MessagingFee memory fee = stargate.quoteSend(sendParam, false); IERC20(lzd).approve(address(stargate), fee.nativeFee); // approve LZD stargate.send{value: 0}(sendParam, fee, refundAddress); // msg.value = 0 ``` `quoteSend()` returns both `nativeFee` and `lzTokenFee`. `_payInLzToken` selects the payment token. At the time of writing, `_payInLzToken` is not enabled on Tempo, so set it to `false` and use `nativeFee` (LZD). ## Approval flow Sending through Stargate on Tempo requires approving **both** the asset token and the LZD fee token: ```solidity wrap theme={null} // 1. Quote the send MessagingFee memory fee = stargate.quoteSend(sendParam, false); // 2. Approve the asset token (e.g., USDC.e) for the bridge amount IERC20(usdce).approve(address(stargate), bridgeAmount); // 3. Wrap stablecoin into LZD and approve for the fee IERC20(usdce).approve(address(lzd), fee.nativeFee); lzd.wrap(usdce, msg.sender, fee.nativeFee); IERC20(lzd).approve(address(stargate), fee.nativeFee); // 4. Send with msg.value = 0 stargate.send{value: 0}(sendParam, fee, refundAddress); ``` The [TempoOFTWrapper](/v2/developers/tempo/reference/lz-endpoint-dollar#tempooftwrapper) reduces this to a single approval + one `sendOFT` call by bundling wrap, approve, and send into an atomic transaction. ## Supported tokens | Source token | Tempo token | Can pay gas on Tempo | Can pay LZ fees | | ------------ | ----------- | -------------------- | --------------- | | USDC | USDC.e | yes | yes | | EURC | EURC.e | **no** | **no** | | USDT | USDT0 | TBD | TBD | ## The EURC problem Users who bridge **only EURC** to Tempo will be stuck. EURC.e cannot pay gas on Tempo and is not whitelisted by LZD for LayerZero fee payment. Without a USD stablecoin balance, the user cannot execute any transaction, not even a transfer or swap. If your frontend supports EURC bridging to Tempo, display a warning modal informing users that they must also bridge USDC to cover transaction fees on Tempo. ## Comparison table | Aspect | Standard Stargate | Stargate on Tempo | | ---------------------- | -------------------------- | ------------------------------------------------- | | **Fee payment** | `msg.value` (native token) | LZD approval (ERC-20) | | **Approvals** | asset token only | asset token + stablecoin to LZD + LZD to Stargate | | **msg.value** | required for fees | must be 0 | | **Native drop** | supported | not supported | | **Quote denomination** | native token (ETH, etc.) | LZD (USD-denominated) | | **Bus mode** | supported | disabled | ## Bus mode Bus mode is disabled on Tempo. Sends are executed as direct on-chain transactions with their own quoted fees. # TIP-20 Token Standard Source: https://docs.layerzero.network/v2/developers/tempo/reference/tip-20-token-standard How Tempo's TIP-20 token standard works with LayerZero: role-based administration, ERC-20 compatibility, and OFTAlt integration. ## What is TIP-20 TIP-20 is Tempo's native token standard. It shares the same interface as ERC-20 (`transfer`, `approve`, `balanceOf` all work the same way) but uses role-based access control (for example issuer, admin, and pause roles) instead of a single `Ownable` owner. TIP-20 is built for controlled tokens like stablecoins, where minting and burning require strict permissions. On Tempo, tokens like pathUSD are TIP-20 tokens. This page covers TIP-20 from a LayerZero integration perspective. For the full TIP-20 specification, see the [Tempo documentation](https://docs.tempo.xyz/protocol/tip20/overview). ## TIP-20 vs ERC-20 | Aspect | ERC-20 | TIP-20 | | ------------------ | ----------------------------- | ---------------------------------------------------------------------------------------------------------- | | **Interface** | standard ERC-20 | ERC-20 compatible (same read/transfer API) | | **Admin model** | `Ownable` or custom | [role-based access control](https://docs.tempo.xyz/protocol/tip20/overview#role-based-access-control-rbac) | | **Minting** | owner or custom logic | requires `ISSUER_ROLE` | | **Burning** | owner, self, or custom | requires `ISSUER_ROLE` | | **Fee payment** | not built-in | built-in stablecoin fee payment on Tempo | | **Payment lanes** | not available | dedicated blockspace for token transfers | | **Transfer memos** | not built-in | 32-byte memo field on transfers | | **Permit** | EIP-2612 (optional extension) | TIP-1004 (optional extension) | | **Compliance** | not built-in | TIP-403 policy registry integration | ## Role-based administration TIP-20 tokens use `ISSUER_ROLE` to control mint and burn operations. This differs from the typical `Ownable` pattern: * **ERC-20 with Ownable**: a single owner address controls privileged operations * **TIP-20 with ISSUER\_ROLE**: multiple addresses can hold the role, and role management follows a standard access control pattern Any contract that needs to mint or burn a TIP-20 token, including OFTAlt adapters, must hold `ISSUER_ROLE` on that token. ## Built-in features TIP-20 extends the ERC-20 interface with Tempo-specific features: * **Fee payment**: TIP-20 stablecoins can pay Tempo transaction fees (no native gas token needed) * **Payment lanes**: dedicated blockspace for TIP-20 transfers, providing predictable throughput * **Transfer memos**: 32-byte memo field attached to transfers for payment references or metadata * **Permit (TIP-1004)**: EIP-2612 `permit` functionality for gasless approvals via off-chain signatures * **Burning**: authorized contracts with `ISSUER_ROLE` can call `burn` ## Compliance integration Tempo provides the **TIP-403 policy registry** for whitelist/blacklist enforcement on token transfers. When a TIP-20 token registers a policy, the registry validates every transfer against the policy rules before execution. * Registry address: `0x403c...` (on Tempo) * Supports compound policies with separate sender and recipient rules (TIP-1015) * Integrators should be aware that transfers may revert if either party is blacklisted by the token's policy For full TIP-403 details, see the [Tempo documentation](https://docs.tempo.xyz/protocol/tip403/overview). ## How TIP-20 and ERC-20 coexist on Tempo Tempo supports both TIP-20 and ERC-20 tokens: * **TIP-20 tokens** are native to Tempo (e.g., pathUSD). They have built-in fee payment, payment lanes, and compliance features. * **ERC-20 tokens** can be deployed directly on Tempo or arrive via bridges. They follow standard ERC-20 semantics without TIP-20 extensions. Standard EVM tooling works for both: * `balanceOf`, `transfer`, `approve`, and `transferFrom` behave the same way * Wallets, block explorers, and SDKs interact with both standards through the same ERC-20 interface * Role-gated operations (`mint`, `burn`) are only available on TIP-20 tokens and require `ISSUER_ROLE` ## Integration with OFTAlt When bridging a TIP-20 token cross-chain using LayerZero, the adapter on Tempo needs to mint tokens on receive and burn tokens on send. TIP-20 tokens only support `burn(amount)` (burning from `msg.sender`), not `burn(from, amount)`, so the adapter uses a transfer-then-burn pattern: it transfers tokens to itself first, then burns from its own address. This requires `ISSUER_ROLE`: 1. Deploy a `OFTBurnSelfMintAlt` on Tempo pointing to the TIP-20 token 2. Grant `ISSUER_ROLE` to the adapter contract address on the TIP-20 token 3. The adapter can now mint on receive and burn on send ```solidity wrap theme={null} // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import { OFTBurnSelfMintExtendedRBACAltUpgradeable } from "@layerzerolabs/oft-evm-upgradeable-impl/contracts/extended/alt/OFTBurnSelfMintExtendedRBACAltUpgradeable.sol"; /** * @title OFTBurnSelfMintAlt * @notice OFTBurnSelfMint variant that pays native fees using an ERC20 token instead of msg.value. * @dev For chains where gas/native fees are paid via an ERC20 token (e.g., Tempo using EndpointV2Alt). */ contract OFTBurnSelfMintAlt is OFTBurnSelfMintExtendedRBACAltUpgradeable { constructor( address _token, address _burnerMinter, address _endpoint, bytes4 _burnSelector, bytes4 _mintSelector, uint8 _rateLimiterScaleDecimals ) OFTBurnSelfMintExtendedRBACAltUpgradeable( _token, _burnerMinter, _endpoint, _burnSelector, _mintSelector, _rateLimiterScaleDecimals ) {} } ``` Without `ISSUER_ROLE`, the adapter's mint and burn calls will revert. Ensure the role is granted before configuring cross-chain pathways. On other chains, the same token uses a standard OFT or OFTAdapter. No TIP-20 awareness is needed outside of Tempo. ## Whitelisted fee tokens [LZEndpointDollar (LZD)](/v2/developers/tempo/reference/lz-endpoint-dollar) accepts whitelisted 6-decimal TIP-20 stablecoins for fee wrapping: | Token | Type | Can pay LZ fees | | ------- | ------ | --------------- | | pathUSD | TIP-20 | yes | | USDC.e | TIP-20 | yes | | USDT0 | TIP-20 | yes | To pay fees, wrap any whitelisted token into LZD using `LZD.wrap()`. See the [LZD reference](/v2/developers/tempo/reference/lz-endpoint-dollar#fee-payment-flow) for the complete flow. # Build User Steps Source: https://docs.layerzero.network/v2/developers/value-transfer-api/api-reference/build-user-steps POST /build-user-steps Generate fresh transaction data for Solana transfers with up-to-date blockhash. Generates fresh transaction data for a quote. Required for **Solana transfers** due to short blockhash validity. *** ## Reference ### When to use | Chain type | When to call | | ---------- | --------------------------------------------------------------------------------------------------------------------------------------- | | **EVM** | Not required. Use `userSteps` directly from quote response. | | **Solana** | **Always required**. Solana transactions have short blockhash validity (\~60 seconds). Call this endpoint immediately before execution. | ### Request body | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------ | | `quoteId` | string | Yes | Quote ID from the `/quotes` response | ### Response Returns a `userSteps` array with fresh transaction data. #### User step attributes User steps can be `TRANSACTION` or `SIGNATURE` (same structure as quote response): **TRANSACTION step:** | Attribute | Type | Description | | --------------------- | ------ | --------------------------------- | | `type` | string | `TRANSACTION` | | `description` | string | Human-readable action description | | `chainKey` | string | Chain to execute on | | `chainType` | enum | `EVM`, `SOLANA`, `STARKNET` | | `signerAddress` | string | Wallet that must sign | | `transaction` | object | Transaction details | | `transaction.encoded` | object | Encoded transaction data | For Solana, the transaction data is base64-encoded: ```typescript wrap theme={null} { type: "TRANSACTION", chainType: "SOLANA", transaction: { encoded: { encoding: "base64", data: "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA..." } } } ``` ## Code examples ```bash wrap theme={null} curl -X POST "https://transfer.layerzero-api.com/v1/build-user-steps" \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{"quoteId": "QUOTE_ID_FROM_SOLANA_QUOTE"}' ``` ```typescript wrap theme={null} const response = await fetch('https://transfer.layerzero-api.com/v1/build-user-steps', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': 'YOUR_API_KEY', }, body: JSON.stringify({quoteId: quote.id}), }); const {userSteps} = await response.json(); console.log('User steps:', userSteps); ``` ```python wrap theme={null} import requests response = requests.post( "https://transfer.layerzero-api.com/v1/build-user-steps", headers={"x-api-key": "YOUR_API_KEY"}, json={"quoteId": quote["id"]}, ) user_steps = response.json()["userSteps"] print("User steps:", user_steps) ``` ### Response ```json wrap theme={null} { "userSteps": [ { "type": "TRANSACTION", "description": "bridge", "chainKey": "solana", "chainType": "SOLANA", "signerAddress": "Dz93pUVjXuaMnSsPSn7V99V4cUzhKoQdx9ECwZJZiafG", "transaction": { "encoded": { "encoding": "base64", "data": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAkT..." } } } ] } ``` ## Solana execution After building user steps, deserialize and sign the transaction: ```typescript wrap theme={null} import {VersionedTransaction, Connection} from '@solana/web3.js'; // Get fresh user steps const {userSteps} = await buildUserSteps(quoteId); const step = userSteps[0]; // Decode base64 transaction const txBuffer = Buffer.from(step.transaction.encoded.data, 'base64'); const tx = VersionedTransaction.deserialize(txBuffer); // Sign and send const signedTx = await wallet.signTransaction(tx); const signature = await connection.sendRawTransaction(signedTx.serialize()); await connection.confirmTransaction(signature, 'confirmed'); ``` ## Why Solana needs fresh transactions Solana transactions include a recent blockhash that expires after \~60 seconds. The `/build-user-steps` endpoint generates transactions with the latest blockhash, ensuring they remain valid during execution. **Workflow:** 1. Request quote → Receive quote with `quoteId` 2. Call `/build-user-steps` with `quoteId` → Get fresh transaction 3. Sign and submit immediately (within 60 seconds) ## Related endpoints * [Quotes](./quotes) — Request transfer quotes (includes `userSteps` for EVM) * [Status](./status) — Track transfer progress after execution ## Examples * [Solana Example](../examples/solana) — Complete Solana transfer with transaction building # List Chains Source: https://docs.layerzero.network/v2/developers/value-transfer-api/api-reference/chains GET /chains Retrieve all supported blockchain networks with their identifiers and native currencies. Returns a list of all blockchain networks supported by the Value Transfer API. Some supported chains enforce permissioned access and will reject transactions from non-whitelisted wallets. See [Permissioned chains](#permissioned-chains) for details. *** ## Reference ### Parameters | Parameter | Type | Required | Description | | ----------------------- | ------ | -------- | ---------------------------------------- | | `pagination[nextToken]` | string | No | Pagination cursor from previous response | ### Response Returns a `chains` array containing chain objects and a `pagination` object for cursor-based pagination. #### Attributes | Attribute | Type | Description | | ------------------------- | ------ | --------------------------------------------------------- | | `name` | string | Full chain name (for example, `Ethereum Mainnet`) | | `shortName` | string | Short display name (for example, `Ethereum`) | | `chainKey` | string | Unique chain identifier (for example, `ethereum`, `base`) | | `nativeCurrency` | object | Native currency details | | `nativeCurrency.chainKey` | string | Chain where currency exists | | `nativeCurrency.address` | string | Token address for native currency | | `nativeCurrency.decimals` | number | Decimal places | | `nativeCurrency.symbol` | string | Currency symbol (for example, `ETH`) | | `nativeCurrency.name` | string | Currency name | | `chainType` | enum | Blockchain type: `EVM`, `SOLANA`, `STARKNET` | ## Code examples ```bash wrap theme={null} curl -X GET "https://transfer.layerzero-api.com/v1/chains" ``` ```typescript wrap theme={null} const response = await fetch('https://transfer.layerzero-api.com/v1/chains'); const {chains} = await response.json(); console.log(chains); ``` ```python wrap theme={null} import requests response = requests.get("https://transfer.layerzero-api.com/v1/chains") chains = response.json()["chains"] print(chains) ``` ### Response ```json wrap theme={null} { "chains": [ { "name": "Ethereum", "shortName": "Ethereum", "chainKey": "ethereum", "nativeCurrency": { "chainKey": "ethereum", "address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", "decimals": 18, "symbol": "ETH", "name": "ETH" }, "chainType": "EVM" }, { "name": "Base", "shortName": "Base", "chainKey": "base", "nativeCurrency": { "chainKey": "base", "address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", "decimals": 18, "symbol": "ETH", "name": "Ether" }, "chainType": "EVM" }, { "name": "Solana", "shortName": "Solana", "chainKey": "solana", "nativeCurrency": { "chainKey": "solana", "address": "So11111111111111111111111111111111111111112", "decimals": 9, "symbol": "SOL", "name": "SOL" }, "chainType": "SOLANA" }, { "name": "Starknet", "shortName": "Starknet", "chainKey": "starknet", "nativeCurrency": { "chainKey": "starknet", "address": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", "decimals": 18, "symbol": "STRK", "name": "Starknet Token" }, "chainType": "STARKNET" }, { "name": "Avalanche", "shortName": "Avalanche", "chainKey": "avalanche", "nativeCurrency": { "chainKey": "avalanche", "address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", "decimals": 18, "symbol": "AVAX", "name": "Avalanche Token" }, "chainType": "EVM" } ], "pagination": {} } ``` ## Native token addresses | Chain type | Native token | Address format | Example | | ---------- | ---------------- | ----------------- | -------------------------------------------------------------------- | | EVM | ETH, MATIC, etc. | Standard address | `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` | | SOLANA | SOL | Base58 public key | `So11111111111111111111111111111111111111112` | | STARKNET | STRK | Felt252 address | `0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d` | ## Permissioned chains Some chains supported by the Value Transfer API enforce permissioned access. Transactions sent to these chains through public RPC endpoints will be rejected unless your wallet has been approved. Check the table below before integrating with these chains. | Chain | Access model | Public RPC restriction | How to get access | | -------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- | | Redbelly | Protocol-level KYC via verifiable credentials | `governors.mainnet.redbelly.network` is read-only; write operations (e.g., sending transactions) are blocked | [Apply for whitelisting](https://access.redbelly.network/) | ## Related endpoints * [Tokens](/v2/developers/value-transfer-api/api-reference/tokens) — Discover transferrable tokens for specific chains * [Quotes](/v2/developers/value-transfer-api/api-reference/quotes) — Request transfer quotes between chains # Get Metadata Source: https://docs.layerzero.network/v2/developers/value-transfer-api/api-reference/metadata GET /metadata Retrieve deployment addresses for multicall and transfer delegate contracts by chain. Returns deployment metadata for each supported chain, including contract addresses for the multicall and transfer delegate contracts. Use this endpoint to look up the correct contract addresses for building transactions or verifying approvals on a specific chain. *** ## Reference ### Response Returns an object keyed by chain name, where each chain contains a `deployments` object with contract addresses. #### Chain deployment attributes | Attribute | Type | Description | | -------------------------------------- | ------ | --------------------------------------------------------- | | `deployments` | object | Contract deployment addresses for this chain | | `deployments.multicall` | object | Multicall contract details | | `deployments.multicall.address` | string | Address of the multicall (fee wrapper) contract | | `deployments.transferDelegate` | object | Transfer delegate contract details | | `deployments.transferDelegate.address` | string | Address of the transfer delegate (token spender) contract | ## Code examples ```bash wrap theme={null} curl -X GET "https://transfer.layerzero-api.com/v1/metadata" ``` ```typescript wrap theme={null} const response = await fetch('https://transfer.layerzero-api.com/v1/metadata'); const metadata = await response.json(); // Get transfer delegate address for Base const baseDelegate = metadata.base.deployments.transferDelegate.address; console.log('Base TransferDelegate:', baseDelegate); ``` ```python wrap theme={null} import requests response = requests.get("https://transfer.layerzero-api.com/v1/metadata") metadata = response.json() # Get transfer delegate address for Base base_delegate = metadata["base"]["deployments"]["transferDelegate"]["address"] print(f"Base TransferDelegate: {base_delegate}") ``` ### Response ```json wrap theme={null} { "base": { "deployments": { "multicall": { "address": "0x0564F89f6edf2cA62fBd174378f9187e447DD410" }, "transferDelegate": { "address": "0xf45722F37f602c0788Beb7C1471ebEB281308860" } } }, "ethereum": { "deployments": { "multicall": { "address": "0xFD268A4813005A9fb04982073E5f4916f8653B75" }, "transferDelegate": { "address": "0x0CD0aD832f06b05f8fE78E6DB825c3d4eA944004" } } }, "arbitrum": { "deployments": { "multicall": { "address": "0x26F97eb05469fF5d2169FD4d83cF96939C9B8e37" }, "transferDelegate": { "address": "0x43093Bb72d982C04DFb858Fd31a85fBcB6c13CBD" } } } } ``` ## Related endpoints * [Chains](/v2/developers/value-transfer-api/api-reference/chains) — List supported blockchain networks * [Contracts overview](/v2/developers/value-transfer-api/contracts/overview) — Learn about the multicall and transfer delegate contract architecture * [Contract addresses](/v2/developers/value-transfer-api/contracts/addresses) — Static list of deployed contract addresses # API Reference Source: https://docs.layerzero.network/v2/developers/value-transfer-api/api-reference/overview Reference documentation for the Value Transfer API endpoints, authentication, and error handling. The Value Transfer API provides endpoints for cross-chain token transfers across 150+ blockchains. The API uses REST principles, returns JSON responses, and uses standard HTTP status codes. ``` https://transfer.layerzero-api.com/v1 ``` *** ## Reference ### Endpoints #### Discovery * [Chains](/v2/developers/value-transfer-api/api-reference/chains) — List supported blockchain networks * [Tokens](/v2/developers/value-transfer-api/api-reference/tokens) — Discover transferrable tokens and validate transfer routes * [Metadata](/v2/developers/value-transfer-api/api-reference/metadata) — Retrieve deployment addresses by chain #### Transfers * [Quotes](/v2/developers/value-transfer-api/api-reference/quotes) — Request transfer quotes with fees and execution steps * [Build user steps](/v2/developers/value-transfer-api/api-reference/build-user-steps) — Generate fresh transaction data for Solana transfers * [Submit signature](/v2/developers/value-transfer-api/api-reference/submit-signature) — Submit EIP-712 signatures for intent-based routes * [Status](/v2/developers/value-transfer-api/api-reference/status) — Track transfer progress and completion ## Authentication Transfer endpoints require an API key passed via the `x-api-key` header: ```bash wrap theme={null} curl -X POST "https://transfer.layerzero-api.com/v1/quotes" \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ ... }' ``` | Endpoint | Auth required | | ------------------------ | ------------- | | `GET /chains` | No | | `GET /tokens` | No | | `GET /metadata` | No | | `POST /quotes` | Yes | | `POST /build-user-steps` | Yes | | `POST /submit-signature` | Yes | | `GET /status/{quoteId}` | Yes | Requests to authenticated endpoints without a valid API key return: ```json wrap theme={null} { "error": "Unauthorized" } ``` ## Pagination Endpoints that return lists support cursor-based pagination using `pagination[nextToken]`: ```bash wrap theme={null} curl "https://transfer.layerzero-api.com/v1/tokens?pagination%5BnextToken%5D=abc123" ``` The response includes a `pagination` object with `nextToken` if more results exist: ```json wrap theme={null} { "tokens": [...], "pagination": { "nextToken": "def456" } } ``` Continue fetching until `pagination.nextToken` is `undefined` or absent. # Request Quotes Source: https://docs.layerzero.network/v2/developers/value-transfer-api/api-reference/quotes POST /quotes Request cross-chain transfer quotes with fees, routes, and execution steps. Returns one or more quotes for cross-chain transfers, each with fee breakdowns, estimated duration, and execution steps. *** ## Reference ### Request body | Parameter | Type | Required | Description | | ----------------------------- | ------ | -------- | --------------------------------------------------------- | | `amount` | string | Yes | Amount in smallest units (wei for ETH, lamports for SOL) | | `srcChainKey` | string | Yes | Source chain identifier (for example, `base`, `ethereum`) | | `srcTokenAddress` | string | Yes | Source token contract address | | `srcWalletAddress` | string | Yes | Sender wallet address | | `dstChainKey` | string | Yes | Destination chain identifier | | `dstTokenAddress` | string | Yes | Destination token contract address | | `dstWalletAddress` | string | Yes | Recipient wallet address | | `options` | object | No | Optional transfer configuration | | `options.amountType` | enum | No | `EXACT_SRC_AMOUNT` (default) | | `options.feeTolerance` | object | No | Maximum acceptable fee variance | | `options.feeTolerance.type` | string | No | `PERCENT` | | `options.feeTolerance.amount` | number | No | Tolerance percentage (0-100). Default: `1` | | `options.dstNativeDropAmount` | string | No | Native gas drop on destination. Default: `0` | ## Response Returns a `quotes` array, `rejectedQuotes` array, and `tokens` array. ### Quote attributes | Attribute | Type | Description | | ----------------------------- | -------------- | ------------------------------------------------------------- | | `id` | string | Unique quote identifier (becomes transfer ID after execution) | | `routeSteps` | array | Array of route segments with protocol types | | `fees` | array | Detailed fee breakdown by chain and type | | `duration` | object | Estimated transfer duration | | `duration.estimated` | string or null | Duration in milliseconds (null if unknown) | | `feeUsd` | string | Total fees in USD | | `feePercent` | string | Fee as percentage of transfer amount | | `srcAmount` | string | Exact amount leaving source chain | | `dstAmount` | string | Expected amount on destination chain | | `dstAmountMin` | string | Minimum guaranteed amount (with slippage protection) | | `srcAmountUsd` | string | Source amount in USD | | `dstAmountUsd` | string | Destination amount in USD | | `userSteps` | array | Actions to execute (transactions or signatures) | | `options` | object | Transfer options | | `options.dstNativeDropAmount` | string | Native gas drop configuration | | `expiresAt` | string | Quote expiration timestamp (may not be present on all quotes) | ### Route step attributes | Attribute | Type | Description | | ------------- | ------ | ------------------------------------------------ | | `type` | enum | Route protocol (see [Route types](#route-types)) | | `srcChainKey` | string | Chain where this step executes | | `description` | string | Human-readable step description | ### Fee attributes | Attribute | Type | Description | | ------------- | ------ | ------------------------------------------------------- | | `chainKey` | string | Chain where fee is paid | | `type` | enum | `MESSAGE`, `GENERAL`, `DST_NATIVE_DROP`, `CCTP_RECEIVE` | | `description` | string | Human-readable fee description | | `amount` | string | Fee amount in token units | | `address` | string | Token address for fee | ### User step types User steps can be `TRANSACTION` or `SIGNATURE`: **TRANSACTION step:** ```typescript wrap theme={null} { type: "TRANSACTION", description: string, chainKey: string, chainType: "EVM" | "SOLANA" | "STARKNET", signerAddress: string, transaction: { encoded: { to: string, data: string, value?: string, chainId: number, from?: string, gasLimit?: string } } } ``` **SIGNATURE step (EIP-712 for Aori routes):** ```typescript wrap theme={null} { type: "SIGNATURE", description: string, chainKey: string, signerAddress: string, signature: { type: "EIP712", typedData: { domain: object, types: object, message: object } } } ``` ### Executing userSteps safely For ERC20 token transfers, the API returns two user steps with **different** `to` addresses: | Step | `transaction.encoded.to` | Purpose | | ------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------- | | Approve | ERC20 token contract (e.g., USDC) | Calls `approve(TransferDelegate, amount)`. The TransferDelegate address is encoded inside the `data` field. | | Bridge | **LZMulticall** contract | Routes the bridge transaction through LayerZero's feeWrapper. | **Never approve the LZMulticall (Wrapper) as a token spender** LZMulticall executes bridge transactions. It is not the right spender, and approving it will lose you tokens. The correct spender is the TransferDelegate, and the API's approve step already has this set in the calldata. Execute every userStep as returned. See [Contracts Overview](/v2/developers/value-transfer-api/contracts/overview) for details on the contract architecture. ### Route types The API evaluates multiple protocols and returns the best routes: | Type | Description | | ------------------ | ----------------------------- | | `OFT` | OFT Standard transfers | | `STARGATE_V2_TAXI` | Stargate V2 instant transfers | | `STARGATE_V2_BUS` | Stargate V2 batched transfers | | `CCTP` | Circle CCTP for native USDC | | `AORI` | Intent-based swaps via Aori | ## Code examples ### EVM transfer ```bash wrap theme={null} curl -X POST "https://transfer.layerzero-api.com/v1/quotes" \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "srcChainKey": "base", "dstChainKey": "arbitrum", "srcTokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "dstTokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "srcWalletAddress": "0x1234567890123456789012345678901234567890", "dstWalletAddress": "0x1234567890123456789012345678901234567890", "amount": "1000000" }' ``` ```typescript wrap theme={null} const response = await fetch('https://transfer.layerzero-api.com/v1/quotes', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': 'YOUR_API_KEY', }, body: JSON.stringify({ srcChainKey: 'base', dstChainKey: 'arbitrum', srcTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', dstTokenAddress: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', srcWalletAddress: '0x1234567890123456789012345678901234567890', dstWalletAddress: '0x1234567890123456789012345678901234567890', amount: '1000000', }), }); const {quotes} = await response.json(); console.log('Quote ID:', quotes[0].id); console.log('Fee:', quotes[0].feeUsd, 'USD'); ``` ```python wrap theme={null} import requests response = requests.post( "https://transfer.layerzero-api.com/v1/quotes", headers={"x-api-key": "YOUR_API_KEY"}, json={ "srcChainKey": "base", "dstChainKey": "arbitrum", "srcTokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "dstTokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "srcWalletAddress": "0x1234567890123456789012345678901234567890", "dstWalletAddress": "0x1234567890123456789012345678901234567890", "amount": "1000000", }, ) quote = response.json()["quotes"][0] print(f"Quote ID: {quote['id']}") print(f"Fee: {quote.get('feeUsd', 'N/A')} USD") ``` ### Response ```json wrap theme={null} { "error": null, "quotes": [ { "id": "0x00000000000000000000000000000000019c43bf3589735e9c7299af4c6f5971", "routeSteps": [ { "type": "STARGATE_V2_TAXI", "srcChainKey": "base", "description": "Stargate" } ], "fees": [ { "chainKey": "base", "type": "MESSAGE", "description": "", "amount": "45604559761712", "address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" } ], "duration": { "estimated": "27600" }, "feeUsd": "0.00033795", "feePercent": "0.00033800", "srcAmount": "1000000", "dstAmount": "999662", "dstAmountMin": "990000", "srcAmountUsd": "0.99986254", "dstAmountUsd": "0.99952459", "userSteps": [ { "type": "TRANSACTION", "description": "approve", "chainKey": "base", "chainType": "EVM", "signerAddress": "0x1234567890123456789012345678901234567890", "transaction": { "encoded": { "chainId": 8453, "data": "0x095ea7b300000000000000000000000027a16dc786820b16e5c9028b75b99f6f604b5d2600000000000000000000000000000000000000000000000000000000000f4240", "from": "0x1234567890123456789012345678901234567890", "to": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" } } }, { "type": "TRANSACTION", "description": "bridge", "chainKey": "base", "chainType": "EVM", "signerAddress": "0x1234567890123456789012345678901234567890", "transaction": { "encoded": { "chainId": 8453, "data": "0xc7c7f5b3...", "from": "0x1234567890123456789012345678901234567890", "to": "0x27a16dc786820B16E5c9028b75B99F6f604b5d26", "value": "45604559761712" } } } ], "options": { "dstNativeDropAmount": "0" } } ], "rejectedQuotes": [], "tokens": [ { "chainKey": "base", "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "decimals": 6, "symbol": "USDC", "name": "USD Coin", "price": { "usd": 0.99986254 } }, { "chainKey": "arbitrum", "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "decimals": 6, "symbol": "USDC", "name": "USD Coin", "price": { "usd": 0.99986254 } }, { "chainKey": "base", "address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", "decimals": 18, "symbol": "ETH", "name": "Ether", "price": { "usd": 2132.32235403 } }, { "chainKey": "arbitrum", "address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", "decimals": 18, "symbol": "ETH", "name": "ETH", "price": { "usd": 2132.32235403 } } ] } ``` ## Errors The API returns an error object when the request fails: ```json wrap theme={null} { "error": { "status": 4000, "message": "Invalid input", "issues": [ { "message": "Invalid source token" } ] }, "quotes": [] } ``` Check the `error` field before processing quotes: ```typescript wrap theme={null} const result = await response.json(); if (result.error) { console.error('Quote error:', result.error.message); result.error.issues?.forEach((issue) => { console.error(' -', issue.message); }); return; } ``` ## Quote expiration Quotes may include an `expiresAt` timestamp. Execute quotes promptly or request a new quote if expired. ```typescript wrap theme={null} const quote = quotes[0]; if (quote.expiresAt) { const expiresAt = new Date(quote.expiresAt); if (new Date() > expiresAt) { console.log('Quote expired, request a new one'); } } ``` ## Related endpoints * [Build user steps](./build-user-steps) — Generate fresh transaction data for Solana transfers * [Submit signature](./submit-signature) — Submit EIP-712 signatures for intent-based routes * [Status](./status) — Track transfer progress after execution ## Examples * [EVM Example](../examples/evm) — Complete transfer with viem * [Solana Example](../examples/solana) — Complete Solana transfer # Track Transfer Status Source: https://docs.layerzero.network/v2/developers/value-transfer-api/api-reference/status GET /status/{quoteId} Monitor cross-chain transfer progress with real-time status updates. Returns the current status of a cross-chain transfer. Poll this endpoint after executing user steps to monitor progress until completion. *** ## Reference ### Parameters | Parameter | Type | Location | Required | Description | | --------- | ------ | -------- | -------- | ---------------------------------------------------------------- | | `quoteId` | string | Path | Yes | Quote ID from the `/quotes` response | | `txHash` | string | Query | No | Transaction hash from execution (recommended for faster updates) | ### Response Returns transfer status information with optional execution history. #### Attributes | Attribute | Type | Description | | ------------------ | ------ | --------------------------------------------------------------------------------- | | `status` | enum | Current transfer state: `PENDING`, `PROCESSING`, `SUCCEEDED`, `FAILED`, `UNKNOWN` | | `explorerUrl` | string | Optional LayerZero Scan URL for tracking | | `executionHistory` | array | Optional array of execution events | #### Status values | Status | Description | Terminal | | ------------ | ----------------------------------------- | -------- | | `UNKNOWN` | Transfer not found or not started | Yes | | `PENDING` | Transfer initiated but not yet processing | No | | `PROCESSING` | Cross-chain message in transit | No | | `SUCCEEDED` | Transfer completed successfully | Yes | | `FAILED` | Transfer failed (reverted or timeout) | Yes | #### Execution history events | Event | Description | Chain | | ----------- | --------------------------------- | ----------------- | | `SENT` | Transaction submitted | Source chain | | `BUS_RODE` | Batch executed (Stargate V2 only) | Source chain | | `DELIVERED` | Message delivered | Destination chain | Each event includes: | Attribute | Type | Description | | ----------------------- | ------ | ------------------------------ | | `event` | enum | Event type | | `transaction` | object | Transaction details | | `transaction.chainKey` | string | Chain where event occurred | | `transaction.hash` | string | Transaction hash | | `transaction.timestamp` | number | Unix timestamp in milliseconds | ## Code examples ```bash wrap theme={null} curl -X GET "https://transfer.layerzero-api.com/v1/status/QUOTE_ID?txHash=0x..." \ -H "x-api-key: YOUR_API_KEY" ``` ```typescript wrap theme={null} const params = new URLSearchParams({txHash: '0x...'}); const response = await fetch( `https://transfer.layerzero-api.com/v1/status/QUOTE_ID?${params}`, { headers: { 'x-api-key': 'YOUR_API_KEY', }, }, ); const {status, explorerUrl} = await response.json(); console.log('Status:', status); console.log('Explorer:', explorerUrl); ``` ```python wrap theme={null} import requests response = requests.get( "https://transfer.layerzero-api.com/v1/status/QUOTE_ID", headers={"x-api-key": "YOUR_API_KEY"}, params={"txHash": "0x..."}, ) data = response.json() print(f"Status: {data['status']}") print(f"Explorer: {data.get('explorerUrl')}") ``` ### Response ```json wrap theme={null} { "status": "SUCCEEDED", "explorerUrl": "https://layerzeroscan.com/tx/0x...", "executionHistory": [ { "event": "SENT", "transaction": { "chainKey": "base", "hash": "0x123...", "timestamp": 1704067200000 } }, { "event": "DELIVERED", "transaction": { "chainKey": "arbitrum", "hash": "0x456...", "timestamp": 1704067260000 } } ] } ``` ## Error handling | HTTP Status | Description | Action | | ----------- | ------------------- | ------------------------------ | | `404` | Quote not found | Return `UNKNOWN` status | | `429` | Rate limit exceeded | Wait 5 seconds, retry | | `500` | Server error | Retry with exponential backoff | ```typescript wrap theme={null} async function checkStatusSafe(quoteId: string, txHash: string): Promise { try { const params = new URLSearchParams({txHash}); const response = await fetch( `https://transfer.layerzero-api.com/v1/status/${quoteId}?${params}`, {headers: {'x-api-key': 'YOUR_API_KEY'}}, ); if (response.status === 404) return 'UNKNOWN'; if (response.status === 429) { await new Promise((r) => setTimeout(r, 5000)); return checkStatusSafe(quoteId, txHash); } const {status} = await response.json(); return status; } catch (error) { console.error('Status check failed:', error); return 'UNKNOWN'; } } ``` ## Related endpoints * [Quotes](./quotes) — Request transfer quotes (provides the `quoteId`) * [Build user steps](./build-user-steps) — Generate fresh transactions for Solana * [Submit signature](./submit-signature) — Submit signatures for Aori routes ## Examples * [EVM Example](../examples/evm) — Complete transfer with status tracking * [Solana Example](../examples/solana) — Solana transfer with polling # Submit Signature Source: https://docs.layerzero.network/v2/developers/value-transfer-api/api-reference/submit-signature POST /submit-signature Submit EIP-712 signatures for intent-based transfer routes. Submits EIP-712 signatures for intent-based routes like Aori. Required when a quote includes `SIGNATURE` user steps. *** ## Reference ### When to use | Route type | When to call | | ---------------- | ---------------------------------------------------------------------- | | **AORI\_V1** | **Required**. Intent-based routes need off-chain signature submission. | | **Other routes** | Not applicable. Use on-chain transactions only. | ### Request body | Parameter | Type | Required | Description | | ------------ | --------------- | -------- | ------------------------------------- | | `quoteId` | string | Yes | Quote ID from the `/quotes` response | | `signatures` | string or array | Yes | EIP-712 signature(s) as hex string(s) | ### Response Returns an empty object on success. ```json wrap theme={null} {} ``` ## Code examples ```bash wrap theme={null} curl -X POST "https://transfer.layerzero-api.com/v1/submit-signature" \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "quoteId": "QUOTE_ID", "signatures": ["0x1234567890abcdef..."] }' ``` ```typescript wrap theme={null} const response = await fetch('https://transfer.layerzero-api.com/v1/submit-signature', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': 'YOUR_API_KEY', }, body: JSON.stringify({ quoteId: 'QUOTE_ID', signatures: ['0x1234567890abcdef...'], }), }); if (!response.ok) { throw new Error(`Signature submission failed: ${response.statusText}`); } console.log('Signature submitted successfully'); ``` ```python wrap theme={null} import requests response = requests.post( "https://transfer.layerzero-api.com/v1/submit-signature", headers={"x-api-key": "YOUR_API_KEY"}, json={ "quoteId": "QUOTE_ID", "signatures": ["0x1234567890abcdef..."], }, ) if not response.ok: raise Exception(f"Signature submission failed: {response.status_code}") print("Signature submitted successfully") ``` ## Signing EIP-712 messages When a quote includes a `SIGNATURE` user step, sign the EIP-712 typed data and submit it: ### Step 1: Extract typed data ```typescript wrap theme={null} const signatureStep = quote.userSteps.find((step) => step.type === 'SIGNATURE'); const {domain, types, message} = signatureStep.signature.typedData; ``` ### Step 2: Convert BigInt fields **BigInt conversion required:** The API returns numeric message fields as strings for JSON compatibility. Convert these to `BigInt` before signing. ```typescript wrap theme={null} const normalizedMessage = { ...message, inputAmount: BigInt(message.inputAmount), outputAmount: BigInt(message.outputAmount), startTime: BigInt(message.startTime), endTime: BigInt(message.endTime), }; ``` ### Step 3: Sign typed data ```typescript wrap theme={null} import {type WalletClient} from 'viem'; const signature = await walletClient.signTypedData({ domain, types, primaryType: Object.keys(types).find((key) => key !== 'EIP712Domain'), message: normalizedMessage, }); ``` ```typescript wrap theme={null} import {ethers} from 'ethers'; const signature = await signer._signTypedData(domain, types, normalizedMessage); ``` ### Step 4: Submit signature ```typescript wrap theme={null} await fetch('https://transfer.layerzero-api.com/v1/submit-signature', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': 'YOUR_API_KEY', }, body: JSON.stringify({ quoteId: quote.id, signatures: [signature], }), }); ``` ## Complete example ```typescript wrap theme={null} async function executeAoriRoute(quote: Quote, walletClient: WalletClient) { for (const step of quote.userSteps) { if (step.type === 'SIGNATURE') { const {domain, types, message} = step.signature.typedData; // Convert numeric fields to BigInt const normalizedMessage = { ...message, inputAmount: BigInt(message.inputAmount), outputAmount: BigInt(message.outputAmount), startTime: BigInt(message.startTime), endTime: BigInt(message.endTime), }; // Sign EIP-712 message const signature = await walletClient.signTypedData({ domain, types, primaryType: Object.keys(types).find((k) => k !== 'EIP712Domain'), message: normalizedMessage, }); // Submit signature to API await fetch('https://transfer.layerzero-api.com/v1/submit-signature', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': 'YOUR_API_KEY', }, body: JSON.stringify({ quoteId: quote.id, signatures: [signature], }), }); } } } ``` ## Errors | HTTP Status | Description | | ----------- | ----------------------------- | | `400` | Invalid signature or quote ID | | `404` | Quote not found | ## Related endpoints * [Quotes](./quotes) — Request transfer quotes (includes `SIGNATURE` steps for Aori routes) * [Status](./status) — Track transfer progress after signature submission # List Tokens Source: https://docs.layerzero.network/v2/developers/value-transfer-api/api-reference/tokens GET /tokens Retrieve supported tokens and validate transfer routes between chains. Returns tokens supported by the Value Transfer API. Use query parameters to filter for tokens transferrable from a specific source chain and token. *** ## Reference ### Parameters | Parameter | Type | Required | Description | | ------------------------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------- | | `transferrableFromChainKey` | string | No | Source chain key (for example, `base`, `ethereum`). **Must be combined** with `transferrableFromTokenAddress`. | | `transferrableFromTokenAddress` | string | No | Source token address. **Must be combined** with `transferrableFromChainKey`. | | `pagination[nextToken]` | string | No | Pagination cursor from previous response | **Validate transfer routes** Provide **both** `transferrableFromChainKey` and `transferrableFromTokenAddress` to get only destination tokens you can transfer to. ### Response Returns a `tokens` array and a `pagination` object. #### Attributes | Attribute | Type | Description | | ------------- | ------- | -------------------------------------------------- | | `isSupported` | boolean | Whether the token is available for transfers | | `chainKey` | string | Chain identifier (for example, `ethereum`, `base`) | | `address` | string | Token contract address | | `decimals` | number | Token decimal places | | `symbol` | string | Token symbol (for example, `ETH`, `USDC`) | | `name` | string | Full token name | | `logoUrl` | string | Optional token logo URL | | `price` | object | Optional price information | | `price.usd` | number | Current price in USD | ## Code examples ### List all tokens Returns the complete token catalog across all chains. ```bash wrap theme={null} curl -X GET "https://transfer.layerzero-api.com/v1/tokens" ``` ```typescript wrap theme={null} const response = await fetch('https://transfer.layerzero-api.com/v1/tokens'); const {tokens} = await response.json(); console.log(tokens); ``` ```python wrap theme={null} import requests response = requests.get("https://transfer.layerzero-api.com/v1/tokens") tokens = response.json()["tokens"] print(tokens) ``` ### List transferrable destinations Returns only tokens you can transfer to from a specific source. ```bash wrap theme={null} curl -X GET "https://transfer.layerzero-api.com/v1/tokens?transferrableFromChainKey=base&transferrableFromTokenAddress=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" ``` ```typescript wrap theme={null} const params = new URLSearchParams({ transferrableFromChainKey: 'base', transferrableFromTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', }); const response = await fetch(`https://transfer.layerzero-api.com/v1/tokens?${params}`); const {tokens} = await response.json(); console.log(tokens); ``` ```python wrap theme={null} import requests response = requests.get( "https://transfer.layerzero-api.com/v1/tokens", params={ "transferrableFromChainKey": "base", "transferrableFromTokenAddress": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", }, ) tokens = response.json()["tokens"] print(tokens) ``` ### Response ```json wrap theme={null} { "tokens": [ { "isSupported": true, "chainKey": "base", "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "decimals": 6, "symbol": "USDC", "name": "USD Coin", "price": { "usd": 0.99986254 } }, { "isSupported": true, "chainKey": "base", "address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", "decimals": 18, "symbol": "ETH", "name": "Ether", "price": { "usd": 2132.3224 } }, { "isSupported": true, "chainKey": "ethereum", "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "decimals": 6, "symbol": "USDC", "name": "USDC", "price": { "usd": 0.99986254 } }, { "isSupported": true, "chainKey": "ethereum", "address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", "decimals": 18, "symbol": "ETH", "name": "ETH", "price": { "usd": 2132.3224 } }, { "isSupported": true, "chainKey": "arbitrum", "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "decimals": 6, "symbol": "USDC", "name": "USD Coin", "price": { "usd": 0.99986254 } } ], "pagination": {} } ``` ## Validate transfer routes Check if a transfer route exists before requesting quotes: ```typescript wrap theme={null} async function validatePath( srcChain: string, srcToken: string, dstChain: string, dstToken: string, ): Promise { const params = new URLSearchParams({ transferrableFromChainKey: srcChain, transferrableFromTokenAddress: srcToken, }); const response = await fetch(`https://transfer.layerzero-api.com/v1/tokens?${params}`); const {tokens} = await response.json(); return tokens.some( (t) => t.chainKey === dstChain && t.address.toLowerCase() === dstToken.toLowerCase(), ); } const isSupported = await validatePath( 'base', '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', 'arbitrum', '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', ); ``` ## Related endpoints * [Chains](./chains) — List supported blockchain networks * [Quotes](./quotes) — Request transfer quotes for validated routes # Contract Addresses Source: https://docs.layerzero.network/v2/developers/value-transfer-api/contracts/addresses LZMulticall (Wrapper) and TransferDelegate contract addresses for each supported chain. Deployed addresses for the LZMulticall (Wrapper) and TransferDelegate (Delegate) on each chain. For what each contract does and which one to use for approvals, see the [Contracts Overview](/v2/developers/value-transfer-api/contracts/overview). ## EVM ## Non-EVM # Contracts Source: https://docs.layerzero.network/v2/developers/value-transfer-api/contracts/overview How the Value Transfer API's contracts handle token approvals, bridge execution, and fees on each chain. The Value Transfer API relies on three contracts per EVM chain: the **TransferDelegate** (handles token approvals), the **LZMulticall** (batches and executes bridge transactions), and the **Treasury** (collects fees). **Setting token allowances** The Value Transfer API uses two separate contracts for approvals and execution. The **TransferDelegate** is the only contract that should be approved as a token spender. The **LZMulticall** (Wrapper) batches and executes bridge transactions but does not need or support token allowances. The API's approve `userStep` already encodes the correct TransferDelegate address in its calldata, so you don't need to look up or hardcode any contract address. Execute the step as returned. See [Contract Addresses](/v2/developers/value-transfer-api/contracts/addresses) for the Wrapper and Delegate addresses on each chain. ## Contract architecture Token transfers go through two contracts in sequence: the **Delegate** pulls tokens, then the **Wrapper** executes the bridge. A third contract, the **Treasury**, collects fees behind the scenes. | Contract | Role | Function | | --------------------------- | ------------------------------------ | ----------------------------------------------- | | TransferDelegate (Delegate) | Approved spender for ERC20 transfers | `delegateTransferFrom(token, from, to, amount)` | | LZMulticall (Wrapper) | Batches and executes bridge calls | `execute(calls, quoteId)` | | Treasury | Fee collection | `getFees()` | ## TransferDelegate (Delegate) This is the contract users approve as the ERC20 spender. One function: ``` delegateTransferFrom(address token, address from, address to, uint256 amount) ``` When LZMulticall executes a bridge, it calls TransferDelegate to pull tokens from the user's wallet. That call only succeeds if the user has already approved TransferDelegate for that token. The approve `userStep` returned by the API already encodes the TransferDelegate address in its calldata. No need to look it up or hardcode it. ## LZMulticall (Wrapper) LZMulticall is a batch executor. It takes an array of calls and runs them in a single transaction, routing bridge operations through LayerZero. Each LZMulticall deploys its own TransferDelegate in the constructor, so the two contracts are always paired 1:1. You can look up the paired Delegate address via the `TRANSFER_DELEGATE` getter on any LZMulticall deployment. There are two execution modes: * **Direct execution** — the caller is the signer. The API's bridge `userStep` uses this mode. * **Signature-based execution** — a third party submits a pre-signed EIP-712 payload on behalf of the signer. Includes an expiration timestamp and per-signer nonce for replay protection. When a call in the batch targets TransferDelegate, LZMulticall validates that the `from` address in the calldata matches the signer. One user can't move another user's tokens through the Delegate, even within the same batch. The contract also has a `sweep()` function that recovers any ETH or tokens left over after execution. LZMulticall is also the fee wrapper. Inside each `execute()` call, the batch deducts fees from the transfer amount and sends them to the Treasury before running the bridge. See [Fees](#fees) for details. **Never approve the LZMulticall (Wrapper) contract as a token spender** LZMulticall can forward arbitrary calls to any contract. If you approve it as a spender on any token, anyone can drain those tokens. Only approve the **TransferDelegate** as the spender. ## Treasury Treasury collects fees for cross-chain transfers using basis points on native currency. ``` getFees(bool payInZro, uint256 relayerFee, uint256 oracleFee) ``` You don't interact with Treasury directly. The API calculates fees and includes the amounts in the bridge transaction's `value` field. ## Fees LZMulticall is also the fee wrapper. Inside `execute()`, the batched calls deduct fees and send them to the Treasury before bridging the rest. The API builds this into the bridge `userStep` for you. ### Fee types Up to three fees can apply per transfer: | Fee | Type | Description | | ---------------- | ---------- | ----------------------------------------------------------------------- | | Base Fee | Percentage | A percentage of the transfer amount. | | Partner Fee | Percentage | A partner commission, configured per API key. | | CCTP Receive Fee | Fixed | A flat fee on CCTP (Circle) routes for destination-chain receive costs. | Zero-amount fees are omitted. The total is subtracted from the source amount before bridging, so the destination amount is the post-fee value. ### How fees flow through the contracts The fee mechanism depends on the token type: **Native tokens (e.g., ETH):** 1. Call `LZMulticall.execute()` with `msg.value` set to the fee plus the bridge cost. 2. Inside the batch, one call sends the fee to the Treasury. 3. The remaining calls run the bridge. **ERC20 tokens (e.g., USDC):** 1. Approve TransferDelegate for the full source amount (fees included). 2. Call `LZMulticall.execute()`. 3. Inside the batch, TransferDelegate pulls the full token amount from the user to the Wrapper. 4. A transfer sends the fee portion from the Wrapper to Treasury. 5. The remaining calls bridge the post-fee amount. ### Tracking Each `execute()` call includes a `quoteId`. On confirmation, LZMulticall emits an `Executed(signer, quoteId, nonce)` event. The API uses this event to attribute the transfer to the API key that requested the quote, linking volume and fee data back to each integration. ## How the contracts work together A typical ERC20 cross-chain transfer: 1. You request a quote. The API returns two `userSteps`: approve and bridge. 2. The **approve step** calls the ERC20 token's `approve()` with TransferDelegate as spender. 3. The **bridge step** calls LZMulticall's `execute()` on the source chain. 4. Inside that execution, LZMulticall calls `TransferDelegate.delegateTransferFrom()` to pull the full token amount (fees included) from your wallet to the Wrapper. 5. LZMulticall sends the fee portion to Treasury and bridges the remaining amount. | Step | `transaction.encoded.to` | What it does | | ------- | --------------------------------- | ----------------------------------------- | | Approve | ERC20 token contract (e.g., USDC) | Calls `approve(TransferDelegate, amount)` | | Bridge | LZMulticall contract | Routes the bridge through LayerZero | ## Contract addresses Each chain has its own Wrapper and Delegate deployments. See [Contract Addresses](/v2/developers/value-transfer-api/contracts/addresses) for the full list. ## Related * [Executing userSteps safely](/v2/developers/value-transfer-api/api-reference/quotes#executing-usersteps-safely) * [EVM Example](/v2/developers/value-transfer-api/examples/evm) # EVM Integration Source: https://docs.layerzero.network/v2/developers/value-transfer-api/examples/evm Complete TypeScript example for cross-chain EVM transfers using the Value Transfer API. This example demonstrates a complete cross-chain transfer from Base to Optimism using TypeScript and viem. ## Prerequisites * Node.js 18+ * An API key from LayerZero * A funded wallet on Base (source chain) ## Installation ```bash wrap theme={null} pnpm add viem dotenv ``` ```bash wrap theme={null} npm install viem dotenv ``` ```bash wrap theme={null} yarn add viem dotenv ``` ## Environment setup Create a `.env` file in your project root: ```bash wrap theme={null} VT_API_KEY=your_api_key_here EVM_PRIVATE_KEY=0xyour_private_key_here ``` *** Before requesting a quote, verify that the destination token is reachable from your source token. Query the tokens endpoint with filters to check if your destination exists in the list of transferrable tokens: ```typescript wrap theme={null} const response = await fetch( 'https://transfer.layerzero-api.com/v1/tokens?' + new URLSearchParams({ transferrableFromChainKey: 'base', transferrableFromTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', }), ); const {tokens} = await response.json(); // Check if destination token exists in reachable tokens const isSupported = tokens.some( (t) => t.chainKey === 'optimism' && t.address === '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', ); if (!isSupported) { throw new Error('Transfer path not supported'); } console.log(`Found ${tokens.length} reachable destinations`); ``` **Why validate first?** Validating the transfer path before requesting quotes prevents unnecessary API calls and provides immediate feedback if a route doesn't exist. Request a quote for your cross-chain transfer. The API returns available routes with fees, estimated duration, and the steps needed to execute. ```typescript wrap theme={null} const quoteResponse = await fetch('https://transfer.layerzero-api.com/v1/quotes', { method: 'POST', headers: { 'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ srcChainKey: 'base', dstChainKey: 'optimism', srcTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', dstTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', srcWalletAddress: '0xYourWallet', dstWalletAddress: '0xYourWallet', amount: '100000000000000', // 0.0001 ETH in wei options: { amountType: 'EXACT_SRC_AMOUNT', feeTolerance: {type: 'PERCENT', amount: 2}, }, }), }); const {quotes} = await quoteResponse.json(); const quote = quotes[0]; console.log('Quote ID:', quote.id); console.log('Route:', quote.routeSteps[0].type); console.log('Fee:', quote.feeUsd, 'USD'); ``` **Quote response structure:** For native token transfers (like ETH), the response contains a single bridge step. For ERC20 transfers (like USDC), the response contains two steps — an approve followed by a bridge. See [Executing userSteps safely](/v2/developers/value-transfer-api/api-reference/quotes#executing-usersteps-safely) for details on the two-step flow. ```typescript wrap theme={null} { id: '0x0000...7a', // hex quote ID routeSteps: [ { type: 'STARGATE_V2_TAXI', srcChainKey: 'base', description: 'Stargate', }, ], feeUsd: '0.42', feePercent: '0.42', srcAmount: '100000000000000', dstAmount: '99580000000000', duration: {estimated: '60000'}, userSteps: [ // Native transfers: single bridge step // ERC20 transfers: approve step + bridge step (execute both in order) { type: 'TRANSACTION', description: 'bridge', chainKey: 'base', chainType: 'EVM', signerAddress: '0xYourWallet', transaction: { encoded: { chainId: 8453, data: '0xc7c7f5b3...', from: '0xYourWallet', to: '0x27a16dc786820B16E5c9028b75B99F6f604b5d26', value: '100024887844265667', }, }, }, ], } ``` Process each user step in the quote. The quote contains an array of steps—execute them in order. **Never approve the LZMulticall (Wrapper) as a token spender** LZMulticall executes bridge transactions. It is not the right spender, and approving it will lose you tokens. The correct spender is the **TransferDelegate**, and the API's approve step already has this set in the calldata. Execute every `userStep` as returned. See [Contracts Overview](/v2/developers/value-transfer-api/contracts/overview) for details on the contract architecture. ```typescript wrap theme={null} import {createWalletClient, createPublicClient, http} from 'viem'; import {privateKeyToAccount} from 'viem/accounts'; import {base} from 'viem/chains'; const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY); const wallet = createWalletClient({account, chain: base, transport: http()}); const client = createPublicClient({chain: base, transport: http()}); let txHash; for (const step of quote.userSteps) { if (step.type === 'TRANSACTION') { const tx = step.transaction.encoded; txHash = await wallet.sendTransaction({ account, to: tx.to, data: tx.data, value: BigInt(tx.value ?? '0'), }); await client.waitForTransactionReceipt({hash: txHash}); console.log('Transaction sent:', txHash); } else if (step.type === 'SIGNATURE') { const typed = step.signature.typedData; const signature = await wallet.signTypedData({ account, domain: typed.domain, types: typed.types, primaryType: typed.primaryType, message: typed.message, }); await fetch('https://transfer.layerzero-api.com/v1/submit-signature', { method: 'POST', headers: { 'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ quoteId: quote.id, signatures: [signature], }), }); console.log('Signature submitted'); } } ``` **Step types:** Most EVM routes use `TRANSACTION` steps. Intent-based routes (like Aori) may include `SIGNATURE` steps for EIP-712 signed messages. Poll the status endpoint until the transfer completes. The API returns the current status and explorer link. ```typescript wrap theme={null} async function pollStatus(quoteId: string, txHash?: string) { const deadline = Date.now() + 5 * 60_000; // 5 minute timeout while (Date.now() < deadline) { const query = txHash ? `?txHash=${txHash}` : ''; const response = await fetch( `https://transfer.layerzero-api.com/v1/status/${encodeURIComponent(quoteId)}${query}`, {headers: {'x-api-key': 'YOUR_API_KEY'}}, ); const {status, explorerUrl} = await response.json(); console.log('Status:', status); if (status === 'SUCCEEDED') { console.log('Transfer complete!'); console.log('Explorer:', explorerUrl); return status; } if (status === 'FAILED' || status === 'UNKNOWN') { throw new Error(`Transfer ${status.toLowerCase()}`); } await new Promise((r) => setTimeout(r, 4000)); } throw new Error('Transfer timed out'); } await pollStatus(quote.id, txHash); ``` **Status values:** | Status | Description | | ------------ | -------------------------------------------- | | `PENDING` | Transfer initiated, waiting for confirmation | | `PROCESSING` | Transfer in progress across chains | | `SUCCEEDED` | Transfer completed successfully | | `FAILED` | Transfer failed | | `UNKNOWN` | Status cannot be determined | *** ## Complete example ```typescript wrap theme={null} import {createWalletClient, createPublicClient, http, type Hex} from 'viem'; import {privateKeyToAccount} from 'viem/accounts'; import {base} from 'viem/chains'; import * as dotenv from 'dotenv'; dotenv.config(); const API = 'https://transfer.layerzero-api.com/v1'; const API_KEY = process.env.VT_API_KEY!; const PRIVATE_KEY = process.env.EVM_PRIVATE_KEY as Hex; const account = privateKeyToAccount(PRIVATE_KEY); const wallet = createWalletClient({account, chain: base, transport: http()}); const client = createPublicClient({chain: base, transport: http()}); async function main() { // Step 1: Validate transfer path const tokensRes = await fetch( `${API}/tokens?` + new URLSearchParams({ transferrableFromChainKey: 'base', transferrableFromTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', }), ); const {tokens} = await tokensRes.json(); const isSupported = tokens.some( (t) => t.chainKey === 'optimism' && t.address === '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', ); if (!isSupported) throw new Error('Transfer path not supported'); console.log(`Found ${tokens.length} reachable destinations`); // Step 2: Get quote const quoteRes = await fetch(`${API}/quotes`, { method: 'POST', headers: {'x-api-key': API_KEY, 'Content-Type': 'application/json'}, body: JSON.stringify({ srcChainKey: 'base', dstChainKey: 'optimism', srcTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', dstTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', srcWalletAddress: account.address, dstWalletAddress: account.address, amount: '100000000000000', options: {amountType: 'EXACT_SRC_AMOUNT', feeTolerance: {type: 'PERCENT', amount: 2}}, }), }); const {quotes} = await quoteRes.json(); const quote = quotes[0]; if (!quote) throw new Error('No quote available'); console.log('Quote received:', quote.id); console.log('Fee:', quote.feeUsd, 'USD'); // Step 3: Execute user steps let txHash: Hex | undefined; for (const step of quote.userSteps) { if (step.type === 'SIGNATURE') { const typed = step.signature.typedData; const signature = await wallet.signTypedData({ account, domain: typed.domain, types: typed.types, primaryType: typed.primaryType, message: typed.message, }); await fetch(`${API}/submit-signature`, { method: 'POST', headers: {'x-api-key': API_KEY, 'Content-Type': 'application/json'}, body: JSON.stringify({quoteId: quote.id, signatures: [signature]}), }); console.log('Signature submitted'); } else if (step.type === 'TRANSACTION') { const tx = step.transaction.encoded; txHash = await wallet.sendTransaction({ account, to: tx.to, data: tx.data, value: BigInt(tx.value ?? '0'), }); await client.waitForTransactionReceipt({hash: txHash}); console.log('Transaction sent:', txHash); } } // Step 4: Poll status const deadline = Date.now() + 5 * 60_000; while (Date.now() < deadline) { const statusRes = await fetch( `${API}/status/${encodeURIComponent(quote.id)}${txHash ? `?txHash=${txHash}` : ''}`, {headers: {'x-api-key': API_KEY}}, ); const {status, explorerUrl} = await statusRes.json(); console.log('Status:', status); if (status === 'SUCCEEDED') { console.log('Explorer:', explorerUrl); break; } if (status === 'FAILED' || status === 'UNKNOWN') throw new Error(`Transfer ${status.toLowerCase()}`); await new Promise((r) => setTimeout(r, 4000)); } } main().catch(console.error); ``` ## Next steps * [Solana Example](/v2/developers/value-transfer-api/examples/solana) — Transfer tokens from Solana * [API Reference](/v2/developers/value-transfer-api/api-reference/overview) — Explore all endpoints # Solana Integration Source: https://docs.layerzero.network/v2/developers/value-transfer-api/examples/solana Complete TypeScript example for cross-chain Solana transfers using the Value Transfer API. This example demonstrates a complete cross-chain transfer from Solana to Arbitrum using TypeScript and the Solana Web3.js library. ## Prerequisites * Node.js 18+ * An API key from LayerZero * A funded Solana wallet with the token you want to transfer ## Installation ```bash wrap theme={null} pnpm add @solana/web3.js bs58 dotenv ``` ```bash wrap theme={null} npm install @solana/web3.js bs58 dotenv ``` ```bash wrap theme={null} yarn add @solana/web3.js bs58 dotenv ``` ## Environment setup Create a `.env` file in your project root: ```bash wrap theme={null} VT_API_KEY=your_api_key_here SOLANA_PRIVATE_KEY=your_base58_private_key_here ``` *** Before requesting a quote, verify that the destination token is reachable from your source token. Query the tokens endpoint with filters to check if your destination exists in the list of transferrable tokens: ```typescript wrap theme={null} const response = await fetch( 'https://transfer.layerzero-api.com/v1/tokens?' + new URLSearchParams({ transferrableFromChainKey: 'solana', transferrableFromTokenAddress: 'CAW777xcHVTQZ4CRwVQGB8CV1BVKPm5bNVxFJHWFKiH8', }), ); const {tokens} = await response.json(); // Check if destination token exists in reachable tokens const isSupported = tokens.some( (t) => t.chainKey === 'arbitrum' && t.address === '0x16f1967565aaD72DD77588a332CE445e7cEF752b', ); if (!isSupported) { throw new Error('Transfer path not supported'); } console.log(`Found ${tokens.length} reachable destinations`); ``` **Why validate first?** Validating the transfer path before requesting quotes prevents unnecessary API calls and provides immediate feedback if a route doesn't exist. Request a quote for your cross-chain transfer. The API returns available routes with fees, estimated duration, and quote ID. ```typescript wrap theme={null} const quoteResponse = await fetch('https://transfer.layerzero-api.com/v1/quotes', { method: 'POST', headers: { 'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ srcChainKey: 'solana', dstChainKey: 'arbitrum', srcTokenAddress: 'CAW777xcHVTQZ4CRwVQGB8CV1BVKPm5bNVxFJHWFKiH8', dstTokenAddress: '0x16f1967565aaD72DD77588a332CE445e7cEF752b', srcWalletAddress: 'YourSolanaPublicKey', dstWalletAddress: '0xYourEVMWallet', amount: '1000000000000', options: { amountType: 'EXACT_SRC_AMOUNT', feeTolerance: {type: 'PERCENT', amount: 2}, }, }), }); const {quotes} = await quoteResponse.json(); const quote = quotes[0]; console.log('Quote ID:', quote.id); console.log('Route:', quote.routeSteps[0].type); console.log('Fee:', quote.feeUsd, 'USD'); ``` **Quote response structure:** ```typescript wrap theme={null} { id: '0x0000...7a', // hex quote ID routeSteps: [ { type: 'OFT_V2', srcChainKey: 'solana', description: 'Transfer OFT from Solana to Arbitrum', }, ], feeUsd: '0.15', feePercent: '0.015', srcAmount: '1000000000000', dstAmount: '999850000000', duration: {estimated: '120000'}, } ``` For Solana transfers, call `/build-user-steps` to get fresh transaction data, then sign and submit the transaction. ### Build user steps Solana transactions have short blockhash validity, so you must generate transaction data immediately before signing: ```typescript wrap theme={null} const stepsResponse = await fetch('https://transfer.layerzero-api.com/v1/build-user-steps', { method: 'POST', headers: { 'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({quoteId: quote.id}), }); const {userSteps} = await stepsResponse.json(); ``` **User steps response:** ```typescript wrap theme={null} [ { type: 'TRANSACTION', chainKey: 'solana', chainType: 'SOLANA', description: 'Send OFT tokens via LayerZero', signerAddress: 'YourSolanaPublicKey', transaction: { encoded: { encoding: 'base64', data: 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAf...', }, }, }, ]; ``` ### Execute the transaction Deserialize, sign, and submit the transaction using Solana Web3.js: ```typescript wrap theme={null} import * as web3 from '@solana/web3.js'; import bs58 from 'bs58'; const connection = new web3.Connection(web3.clusterApiUrl('mainnet-beta'), 'confirmed'); // Parse private key (supports hex or base58) function parseSolanaSecretKey(raw: string): Uint8Array { const isHex = /^0x[0-9a-fA-F]+$/.test(raw) || /^[0-9a-fA-F]+$/.test(raw); if (isHex) { const hex = raw.replace(/^0x/, ''); return new Uint8Array(Buffer.from(hex, 'hex')); } return bs58.decode(raw); } const keypair = web3.Keypair.fromSecretKey(parseSolanaSecretKey(process.env.SOLANA_PRIVATE_KEY!)); let lastSignature; for (const step of userSteps) { if (step.type !== 'TRANSACTION') continue; const tx = step.transaction.encoded; if (tx.encoding !== 'base64') continue; // Deserialize the transaction const raw = Buffer.from(tx.data, 'base64'); const vtx = web3.VersionedTransaction.deserialize(new Uint8Array(raw)); // Sign the transaction vtx.sign([keypair]); // Send the transaction lastSignature = await connection.sendTransaction(vtx); console.log('Transaction sent:', lastSignature); // Wait for confirmation const latest = await connection.getLatestBlockhash(); await connection.confirmTransaction({ signature: lastSignature, blockhash: latest.blockhash, lastValidBlockHeight: latest.lastValidBlockHeight, }); console.log('Transaction confirmed'); } ``` **Solana requires build-user-steps:** Unlike EVM chains where `userSteps` are included in the quote response, Solana requires calling `/build-user-steps` to generate fresh transaction data with a valid blockhash. Poll the status endpoint until the transfer completes. The API returns the current status and explorer link. ```typescript wrap theme={null} async function pollStatus(quoteId: string, txSignature?: string) { const deadline = Date.now() + 5 * 60_000; // 5 minute timeout while (Date.now() < deadline) { const query = txSignature ? `?txHash=${encodeURIComponent(txSignature)}` : ''; const response = await fetch( `https://transfer.layerzero-api.com/v1/status/${encodeURIComponent(quoteId)}${query}`, {headers: {'x-api-key': 'YOUR_API_KEY'}}, ); const {status, explorerUrl} = await response.json(); console.log('Status:', status); if (status === 'SUCCEEDED') { console.log('Transfer complete!'); console.log('Explorer:', explorerUrl); return status; } if (status === 'FAILED' || status === 'UNKNOWN') { throw new Error(`Transfer ${status.toLowerCase()}`); } await new Promise((r) => setTimeout(r, 4000)); } throw new Error('Transfer timed out'); } await pollStatus(quote.id, lastSignature); ``` **Status values:** | Status | Description | | ------------ | -------------------------------------------- | | `PENDING` | Transfer initiated, waiting for confirmation | | `PROCESSING` | Transfer in progress across chains | | `SUCCEEDED` | Transfer completed successfully | | `FAILED` | Transfer failed | | `UNKNOWN` | Status cannot be determined | *** ## Complete example ```typescript wrap theme={null} import * as web3 from '@solana/web3.js'; import bs58 from 'bs58'; import * as dotenv from 'dotenv'; dotenv.config(); const API = 'https://transfer.layerzero-api.com/v1'; const API_KEY = process.env.VT_API_KEY!; const PRIVATE_KEY = process.env.SOLANA_PRIVATE_KEY!; const connection = new web3.Connection(web3.clusterApiUrl('mainnet-beta'), 'confirmed'); function parseSolanaSecretKey(raw: string): Uint8Array { const isHex = /^0x[0-9a-fA-F]+$/.test(raw) || /^[0-9a-fA-F]+$/.test(raw); if (isHex) { const hex = raw.replace(/^0x/, ''); return new Uint8Array(Buffer.from(hex, 'hex')); } return bs58.decode(raw); } const keypair = web3.Keypair.fromSecretKey(parseSolanaSecretKey(PRIVATE_KEY)); async function main() { // Step 1: Validate transfer path const tokensRes = await fetch( `${API}/tokens?` + new URLSearchParams({ transferrableFromChainKey: 'solana', transferrableFromTokenAddress: 'CAW777xcHVTQZ4CRwVQGB8CV1BVKPm5bNVxFJHWFKiH8', }), ); const {tokens} = await tokensRes.json(); const isSupported = tokens.some( (t) => t.chainKey === 'arbitrum' && t.address === '0x16f1967565aaD72DD77588a332CE445e7cEF752b', ); if (!isSupported) throw new Error('Transfer path not supported'); console.log(`Found ${tokens.length} reachable destinations`); // Step 2: Get quote const quoteRes = await fetch(`${API}/quotes`, { method: 'POST', headers: {'x-api-key': API_KEY, 'Content-Type': 'application/json'}, body: JSON.stringify({ srcChainKey: 'solana', dstChainKey: 'arbitrum', srcTokenAddress: 'CAW777xcHVTQZ4CRwVQGB8CV1BVKPm5bNVxFJHWFKiH8', dstTokenAddress: '0x16f1967565aaD72DD77588a332CE445e7cEF752b', srcWalletAddress: keypair.publicKey.toBase58(), dstWalletAddress: '0x6d9798053f498451bec79c0397f7f95b079bdcd6', amount: '1000000000000', options: {amountType: 'EXACT_SRC_AMOUNT', feeTolerance: {type: 'PERCENT', amount: 2}}, }), }); const {quotes} = await quoteRes.json(); const quote = quotes[0]; if (!quote) throw new Error('No quote available'); console.log('Quote received:', quote.id); // Step 3: Build user steps (required for Solana) const stepsRes = await fetch(`${API}/build-user-steps`, { method: 'POST', headers: {'x-api-key': API_KEY, 'Content-Type': 'application/json'}, body: JSON.stringify({quoteId: quote.id}), }); const {userSteps} = await stepsRes.json(); // Execute transactions let lastSignature: string | undefined; for (const step of userSteps) { if (step.type !== 'TRANSACTION') continue; const tx = step.transaction.encoded; if (tx.encoding !== 'base64') continue; const raw = Buffer.from(tx.data, 'base64'); const vtx = web3.VersionedTransaction.deserialize(new Uint8Array(raw)); vtx.sign([keypair]); lastSignature = await connection.sendTransaction(vtx); console.log('Transaction sent:', lastSignature); const latest = await connection.getLatestBlockhash(); await connection.confirmTransaction({ signature: lastSignature, blockhash: latest.blockhash, lastValidBlockHeight: latest.lastValidBlockHeight, }); } // Step 4: Poll status const deadline = Date.now() + 5 * 60_000; while (Date.now() < deadline) { const query = lastSignature ? `?txHash=${encodeURIComponent(lastSignature)}` : ''; const statusRes = await fetch(`${API}/status/${encodeURIComponent(quote.id)}${query}`, { headers: {'x-api-key': API_KEY}, }); const {status, explorerUrl} = await statusRes.json(); console.log('Status:', status); if (status === 'SUCCEEDED') { console.log('Explorer:', explorerUrl); break; } if (status === 'FAILED' || status === 'UNKNOWN') throw new Error(`Transfer ${status.toLowerCase()}`); await new Promise((r) => setTimeout(r, 4000)); } } main().catch(console.error); ``` ## Key differences from EVM | Aspect | EVM | Solana | | ------------------ | ------------------------------- | ------------------------------------ | | Transaction format | JSON with `to`, `data`, `value` | Base64-encoded versioned transaction | | User steps | Included in quote response | Requires `/build-user-steps` call | | Signing | EIP-191 or EIP-712 | Ed25519 | | Confirmation | `waitForTransactionReceipt` | `confirmTransaction` with blockhash | ## Next steps * [EVM Example](/v2/developers/value-transfer-api/examples/evm) — Transfer tokens between EVM chains * [API Reference](/v2/developers/value-transfer-api/api-reference/overview) — Explore all endpoints # Value Transfer API Source: https://docs.layerzero.network/v2/developers/value-transfer-api/overview Unified API for cross-chain value transfers across 150+ blockchains using LayerZero infrastructure. The Value Transfer API is the canonical interface for value movement in crypto. One API replaces multiple bridge and swap integrations, giving you access to OFTs, Stargate, Aori, CCTP, and multihop flows through a single endpoint. ## The problem Moving value across blockchains today is fragmented. Developers integrate multiple bridges, swap protocols, and settlement systems to offer users seamless cross-chain experiences. This creates friction for wallets, DeFi apps, exchanges, and fintechs that need a single, predictable way to move assets anywhere in crypto. LayerZero already powers much of this infrastructure through OFTs, Stargate, and Aori. The Value Transfer API abstracts that complexity into one programmable interface to move value of any kind—stablecoins, OFTs, native tokens, wrapped assets—across any chain. ## What the API provides * A single integration point that replaces multiple bridge and swap connections. Access 450+ tokens across 150+ chains without managing separate integrations for OFT, Stargate, CCTP, or Aori. * Production-ready infrastructure with a quote-then-execute pattern, comprehensive error handling, and end-to-end status tracking. * Unified routing that evaluates all available paths - OFT transfers, Stargate pools, CCTP, intent-based swaps and returns optimal routes based on speed, cost, and liquidity. ## How it works The API follows a quote-then-execute pattern: ```mermaid wrap theme={null} sequenceDiagram participant App participant VTAPI as VT API participant Blockchain App->>VTAPI: POST /quotes VTAPI-->>App: Quote with userSteps App->>App: Sign transaction/message App->>Blockchain: Submit transaction App->>VTAPI: GET /status VTAPI-->>App: SUCCEEDED ``` 1. **Request a quote**: Submit source and destination details to receive available transfer routes 2. **Execute user steps**: Sign and submit the transactions or signatures returned in the quote 3. **Track status**: Poll the status endpoint until the transfer completes ## Supported route types The API automatically selects the optimal route based on your transfer parameters: | Type | Description | | ------------------ | ----------------------------- | | `OFT` | OFT Standard transfers | | `STARGATE_V2_TAXI` | Stargate V2 instant transfers | | `STARGATE_V2_BUS` | Stargate V2 batched transfers | | `CCTP` | Circle CCTP for native USDC | | `AORI` | Intent-based swaps via Aori | ### Learn more about route protocols * **[Stargate](/v2/developers/evm/stargate/overview)**: Unified liquidity pools enabling native asset transfers with instant guaranteed finality * **[OFT (Omnichain Fungible Token)](/v2/developers/evm/oft/quickstart)**: Token standard for seamless cross-chain token transfers without wrapped assets * **[CCTP (Cross-Chain Transfer Protocol)](https://developers.circle.com/stablecoins/cctp-getting-started)**: Circle's native USDC cross-chain transfer protocol with burn-and-mint mechanism * **[Aori](https://docs.aori.io/)**: Intent-based trading protocol enabling optimal cross-chain swaps through a decentralized order book ## Key capabilities * **Transparent pricing**: Upfront quotes with fees, slippage, and USD valuations before execution * **End-to-end tracking**: Monitor transfer status from initiation to delivery * **Multi-VM support**: Transfer between EVM, Solana, Aptos, TON, and other chains * **Source-chain gas only**: Users pay gas once; destination chain fees are handled through LayerZero messaging * **Intelligent routing**: The API evaluates transfer speed, fees, and liquidity to return optimal routes ## Use cases ### Wallets Embed cross-chain swaps and bridges without managing multiple integrations. Provide users with transparent fee breakdowns and real-time transfer tracking. ### Exchanges and on/off ramps Accept deposits from any chain and credit users on your settlement chain. Enable cross-chain withdrawals without manual reconciliation. ### DeFi applications Enable one-click deposits from any chain into your protocol. Abstract away the complexity of liquidity fragmentation. ### Fintechs and neobanks Build crypto transfer features using a single, well-documented API. Monetize transfers with configurable fee structures. ## Next steps * **[Quickstart](/v2/developers/value-transfer-api/quickstart)**: Make your first API request in minutes * **[Contracts](/v2/developers/value-transfer-api/contracts/overview)**: How the Wrapper and Delegate contracts handle bridge execution and token approvals * **[API Reference](/v2/developers/value-transfer-api/api-reference/overview)**: Explore all endpoints with interactive testing # Quickstart Source: https://docs.layerzero.network/v2/developers/value-transfer-api/quickstart Execute your first cross-chain transfer using the Value Transfer API. Get started with the Value Transfer API in minutes. This guide walks you through the complete transfer flow. ## Prerequisites * An API key from LayerZero * A funded wallet on the source chain ## Base URL ``` https://transfer.layerzero-api.com/v1 ``` *** Before requesting a quote, verify your source and destination chains are supported and discover available tokens. ### List supported chains ```bash wrap theme={null} curl -X GET "https://transfer.layerzero-api.com/v1/chains" ``` ```typescript wrap theme={null} const response = await fetch('https://transfer.layerzero-api.com/v1/chains'); const {chains} = await response.json(); console.log(chains); ``` ```python wrap theme={null} import requests response = requests.get("https://transfer.layerzero-api.com/v1/chains") chains = response.json()["chains"] print(chains) ``` #### Response | Field | Description | | ----------- | --------------------------------------------------------- | | `chainKey` | Unique chain identifier (e.g., `ethereum`, `base`) | | `chainType` | Blockchain type (`EVM`, `SOLANA`, `STARKNET`) | | `chainId` | Native chain ID (e.g., `1` for Ethereum, `8453` for Base) | See the [Chains API reference](/v2/developers/value-transfer-api/api-reference/chains) for complete endpoint documentation. ### List supported tokens Query tokens with optional filters. Without parameters, the API returns all tokens across all supported chains. ```bash wrap theme={null} # Get all tokens (full catalog) curl -X GET "https://transfer.layerzero-api.com/v1/tokens" # Get valid destinations for ETH from Base curl -X GET "https://transfer.layerzero-api.com/v1/tokens?transferrableFromChainKey=&transferrableFromTokenAddress=&pagination%5BnextToken%5D=" ``` ```typescript wrap theme={null} // Get tokens transferrable from Base ETH const response = await fetch( 'https://transfer.layerzero-api.com/v1/tokens?' + new URLSearchParams({ transferrableFromChainKey: 'base', transferrableFromTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', }), ); const {tokens} = await response.json(); console.log(tokens); ``` ```python wrap theme={null} import requests # Get tokens transferrable from Base ETH response = requests.get( "https://transfer.layerzero-api.com/v1/tokens", params={ "transferrableFromChainKey": "base", "transferrableFromTokenAddress": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", }, ) tokens = response.json()["tokens"] print(tokens) ``` #### Query parameters | Parameter | Type | Description | | ------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------- | | `transferrableFromChainKey` | string | Source chain key (e.g., `base`, `ethereum`). **Must be combined** with `transferrableFromTokenAddress` to filter results. | | `transferrableFromTokenAddress` | string | Source token address. **Must be combined** with `transferrableFromChainKey` to filter results. | **Valid destinations:** To get all destination tokens you can transfer to, provide **both** the source chain and token address. See [Tokens](/v2/developers/value-transfer-api/api-reference/tokens) for the full endpoint reference. #### Response | Field | Description | | ------------- | -------------------------------------------- | | `isSupported` | Whether the token is available for transfers | | `chainKey` | Chain identifier (e.g., `ethereum`, `base`) | | `address` | Token contract address | | `decimals` | Token decimal places | | `symbol` | Token symbol (e.g., `ETH`, `USDC`) | | `name` | Full token name | | `price.usd` | Current price in USD | Request a quote for your cross-chain transfer. The API returns available routes with fees, estimated duration, and the steps needed to execute. **Quote response:** The quote includes `userSteps` with transaction or signature data to execute. See [Quotes](/v2/developers/value-transfer-api/api-reference/quotes) for the full request/response schema. ```bash wrap theme={null} curl -X POST "https://transfer.layerzero-api.com/v1/quotes" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "srcChainKey": "base", "dstChainKey": "optimism", "srcTokenAddress": "<0xTOKEN_ADDRESS>", "dstTokenAddress": "<0xTOKEN_ADDRESS>", "srcWalletAddress": "<0xYOUR_WALLET>", "dstWalletAddress": "<0xYOUR_WALLET>", "amount": "", "options": { "amountType": "EXACT_SRC_AMOUNT", "feeTolerance": { "type": "PERCENT", "amount": 2 } } }' ``` ```typescript wrap theme={null} const response = await fetch('https://transfer.layerzero-api.com/v1/quotes', { method: 'POST', headers: { 'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ srcChainKey: 'base', dstChainKey: 'optimism', srcTokenAddress: '<0xTOKEN_ADDRESS>', dstTokenAddress: '<0xTOKEN_ADDRESS>', srcWalletAddress: '<0xYOUR_WALLET>', dstWalletAddress: '<0xYOUR_WALLET>', amount: '', // 0.0001 ETH in wei options: { amountType: 'EXACT_SRC_AMOUNT', feeTolerance: {type: 'PERCENT', amount: 2}, }, }), }); const {quotes} = await response.json(); const quote = quotes[0]; console.log('Quote ID:', quote.id); console.log('Fee:', quote.feeUsd, 'USD'); ``` ```python wrap theme={null} import requests response = requests.post( "https://transfer.layerzero-api.com/v1/quotes", headers={ "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json", }, json={ "srcChainKey": "base", "dstChainKey": "optimism", "srcTokenAddress": "<0xTOKEN_ADDRESS>", "dstTokenAddress": "<0xTOKEN_ADDRESS>", "srcWalletAddress": "<0xYOUR_WALLET>", "dstWalletAddress": "<0xYOUR_WALLET>", "amount": "", # 0.0001 ETH in wei "options": { "amountType": "EXACT_SRC_AMOUNT", "feeTolerance": {"type": "PERCENT", "amount": 2}, }, }, ) quote = response.json()["quotes"][0] print(f"Quote ID: {quote['id']}") print(f"Fee: {quote.get('feeUsd', 'N/A')} USD") ``` For **EVM transfers**, the quote response already includes `userSteps` with transaction data - skip to Step 4. For **Solana transfers**, you must call `/build-user-steps` to get the encoded transaction data. Solana transactions have short blockhash validity (\~60 seconds), so this step generates fresh transaction data. **EVM vs Solana:** | Chain type | Where to get `userSteps` | | ---------- | ----------------------------------- | | **EVM** | Directly from the quote response | | **Solana** | Must call `/build-user-steps` first | See [Build User Steps](/v2/developers/value-transfer-api/api-reference/build-user-steps) for complete documentation. ```bash wrap theme={null} curl -X POST "https://transfer.layerzero-api.com/v1/build-user-steps" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"quoteId": "QUOTE_ID"}' ``` ```typescript wrap theme={null} const response = await fetch('https://transfer.layerzero-api.com/v1/build-user-steps', { method: 'POST', headers: { 'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({quoteId: 'QUOTE_ID'}), }); const {userSteps} = await response.json(); console.log('User steps:', userSteps); ``` ```python wrap theme={null} import requests response = requests.post( "https://transfer.layerzero-api.com/v1/build-user-steps", headers={ "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json", }, json={"quoteId": "QUOTE_ID"}, ) user_steps = response.json()["userSteps"] print("User steps:", user_steps) ``` Process each user step in order. For transactions, sign and submit to the blockchain. For signatures, sign and submit to the API. **Never approve the LZMulticall (Wrapper) as a token spender** LZMulticall executes bridge transactions. It is not the right spender, and approving it will lose you tokens. The correct spender is the **TransferDelegate**, and the API's approve step already has this set in the calldata. Execute every `userStep` as returned. See [Contracts Overview](/v2/developers/value-transfer-api/contracts/overview) for details on the contract architecture. **No cURL for execution:** Executing requires a **wallet** to sign and broadcast the transaction. This step cannot be done with cURL alone - you need a signing library (viem, web3.py, @solana/web3.js) and an RPC connection. **Where to get `userSteps`:** * **EVM**: Use `quote.userSteps` directly from the quote response * **Solana**: Use `userSteps` from the `/build-user-steps` response ### Execute transaction steps Loop through every `userStep` and execute each one in order. ERC20 transfers return two transaction steps (approve + bridge) — you must execute both. ```typescript wrap theme={null} import {createWalletClient, createPublicClient, http} from 'viem'; import {privateKeyToAccount} from 'viem/accounts'; import {base} from 'viem/chains'; const account = privateKeyToAccount('0xYOUR_PRIVATE_KEY'); const wallet = createWalletClient({account, chain: base, transport: http()}); const client = createPublicClient({chain: base, transport: http()}); let txHash; for (const step of userSteps) { if (step.type !== 'TRANSACTION') continue; const tx = step.transaction.encoded; txHash = await wallet.sendTransaction({ to: tx.to, data: tx.data, value: BigInt(tx.value ?? '0'), }); // Wait for confirmation before executing the next step await client.waitForTransactionReceipt({hash: txHash}); console.log(`${step.description} tx confirmed:`, txHash); } ``` ```python wrap theme={null} from web3 import Web3 w3 = Web3(Web3.HTTPProvider("https://mainnet.base.org")) account = w3.eth.account.from_key("YOUR_PRIVATE_KEY") tx_hash = None for step in user_steps: if step["type"] != "TRANSACTION": continue tx = step["transaction"]["encoded"] signed = account.sign_transaction( { "to": tx["to"], "data": tx["data"], "value": int(tx.get("value", 0)), "gas": 200000, "gasPrice": w3.eth.gas_price, "nonce": w3.eth.get_transaction_count(account.address), "chainId": tx["chainId"], } ) tx_hash = w3.eth.send_raw_transaction(signed.rawTransaction) w3.eth.wait_for_transaction_receipt(tx_hash) print(f"{step['description']} tx confirmed: {tx_hash.hex()}") ``` ### Submit a signature step For intent-based routes, sign the EIP-712 data and submit to the API. **Signature steps:** See [Submit Signature](/v2/developers/value-transfer-api/api-reference/submit-signature) for EIP-712 typed data handling and BigInt conversion requirements. ```typescript wrap theme={null} // Sign the typed data from userStep.signature.typedData const typed = userStep.signature.typedData; const signature = await wallet.signTypedData({ domain: typed.domain, types: typed.types, primaryType: typed.primaryType, message: typed.message, }); // Submit to API await fetch('https://transfer.layerzero-api.com/v1/submit-signature', { method: 'POST', headers: { 'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ quoteId: quote.id, signatures: [signature], }), }); ``` ```python wrap theme={null} from eth_account.messages import encode_typed_data # Sign the typed data from userStep["signature"]["typedData"] typed = user_step["signature"]["typedData"] signable = encode_typed_data(full_message=typed) signed = account.sign_message(signable) # Submit to API requests.post( "https://transfer.layerzero-api.com/v1/submit-signature", headers={ "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json", }, json={ "quoteId": quote["id"], "signatures": [signed.signature.hex()], }, ) ``` After executing all user steps, poll the status endpoint to monitor completion. **Polling:** Poll every 4 seconds until `status` is `SUCCEEDED` or `FAILED`. See [Status](/v2/developers/value-transfer-api/api-reference/status) for response details and error handling. ```bash wrap theme={null} curl -X GET "https://transfer.layerzero-api.com/v1/status/QUOTE_ID?txHash=0xYOUR_TX_HASH" \ -H "x-api-key: YOUR_API_KEY" ``` ```typescript wrap theme={null} async function pollStatus(quoteId: string, txHash?: string): Promise { const query = txHash ? `?txHash=${txHash}` : ''; while (true) { const response = await fetch( `https://transfer.layerzero-api.com/v1/status/${encodeURIComponent(quoteId)}${query}`, {headers: {'x-api-key': 'YOUR_API_KEY'}}, ); const {status, explorerUrl} = await response.json(); console.log('Status:', status); if (status === 'SUCCEEDED' || status === 'FAILED' || status === 'UNKNOWN') { console.log('Explorer:', explorerUrl); return status; } await new Promise((resolve) => setTimeout(resolve, 4000)); } } ``` ```python wrap theme={null} import time from urllib.parse import quote as url_quote import requests def poll_status(quote_id: str, tx_hash: str | None = None) -> str: query = f"?txHash={tx_hash}" if tx_hash else "" while True: response = requests.get( f"https://transfer.layerzero-api.com/v1/status/{url_quote(quote_id, safe='')}{query}", headers={"x-api-key": "YOUR_API_KEY"}, ) data = response.json() print(f"Status: {data['status']}") if data["status"] in ("SUCCEEDED", "FAILED", "UNKNOWN"): print(f"Explorer: {data.get('explorerUrl')}") return data["status"] time.sleep(4) ``` ### Status values | Status | Description | | ------------ | -------------------------------------------- | | `PENDING` | Transfer initiated, waiting for confirmation | | `PROCESSING` | Transfer in progress across chains | | `SUCCEEDED` | Transfer completed successfully | | `FAILED` | Transfer failed | | `UNKNOWN` | Status cannot be determined | *** ## Next steps * **[EVM Example](/v2/developers/value-transfer-api/examples/evm)**: Complete TypeScript example with viem * **[Solana Example](/v2/developers/value-transfer-api/examples/solana)**: Complete TypeScript example for Solana transfers * **[API Reference](/v2/developers/value-transfer-api/api-reference/overview)**: Explore all endpoints with interactive testing # Value Transfer API Source: https://docs.layerzero.network/v2/developers/value-transfer-api/start Single, unified interface for cross-chain value transfers across 150+ blockchains. The Value Transfer API provides a single, unified interface for cross-chain value transfers. Move tokens across 150+ blockchains using one API. Contact the LayerZero team to obtain an API key for production use. ## Getting Started Execute your first cross-chain transfer in minutes using the quote-then-execute pattern. Learn how the Value Transfer API works, supported routes, key benefits, and use cases. ## Examples Complete TypeScript example using viem for EVM-to-EVM cross-chain transfers. Complete TypeScript example for Solana cross-chain transfers with transaction building. ## Technical Reference How the Wrapper and Delegate contracts handle bridge execution and token approvals, plus safety guidelines. Explore all endpoints with interactive testing: chains, tokens, quotes, and more. # Frequently Asked Questions (FAQ) Source: https://docs.layerzero.network/v2/faq Frequently asked questions about LayerZero V2 protocol. Find answers to common development and integration questions. Crosschain development with LayerZero V2. ## General LayerZero is an omnichain messaging protocol — a permissionless, open framework designed to securely move information between blockchains. It empowers any application to bring its own security, execution, and crosschain interaction, providing a predictable and adaptable foundation for decentralized applications living on multiple networks. For details on how it works, see the [Protocol Overview](/v2/concepts/protocol/protocol-overview). The [devtools repository](https://github.com/LayerZero-Labs/devtools/tree/main) contains contract examples across all supported VMs. It acts as the central hub for the LayerZero developer experience, including application contract standards, CLI examples, packages, scripting tools, and more. The delivery time of a LayerZero message can be broken down into a series of processing stages. Every message goes through the following high-level steps: * Source Block Confirmations: The message waits for the source chain to finalize a specified number of block confirmations. To view the default configuration for a given pathway, refer to the [default configs checker](/v2/deployments/deployed-contracts). * DVN/Verification: Each Decentralized Verifier Network (DVN) submits one transaction to verify the message. * Committer/Commit Verification: One additional transaction is required to commit the verified message. * Executor/Message Execution: A final transaction is submitted to execute the message on the destination chain. Estimated Total Delivery Time You can estimate the total message delivery time with the following formula: `Total Time ≈ (sourceBlockTime × number of block confirmations) + (destinationBlockTime × (2 blocks + number of DVNs))` Note: This formula offers a rough estimate for a message that does not implement `lzCompose`. It assumes that each transaction is included in the next block without delay and does not factor in network latency, or other real-world conditions that may affect transaction processing time. **Chain ID** is an EVM-specific concept for the native identifier assigned by a blockchain network itself (for example, `1` for Ethereum Mainnet, `42161` for Arbitrum Mainnet). It’s used by node clients, wallets, and RPCs to identify the network. **Endpoint ID (EID)** is LayerZero’s internal identifier used by the protocol to route messages between chains. Every LayerZero Endpoint contract has a unique EID. EIDs are VM-agnostic and do not map 1:1 to chain IDs. Key points: * EIDs are what you use in LayerZero configs, CLI commands, and contract calls * Chain IDs are for EVM chains and used for RPCs/wallets; EIDs are for LayerZero messaging * For EIDs, Mainnets typically use the `30xxx` range; testnets use the `40xxx` range Where to find EIDs: see the list of deployed contracts and Endpoint IDs in [Deployed Contracts](/v2/deployments/deployed-contracts) **Owner** is defined on your OApp contract (OpenZeppelin `Ownable`). It controls OApp-level policy and calls `setPeer()`, `setEnforcedOptions()`, and `setDelegate()`. **Delegate** is registered inside the LayerZero Endpoint via `setDelegate()`. It is authorized to configure the protocol/security stack on the Endpoint — `setConfig()` (DVNs, Executor, confirmations), `setSendLibrary()`, `setReceiveLibrary()` — and to manage inbound messages (`skip()`, `nilify()`, `burn()`, `clear()`). The Owner can change the Delegate; the Delegate cannot change the Owner. ⚠️ **Configuration note for devtools:** the owner and delegate should typically be the same address. The `wire` task calls both owner-gated functions (`setPeer`, `setEnforcedOptions`) and delegate-gated Endpoint functions (`setConfig`). If the two roles are different addresses, the calls the signer isn't authorized for revert with `LZ_Unauthorized()`. Use the same address for both unless you have a specific reason to split them. For the full per-role permission table, see [Security and roles](/v2/concepts/technical-reference/oapp-reference#security-and-roles). **RBAC variant (Stablecoin OFT):** Some OApps replace OpenZeppelin `Ownable` with role-based access control (`AccessControl2StepUpgradeable`). There is no single owner — `DEFAULT_ADMIN_ROLE` takes the owner's place and gates `setPeer()`, `setEnforcedOptions()`, and `setMsgInspector()`. In this model `setDelegate()` always reverts: the delegate is permanently synced to the `DEFAULT_ADMIN_ROLE` holder and changes only through the two-step `beginDefaultAdminTransfer()` → `acceptDefaultAdminTransfer()` flow. See the [RBAC Reference](/v2/developers/evm/stablecoin-oft/rbac-reference). While it is possible to integrate existing tokens deployed across multiple chains with the OFT standard, this requires additional setup. Specifically, [MintBurnOFTAdapter](https://github.com/LayerZero-Labs/devtools/tree/main/examples/mint-burn-oft-adapter) — a variant of OFTAdapter.sol enables OFT functionality by calling the mint and burn methods of the innerToken on each chain. However, for this integration to work: * Each innerToken contract must expose externally callable `mint` and `burn` functions. * The innerToken must grant the MintBurnOFTAdapter the necessary permissions (`MINTER_ROLE` and `BURNER_ROLE`) to invoke these functions. The `sharedDecimals` is the "lowest common denominator" of decimal precision across all chains in the OFT system. It limits how many decimal places can be reliably represented when moving tokens cross‑chain. By default, `sharedDecimals` is set to 6, which is sufficient for most use cases across different VMs. However, OApps can override this default if the OApp's total token supply exceeds (2⁶⁴–1) / 10⁶. ⚠️ CAUTION: If you override the vanilla `sharedDecimals` amount or have an existing token supply exceeding 18,446,744,073,709.551615 tokens, extra caution should be applied to ensure `amountSD` and `amountLD` do not overflow. See more explanations [here](/v2/concepts/technical-reference/oft-reference). msgType 1 and msgType 2 are used to distinguish between two types of messages when setting `enforcedOptions`. Use the `OptionsBuilder` to add the message execution options for each msgType. msgType 1 = `SEND` This msgType is a basic token transfer or message send. It does not include a composed message. Options you can use: * [`lzReceive` Option](../v2/tools/sdks/options#lzreceive-option) to specify the gas values the Executor uses when calling `lzReceive` on the destination chain. * [`lzNativeDrop` Option](../v2/tools/sdks/options#lznativedrop-option) to specify how much native gas to drop to any address on the destination chain. msgType 2 = `SEND_AND_CALL` This msgType includes additional composed message(s), allowing for one or more extra calls to be sent along with the message. Options you can use: * [`lzReceive` Option](../v2/tools/sdks/options#lzreceive-option) to specify the gas values the Executor uses when calling `lzReceive` on the destination chain. * [`lzCompose` Option](../v2/tools/sdks/options#lzcompose-option) to allocate gas and value for Composed Messages on the destination chain. [See more information for composed message options](../v2/developers/evm/composer/overview#composed-message-execution-options). Please follow the [best practice](../v2/tools/sdks/options#best-practices) to determine the gas cost for `lzRecieve` and `lzCompose` option. Overestimating wastes funds; underestimating may cause message execution to fail. If you are using Sepolia as the destination chain, transaction costs can spike due to high and volatile gas prices on Sepolia. To avoid high fees during testing, it is recommended to use alternative testnets as the destination chain. LayerZero's transaction pricing model is designed to fairly distribute costs across the various components that enable secure, reliable crosschain messaging. Understanding this model helps developers and users make informed decisions about gas allocation and fee optimization. Learn more: [Transaction Pricing Model](/v2/concepts/protocol/transaction-pricing) Yes. An OApp's delegate can call the `skip()` method on the endpoint to stop delivery. The skip function should be used only in instances where either message verification fails or must be stopped, not message execution. LayerZero provides separate handling for retrying or removing messages that have successfully been verified, but fail to execute. Learn more: [Skip Message Guide](/v2/developers/evm/troubleshooting/debugging-messages#skipping-nonce) LZ Dead DVN Represents a [Dead Decentralized Verifier Network (DVN)](/v2/concepts/glossary#dead-dvn). These contracts are placeholders used when the default LayerZero config is inactive and will require the OApp owner to manually configure the contract's config to use the pathway. LayerZero allows anyone to permissionlessly run DVNs, but default providers (e.g. Google Cloud, Polyhedra) may not cover every chain immediately. If a pathway lacks default DVN support, OApps must explicitly configure supported DVNs on both the source and destination chains. To add a new network to your existing OFT deployment, you'll need to update your project configuration and deploy the OFT to the new chain: 1. Add the new network to your `hardhat.config.ts` with the correct LayerZero Endpoint ID and RPC URL 2. Deploy the OFT on the new network 3. Update your `layerzero.config.ts` to include the new contract and connections 4. Wire peers to apply configuration (If the current OFT deployment involves a non-EVM chain, follow the specific instructions for that VM) For detailed step-by-step instructions, see [Adding Networks](/v2/get-started/create-lz-oapp/adding-networks). ## Error & Troubleshooting This error indicates that your OApp configuration is missing the required DVN and/or Executor settings. On Mainnet, default DVNs are not guaranteed to be available for every pathway. OApps must explicitly configure supported DVNs on both the source and destination chains. DVN Addresses can be found [here](/v2/deployments/dvn-addresses). To simplify setup, use [simple config generator](/v2/tools/simple-config) , which provides CLI commands for setting DVNs. Be sure to also consult the VM-specific configuration sections to complete your configurations properly. Failures in `_quote()` often stem from: * **Incomplete Network Pathway**: Not all pathways are fully wired, especially on testnets. Contact LayerZero if you require support for a specific pathway. * **Missing [`enforcedOptions`](/v2/concepts/message-options#enforcing-options) or [`extraOptions`](/v2/concepts/message-options#extra-options)**: At least one must be set for a successful quote. * **[LZ Dead DVN](/v2/concepts/glossary#dead-dvn)**: If your configuration includes LZ Dead DVN for a particular pathway, the quote will fail. OApps must configure DVNs explicitly on Mainnet. This status typically indicates that the destination OApp is either missing trusted peer settings or the pathway has not been properly initialized. Common causes: * Incorrect peer configuration: Ensure that `setPeer()` is correctly called on both the source and destination chains during deployment. Double-check that the address format and EID (endpoint ID) are accurate. * Pathway not initialized correctly: Confirm that `allowInitializePath()` is properly implemented in your OApp contract. Learn more: [Integration Checklist](/v2/tools/integration-checklist#set-peers-on-every-pathway) On Solana, if you have a custom writing implementation, ensure that `endpoint.initOAppNonce` was called with the correct parameters. A "Blocked" message usually points to configuration issues: * **NotInitializable**: Ensure peers are set correctly on both ends or pathway is initialized correctly as explained above. * **DVN mismatch**: All DVN providers must be the same on source and destination. * **Block confirmations mismatch**: Outbound confirmations must be ≥ inbound confirmations. Step 1: Identify which DVN contract is reverting and capture the exact error. Step 2: Verify that the correct DVN type is configured for your message workflow. * Read-compatible DVNs (`lzRead`) must only be used for `lzRead` (pull / request-response) workflows. * Using a Read-compatible DVN for push messaging will cause the error `DVN_EidNotSupported`. If an incorrect DVN type is configured, update the DVN configuration to resolve the error. Step 3: If the configuration appears correct, contact the LayerZero support team to confirm that the DVN is properly configured for the affected pathway. To avoid out-of-gas errors when executing `lzReceive()` on the destination: * Use `enforcedOptions` (set at the OApp/OFT contract level) to define a default gas value for all sends. * Use `extraOptions` (specified in the `send` function call) for transaction-specific overrides. Both values must sum to at least the gas required by `lzReceive()` on the destination. It is also common to simply use `enforcedOptions` and set `extraOptions` to `0x`. If the destination message fails due to insufficient gas, you can retry it manually: * [Retry a failed message on EVM](/v2/developers/evm/troubleshooting/debugging-messages#retry-message) * [Retry a failed message on Solana](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/solana/retryMessage.ts) ## Ecosystem You can explore the current list of projects on the [Ecosystem page](https://layerzero.network/ecosystem/dapps). This list is not exhaustive—projects can submit a request to be listed. Submit your application via the [Ecosystem Listing Form](https://layerzeronetwork.typeform.com/lzecosystem) if your dApp uses LayerZero for omnichain messaging. LayerZero does not have a traditional grant program. However, the **LayerZero Foundation** runs **lzCatalyst**, a program connecting top builders with leading VCs in the space. More info: [lzCatalyst Program](https://info.layerzero.foundation/lzcatalyst-5f11ee16cc12) # Adding Networks to Your LayerZero Project Source: https://docs.layerzero.network/v2/get-started/create-lz-oapp/adding-networks Add new EVM networks to your LayerZero OApp or OFT project. Configure hardhat, deploy contracts, and wire peers across chains. Step-by-step instructions for ... When working with a LayerZero project, it searches for the closest `hardhat.config.ts` and `layerzero.config.ts` files starting from the Current Working Directory. This file normally lives in the root of your project. This guide shows how to add a new EVM network to your existing LayerZero project. **Example scenario:** You have an OFT deployed on Optimism Sepolia and Base Sepolia, and want to add Arbitrum Sepolia to your mesh. **Existing networks in your mesh:** * Optimism Sepolia (EID: 40232) * Base Sepolia (EID: 40245) **Network being added:** * Arbitrum Sepolia (EID: 40231) You will: * Update `hardhat.config.ts` with the new network's LayerZero [Endpoint ID (EID)](../../deployments/deployed-contracts) and RPC * Deploy your contract to the new network * Update `layerzero.config.ts` to declare the contract and connections * Wire peers and apply configuration across pathways ## Update dependencies Ensure you have the latest versions of [@layerzerolabs/lz-evm-sdk-v2](https://www.npmjs.com/package/@layerzerolabs/lz-evm-sdk-v2) and [@layerzerolabs/lz-definitions](https://www.npmjs.com/package/@layerzerolabs/lz-definitions). Update or install the packages using your preferred package manager: ```bash wrap pnpm theme={null} pnpm add -D @layerzerolabs/lz-evm-sdk-v2@latest @layerzerolabs/lz-definitions@latest ``` ```bash wrap npm theme={null} npm install --save-dev @layerzerolabs/lz-evm-sdk-v2@latest @layerzerolabs/lz-definitions@latest ``` ```bash wrap yarn theme={null} yarn add -D @layerzerolabs/lz-evm-sdk-v2@latest @layerzerolabs/lz-definitions@latest ``` Using an outdated version `@layerzerolabs/lz-definitions` may result in an error such as ` TypeError: Cannot read properties of undefined (reading 'toString')`. Using an outdated version `@layerzerolabs/lz-evm-sdk-v2` may result in the error `Error: No deployment found for: EndpointV2`. ## Add the network to `hardhat.config.ts` Add an entry with the LayerZero Endpoint ID, RPC URL, and your deployer accounts. Here's the diff showing what to add: ```typescript wrap theme={null} // hardhat.config.ts import { EndpointId } from '@layerzerolabs/lz-definitions' export default { networks: { // Existing networks 'optimism-sepolia': { eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OPT_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, 'base-sepolia': { eid: EndpointId.BASSEP_V2_TESTNET, url: process.env.RPC_URL_BASE_SEPOLIA || 'https://sepolia.base.org', accounts, }, // highlight-start + 'arbitrum-sepolia': { + eid: EndpointId.ARBSEP_V2_TESTNET, + url: process.env.RPC_URL_ARB_SEPOLIA || 'https://sepolia-rollup.arbitrum.io/rpc', + accounts, + }, // highlight-end }, } ``` The only notable change from a standard `hardhat.config.ts` setup is the inclusion of a [**LayerZero Endpoint ID**](../../deployments/deployed-contracts). For hardhat specific questions, refer to the [**Hardhat Configuration**](https://hardhat.org/hardhat-runner/docs/config) documentation. The npx package uses `@layerzerolabs/lz-definitions` to enable you to reference both V1 and V2 Endpoints. Make sure if your project uses LayerZero V2 to select the V2 Endpoint (i.e., `eid: EXAMPLE_V2_MAINNET`). ## Deploy your contract to the new network Use the CLI to deploy your contract to the added network. You can deploy interactively or target a specific network. ```bash wrap theme={null} # Interactive (select networks and tags when prompted) pnpm hardhat lz:deploy # Or target a specific network (example name from the previous step) # pnpm hardhat deploy --network arbitrum-sepolia --tags MyOFT ``` ## Add the new contract and connections to `layerzero.config.ts` Specify which contracts should be connected on a per pathway basis and set the security stack values: ```typescript wrap theme={null} // layerzero.config.ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; import {OAppEnforcedOption, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; // highlight-start + const arbitrumSepoliaContract: OmniPointHardhat = { + eid: EndpointId.ARBSEP_V2_TESTNET, + contractName: 'MyOFT', + }; // highlight-end const optimismSepoliaContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', }; const baseSepoliaContract: OmniPointHardhat = { eid: EndpointId.BASSEP_V2_TESTNET, contractName: 'MyOFT', }; // For this example's simplicity, we will use the same enforced options values for sending to all chains // For production, you should ensure `gas` is set to the correct value through profiling the gas usage of calling OApp._lzReceive(...) on the destination chain // To learn more, read https://docs.layerzero.network/v2/concepts/applications/oapp-standard#execution-options-and-enforced-settings const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 80000, value: 0, }, ]; // Add pathways to connect your new network with existing networks // highlight-start + const pathways: TwoWayConfig[] = [ + [ + arbitrumSepoliaContract, // New network contract + optimismSepoliaContract, // Existing network 1 + [['LayerZero Labs', ''], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] + [15, 15], // [A to B confirmations, B to A confirmations] + [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions + ], + [ + arbitrumSepoliaContract, // New network contract + baseSepoliaContract, // Existing network 2 + [['LayerZero Labs', ''], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] + [15, 15], // [A to B confirmations, B to A confirmations] + [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain C enforcedOptions, Chain A enforcedOptions + ], + // ... existing pathways between your current networks + ]; // highlight-end export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [ // highlight-start + {contract: arbitrumSepoliaContract}, // highlight-end {contract: optimismSepoliaContract}, {contract: baseSepoliaContract}, ], connections, }; } ``` You can refer to defaults with the [Defaults Checker](https://layerzeroscan.com/tools/defaults). ## Wire peers and apply configuration Set peers, libraries, [DVN](../../concepts/glossary#dvn-decentralized-verifier-network)/[Executor](../../concepts/glossary#executor) settings, and enforced options per your `layerzero.config.ts`. When delegate and owner are your local EOA (not recommended): ```bash wrap theme={null} pnpm hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` When delegate and owner are Multisigs: ```bash wrap theme={null} pnpm hardhat lz:oapp:wire --oapp-config layerzero.config.ts --output-filename wiring-txns.json ``` * `--output-filename` will generate a JSON file with the list of transactions rather than submitting the transactions for execution directly * When asked to preview txns, you can choose `Y` or just terminate the process immediately, as at this point, the JSON file would have already been generated. If asked "Would you like to submit the required transactions?", respond with `N` to terminate the process. * View the generated JSON at `wiring-txns.json` to view the txns data that need execution * Import them into your Multisig for execution ## Verify connections and configuration ```bash wrap theme={null} # Check peers pnpm hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts # Check pathway config pnpm hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` ## Checking Pathway Configurations To check your OApp's current configuration, you can run: ```bash wrap theme={null} npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` This command will output a table with 3 columns: 1. **Custom OApp Config**: your `layerzero.config.ts` configuration changes, with null values for unchanged parameters. 2. **Default OApp Config**: the default LayerZero configuration for the pathway. 3. **Active OApp Config**: the combination of your customized and default parameters, i.e., the active configuration. ```bash wrap theme={null} ┌────────────────────┬─────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────────────────────────┐ │ │ Custom OApp Config │ Default OApp Config │ Active OApp Config │ ├────────────────────┼─────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤ │ localNetworkName │ bsc_testnet │ bsc_testnet │ bsc_testnet │ ├────────────────────┼─────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤ │ remoteNetworkName │ sepolia │ sepolia │ sepolia │ ├────────────────────┼─────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤ │ sendLibrary │ 0x0000000000000000000000000000000000000000 │ 0x55f16c442907e86D764AFdc2a07C2de3BdAc8BB7 │ 0x55f16c442907e86D764AFdc2a07C2de3BdAc8BB7 │ ├────────────────────┼─────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤ │ receiveLibrary │ 0x0000000000000000000000000000000000000000 │ 0x188d4bbCeD671A7aA2b5055937F79510A32e9683 │ 0x188d4bbCeD671A7aA2b5055937F79510A32e9683 │ ├────────────────────┼─────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤ │ sendUlnConfig │ ┌──────────────────────┬───┐ │ ┌──────────────────────┬────────────────────────────────────────────────────┐ │ ┌──────────────────────┬────────────────────────────────────────────────────┐ │ │ │ │ confirmations │ 0 │ │ │ confirmations │ 5 │ │ │ confirmations │ 5 │ │ │ │ ├──────────────────────┼───┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ │ │ │ requiredDVNs │ │ │ │ requiredDVNs │ ┌───┬────────────────────────────────────────────┐ │ │ │ requiredDVNs │ ┌───┬────────────────────────────────────────────┐ │ │ │ │ ├──────────────────────┼───┤ │ │ │ │ 0 │ 0x0eE552262f7B562eFcED6DD4A7e2878AB897d405 │ │ │ │ │ │ 0 │ 0x0eE552262f7B562eFcED6DD4A7e2878AB897d405 │ │ │ │ │ │ optionalDVNs │ │ │ │ │ └───┴────────────────────────────────────────────┘ │ │ │ │ └───┴────────────────────────────────────────────┘ │ │ │ │ ├──────────────────────┼───┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ │ │ │ optionalDVNThreshold │ 0 │ │ │ optionalDVNs │ │ │ │ optionalDVNs │ │ │ │ │ └──────────────────────┴───┘ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ │ │ │ │ optionalDVNThreshold │ 0 │ │ │ optionalDVNThreshold │ 0 │ │ │ │ │ └──────────────────────┴────────────────────────────────────────────────────┘ │ └──────────────────────┴────────────────────────────────────────────────────┘ │ ├────────────────────┼─────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤ │ sendExecutorConfig │ ┌────────────────┬────────────────────────────────────────────┐ │ ┌────────────────┬────────────────────────────────────────────┐ │ ┌────────────────┬────────────────────────────────────────────┐ │ │ │ │ executor │ 0x0000000000000000000000000000000000000000 │ │ │ executor │ 0x31894b190a8bAbd9A067Ce59fde0BfCFD2B18470 │ │ │ executor │ 0x31894b190a8bAbd9A067Ce59fde0BfCFD2B18470 │ │ │ │ ├────────────────┼────────────────────────────────────────────┤ │ ├────────────────┼────────────────────────────────────────────┤ │ ├────────────────┼────────────────────────────────────────────┤ │ │ │ │ maxMessageSize │ 0 │ │ │ maxMessageSize │ 10000 │ │ │ maxMessageSize │ 10000 │ │ │ │ └────────────────┴────────────────────────────────────────────┘ │ └────────────────┴────────────────────────────────────────────────────┘ │ └────────────────┴────────────────────────────────────────────────────┘ │ ├────────────────────┼─────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤ │ receiveUlnConfig │ ┌──────────────────────┬───┐ │ ┌──────────────────────┬────────────────────────────────────────────────────┐ │ ┌──────────────────────┬────────────────────────────────────────────────────┐ │ │ │ │ confirmations │ 0 │ │ │ confirmations │ 2 │ │ │ confirmations │ 2 │ │ │ │ ├──────────────────────┼───┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ │ │ │ requiredDVNs │ │ │ │ requiredDVNs │ ┌───┬────────────────────────────────────────────┐ │ │ │ requiredDVNs │ ┌───┬────────────────────────────────────────────┐ │ │ │ │ ├──────────────────────┼───┤ │ │ │ │ 0 │ 0x0eE552262f7B562eFcED6DD4A7e2878AB897d405 │ │ │ │ │ │ 0 │ 0x0eE552262f7B562eFcED6DD4A7e2878AB897d405 │ │ │ │ │ │ optionalDVNs │ │ │ │ │ └───┴────────────────────────────────────────────┘ │ │ │ │ └───┴────────────────────────────────────────────┘ │ │ │ │ ├──────────────────────┼───┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ │ │ │ optionalDVNThreshold │ 0 │ │ │ optionalDVNs │ │ │ │ optionalDVNs │ │ │ │ │ └──────────────────────┴───┘ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ │ │ │ │ optionalDVNThreshold │ 0 │ │ │ optionalDVNThreshold │ 0 │ │ │ │ │ └──────────────────────┴────────────────────────────────────────────────────┘ │ └──────────────────────┴────────────────────────────────────────────────────┘ │ └────────────────────┴─────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────┘ ``` ## Specifying Pathway Configurations For more specific configurations on a per pathway basis, review the [Configuring Pathways](./configuring-pathways) page. ## Related References * [Deployed Endpoints, Libraries, Executors](../../deployments/deployed-contracts) * [OFT Technical Reference](../../concepts/technical-reference/oft-reference) # Configuring LayerZero Contracts Source: https://docs.layerzero.network/v2/get-started/create-lz-oapp/configuring-pathways Configure LayerZero OApp contracts with peers, enforced options, send/receive libraries, and DVN settings using layerzero.config.ts. Build omnichain applicat... For each contract in your config file, you can configure the following: ```solidity wrap theme={null} FromOApp.transferOwnership(newOwner) FromOApp.setPeer(dstEid, peer) FromOApp.setEnforcedOptions() EndpointV2.setSendLibrary(OApp, dstEid, newLib) EndpointV2.setReceiveLibrary(OApp, dstEid, newLib, gracePeriod) EndpointV2.setReceiveLibraryTimeout(OApp, dstEid, lib, gracePeriod) EndpointV2.setConfig(OApp, sendLibrary, sendConfig) EndpointV2.setConfig(OApp, receiveLibrary, receiveConfig) EndpointV2.setDelegate(delegate) ``` ## Adding Configurations To configure your OApp, you will need to change your `layerzero.config.ts` for your desired pathways. **Production deployments should use multiple required DVNs from independent operators.** The examples below use `` as a placeholder so the config does not silently resolve to a single-DVN configuration. Replace it with a real DVN name from [DVN Addresses](/v2/deployments/dvn-addresses) before wiring. See the [Integration Checklist](/v2/tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway) for production DVN guidance. LayerZero's CLI makes use of the `@layerzerolabs/metadata-tools` package, which allows for a human readable `layerzero.config.ts` file. Here's how to use it: 1. Install metadata-tools: `pnpm add -D @layerzerolabs/metadata-tools` 2. Create a new [LZ config](/v2/concepts/glossary#lz-config) file named `layerzero.config.ts` (or edit your existing one) in the project root and use the examples below as a starting point: ```typescript wrap theme={null} import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {OAppEnforcedOption, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const avalancheContract: OmniPointHardhat = { eid: EndpointId.AVALANCHE_V2_TESTNET, contractName: 'MyOFT', }; const polygonContract: OmniPointHardhat = { eid: EndpointId.AMOY_V2_TESTNET, contractName: 'MyOFT', }; const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 80000, value: 0, }, { msgType: 2, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 80000, value: 0, }, { msgType: 2, optionType: ExecutorOptionType.COMPOSE, index: 0, gas: 80000, value: 0, }, ]; export default async function () { // note: pathways declared here are automatically bidirectional // if you declare A,B there's no need to declare B,A const connections = await generateConnectionsConfig([ [ avalancheContract, // Chain A contract polygonContract, // Chain B contract [['LayerZero Labs', ''], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [1, 1], // [A to B confirmations, B to A confirmations] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions ], ]); return { contracts: [{contract: avalancheContract}, {contract: polygonContract}], connections, }; } ``` ```typescript wrap theme={null} import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {OAppEnforcedOption, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; export const avalancheContract: OmniPointHardhat = { eid: EndpointId.AVALANCHE_V2_TESTNET, contractName: 'MyOFT', }; export const solanaContract: OmniPointHardhat = { eid: EndpointId.SOLANA_V2_TESTNET, address: 'HBTWw2VKNLuDBjg9e5dArxo5axJRX8csCEBcCo3CFdAy', // your OFT Store address }; const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 80000, value: 0, }, { msgType: 2, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 80000, value: 0, }, { msgType: 2, optionType: ExecutorOptionType.COMPOSE, index: 0, gas: 80000, value: 0, }, ]; const SOLANA_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 200000, value: 2500000, }, { msgType: 2, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 200000, value: 2500000, }, { // Solana options use (gas == compute units, value == lamports) msgType: 2, optionType: ExecutorOptionType.COMPOSE, index: 0, gas: 0, value: 0, }, ]; export default async function () { // note: pathways declared here are automatically bidirectional // if you declare A,B there's no need to declare B,A const connections = await generateConnectionsConfig([ [ avalancheContract, // Chain A contract solanaContract, // Chain B contract [['LayerZero Labs', ''], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [1, 1], // [A to B confirmations, B to A confirmations] [SOLANA_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions ], ]); return { contracts: [{contract: avalancheContract}, {contract: solanaContract}], connections, }; } ``` 2b. If your pathways include Solana, run the Solana init config command: ``` npx hardhat lz:oft:solana:init-config --oapp-config layerzero.config.ts ``` * Note that only the Solana contract object requires `address` to be specified. Do not specify `address` for non-Solana contract objects. * The above examples contains a minimal mesh with only one pathway (two chains) for demonstration purposes. You are able to add as many pathways as you need into the `connections` param, via `generateConnectionsConfig`. 3. Run the wire command: ```bash wrap theme={null} npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` The wire command will process all the transactions required to connect the pathways specified in the config file. If you change anything in the config, run the command again. Each pathway contains a `config`, containing multiple configuration structs for changing how your OApp sends and receives messages, specifically for the chain your OApp is sending `from`: | Name | Type | Description | | ----------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sendLibrary` | Address | The message library used for configuring all sent messages `from` this chain. (e.g., `SendUln302.sol`) | | `receiveLibraryConfig` | Struct | A struct containing the receive message library address (e.g., `ReceiveUln302.sol`), and an optional BigInt, `gracePeriod`, the time to wait before updating to a new MessageLib version during version migration. Controls how the `from` chain receives messages. | | `receiveLibraryTimeoutConfig` | Struct | An optional param, defining when the old receive library (`lib`) will expire (`expiry`) during version migration. | | `sendConfig` | Struct | Controls how the OApp sends `from` this pathway, containing two more structs: `executorConfig` and `ulnConfig` (DVNs). | | `receiveConfig` | Struct | Controls how the OApp (`from`) receives messages, specifically the `ulnConfig` (DVNs). | | `enforcedOptions` | Struct | Controls the minimum destination gas sent to the destination, per message type (e.g., `_lzReceive`, `lzCompose`, etc.) in your OApp. | When adding a `config`, consider that connections moves in a bidirectional, two-way path: * The `sendConfig` applies to all message sent `from` **Chain A** and received by the `to` address, **Chain B**. * The `receiveConfig` applies to all messages received by **Chain A** (`from`), sent from **Chain B** (the `to` contract). For example, this `config: {}` applies only to how the `bscContract` sends messages to the `sepoliaContract`, and how the `bscContract` receives messages from the `sepoliaContract`. ### Adding `sendLibrary` Every configuration should start by adding a `sendLibrary`. ```typescript wrap theme={null} connections: [ { // Sets the peer `from -> to`. Optional, you do not have to connect all pathways. from: bscContract, to: sepoliaContract, // Optional Configuration config: { // Required Send Library Address on BSC // highlight-next-line sendLibrary: "0x0000000000000000000000000000000000000000", }, }, ], ``` When running `lz:oapp:wire`, this will call `EndpointV2`: ```solidity wrap theme={null} // LayerZero/V2/protocol/contracts/interfaces/IMessageLibManager.sol function setSendLibrary(address _oapp, uint32 _eid, address _newLib) external; ``` Each [MessageLib](/v2/concepts/protocol/message-send-library) contains the available configuration options for the protocol, and so must be set by the application owner to prevent unintended updates. You should use the `sendLibrary` address for the chain you're sending `from` (i.e., `SendUln302.sol` on BSC). The MessageLib Registry is append only, meaning that old Message Libraries will always be available for OApps. Locking your Library is only necessary to prevent updates. ### Adding `receiveLibrary` Every configuration should also add a `receiveLibrary`. Similar to the `sendLibrary`, the OApp owner must also set the Receive Library to ensure that your configured application settings will be locked. To do this, add a `receiveLibraryConfig`: ```typescript wrap theme={null} connections: [ { // Sets the peer `from -> to`. Optional, you do not have to connect all pathways. from: bscContract, to: sepoliaContract, // Optional Configuration config: { // Required Send Library Address on BSC sendLibrary: "0x0000000000000000000000000000000000000000", // highlight-start receiveLibraryConfig: { // Required Receive Library Address on BSC receiveLibrary: "0x0000000000000000000000000000000000000000", // Optional Grace Period for Switching Receive Library Address on BSC gracePeriod: BigInt(0), }, // Optional Receive Library Timeout for when the Old Receive Library Address will no longer be valid on BSC receiveLibraryTimeoutConfig: { lib: "0x0000000000000000000000000000000000000000", expiry: BigInt(0), }, // highlight-end }, }, ], ``` The Receive Library also provides two additional parameters to help future-proof OApp's for migrating MessageLib versions: * `gracePeriod`: the time to wait before updating to a new MessageLib version during version migration. If the grace period is 0, it will delete the timeout configuration. * `expiry`: the time at which messages in-flight from the old library will be considered invalid. This is mainly for handling messages that are in-flight during the migration. In most cases, setting the `gracePeriod` to 0 will be sufficient. When running `lz:oapp:wire`, this config will call `EndpointV2`: ```solidity wrap theme={null} // LayerZero/V2/protocol/contracts/interfaces/IMessageLibManager.sol function setReceiveLibrary(address _oapp, uint32 _eid, address _newLib, uint256 _gracePeriod) external; function setReceiveLibraryTimeout(address _oapp, uint32 _eid, address _lib, uint256 _gracePeriod) external; ``` ### Adding `sendConfig` Your `sendConfig` controls what [DVN addresses](../../deployments/dvn-addresses) and [Executor addresses](../../deployments/deployed-contracts) should be paid to verify and execute when a message is sent. Each DVN and Executor contains both onchain and off-chain component. When sending a message, you pay the DVNs and Executors contracts on the source chain, and they relay the message to the equivalent contracts on the destination chain. For your `sendConfig`, use the DVNs and Executor contract addresses on the same chain as your sending OApp. DVNs only need to be the same for a given pathway. You can have one set of DVNs verifying transactions from `Arbitrum` to `Base` and `Base` to `Arbitrum`, and a separate set of DVNs verifying transactions from `Arbitrum` to `Avalanche` and `Avalanche` to `Arbitrum`. ```typescript wrap theme={null} connections: [ { // Sets the peer `from -> to`. Optional, you do not have to connect all pathways. from: bscContract, to: sepoliaContract, // Optional Configuration config: { // Required Send Library Address on BSC sendLibrary: "0x0000000000000000000000000000000000000000", // Required Receive Library Config receiveLibraryConfig: { // Required Receive Library Address on BSC receiveLibrary: "0x0000000000000000000000000000000000000000", // Optional Grace Period for Switching Receive Library Address on BSC gracePeriod: BigInt(0), }, // Optional Receive Library Timeout for when the Old Receive Library Address will no longer be valid on BSC receiveLibraryTimeoutConfig: { lib: "0x0000000000000000000000000000000000000000", expiry: BigInt(0), }, // highlight-start // Optional Send Configuration // @dev Controls how the `from` chain sends messages to the `to` chain. sendConfig: { executorConfig: { maxMessageSize: 10000, // The configured Executor address on BSC executor: "0x0000000000000000000000000000000000000000", }, ulnConfig: { // The number of block confirmations to wait on BSC before emitting the message from the source chain (BSC). confirmations: BigInt(0), // The address of the DVNs you will pay to verify a sent message on the source chain (BSC). // The destination tx will wait until ALL `requiredDVNs` verify the message. requiredDVNs: [], // The address of the DVNs you will pay to verify a sent message on the source chain (BSC). // The destination tx will wait until the configured threshold of `optionalDVNs` verify a message. optionalDVNs: [ "0x_POLYHEDRA_DVN_ADDRESS_ON_BSC", "0x_LAYERZERO_DVN_ADDRESS_ON_BSC", ], // The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified. optionalDVNThreshold: 2, }, }, // highlight-end }, }, ], ``` This will call `EndpointV2.setConfig`: ```solidity wrap theme={null} // LayerZero/V2/protocol/contracts/interfaces/IMessageLibManager.sol struct SetConfigParam { uint32 eid; uint32 configType; bytes config; } function setConfig(address _oapp, address _lib, SetConfigParam[] calldata _params) external; ``` The Executor and ULN `configType` and `config`: ```solidity wrap theme={null} // LayerZero/V2/messagelib/contracts/uln/uln302/SendUln302.sol uint32 internal constant CONFIG_TYPE_EXECUTOR = 1; uint32 internal constant CONFIG_TYPE_ULN = 2; ``` ```solidity wrap theme={null} // LayerZero/V2/messagelib/contracts/uln/SendLibBase.sol struct ExecutorConfig { uint32 maxMessageSize; address executor; } ``` ```solidity wrap theme={null} // LayerZero/V2/messagelib/contracts/uln/UlnBase.sol struct UlnConfig { uint64 confirmations; // we store the length of required DVNs and optional DVNs instead of using DVN.length directly to save gas uint8 requiredDVNCount; // 0 indicate DEFAULT, NIL_DVN_COUNT indicate NONE (to override the value of default) uint8 optionalDVNCount; // 0 indicate DEFAULT, NIL_DVN_COUNT indicate NONE (to override the value of default) uint8 optionalDVNThreshold; // (0, optionalDVNCount] address[] requiredDVNs; // no duplicates. sorted an an ascending order. allowed overlap with optionalDVNs address[] optionalDVNs; // no duplicates. sorted an an ascending order. allowed overlap with requiredDVNs } ``` ### Adding `receiveConfig` The receive configuration controls what [DVN addresses](../../deployments/dvn-addresses) your OApp expects to have verified the message in-flight. For example, if `BSC` is receiving messages from `Sepolia`, you should use the DVN contract addresses on `BSC` for each DVN provider you have in your `sendConfig`. ```typescript wrap theme={null} connections: [ { // Sets the peer `from -> to`. Optional, you do not have to connect all pathways. from: bscContract, to: sepoliaContract, // Optional Configuration config: { // Required Send Library Address on BSC sendLibrary: "0x0000000000000000000000000000000000000000", // Required Receive Library Config receiveLibraryConfig: { // Required Receive Library Address on BSC receiveLibrary: "0x0000000000000000000000000000000000000000", // Optional Grace Period for Switching Receive Library Address on BSC gracePeriod: BigInt(0), }, // Optional Receive Library Timeout for when the Old Receive Library Address will no longer be valid on BSC receiveLibraryTimeoutConfig: { lib: "0x0000000000000000000000000000000000000000", expiry: BigInt(0), }, // Optional Send Configuration // @dev Controls how the `from` chain sends messages to the `to` chain. sendConfig: { executorConfig: { maxMessageSize: 99, // The configured Executor address on BSC executor: "0x0000000000000000000000000000000000000000", }, ulnConfig: { // The number of block confirmations to wait on BSC before emitting the message from the source chain (BSC). confirmations: BigInt(42), // The address of the DVNs you will pay to verify a sent message on the source chain (BSC). // The destination tx will wait until ALL `requiredDVNs` verify the message. requiredDVNs: [], // The address of the DVNs you will pay to verify a sent message on the source chain (BSC). // The destination tx will wait until the configured threshold of `optionalDVNs` verify a message. optionalDVNs: [ "0x_POLYHEDRA_DVN_ADDRESS_ON_BSC", "0x_LAYERZERO_DVN_ADDRESS_ON_BSC", ], // The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified. optionalDVNThreshold: 2, }, }, // highlight-start // Optional Receive Configuration // @dev Controls how the `from` chain receives messages from the `to` chain. receiveConfig: { ulnConfig: { // The number of block confirmations to expect from the `to` chain (Sepolia). confirmations: BigInt(42), // The address of the DVNs your `receiveConfig` expects to receive verifications from on the `from` chain (BSC). // The `from` chain's OApp will wait until the configured threshold of `requiredDVNs` verify the message. requiredDVNs: [], // The address of the `optionalDVNs` you expect to receive verifications from on the `from` chain (BSC). // The destination tx will wait until the configured threshold of `optionalDVNs` verify the message. optionalDVNs: [ "0x_POLYHEDRA_DVN_ADDRESS_ON_BSC", "0x_LAYERZERO_DVN_ADDRESS_ON_BSC", ], // The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified. optionalDVNThreshold: 2, }, }, // highlight-end }, }, ], ``` This will set the `receiveConfig` in `EndpointV2.setConfig`: ```solidity wrap theme={null} // LayerZero/V2/messagelib/contracts/uln/UlnBase.sol struct UlnConfig { uint64 confirmations; // we store the length of required DVNs and optional DVNs instead of using DVN.length directly to save gas uint8 requiredDVNCount; // 0 indicate DEFAULT, NIL_DVN_COUNT indicate NONE (to override the value of default) uint8 optionalDVNCount; // 0 indicate DEFAULT, NIL_DVN_COUNT indicate NONE (to override the value of default) uint8 optionalDVNThreshold; // (0, optionalDVNCount] address[] requiredDVNs; // no duplicates. sorted an an ascending order. allowed overlap with optionalDVNs address[] optionalDVNs; // no duplicates. sorted an an ascending order. allowed overlap with requiredDVNs } ``` ### Adding `enforcedOptions` You can specify both a minimum destination gas and `msg.value` that users must pay for both your contract's `lzReceive` and \`\`lzCompose\` logic to execute as intended. The CLI Toolkit enables you to configure your message options in a human-readable format, provided that your OApp has added an Enforced Options. The **Omnichain Fungible Token (OFT) Standard** by default already has **Enforced Options** added to the contract, with two message types available: ```solidity wrap theme={null} // @dev execution types to handle different enforcedOptions uint16 internal constant SEND = 1; // a standard token transfer via lzReceive uint16 internal constant SEND_AND_CALL = 2; // a token transfer, followed by a composable call via lzCompose ``` ```typescript wrap theme={null} connections: [ { // Sets the peer `from -> to`. Optional, you do not have to connect all pathways. from: bscContract, to: sepoliaContract, // Optional Configuration config: { // Required Send Library Address on BSC sendLibrary: "0x0000000000000000000000000000000000000000", receiveLibraryConfig: { // Required Receive Library Address on BSC receiveLibrary: "0x0000000000000000000000000000000000000000", // Optional Grace Period for Switching Receive Library Address on BSC gracePeriod: BigInt(0), }, // Optional Receive Library Timeout for when the Old Receive Library Address will no longer be valid on BSC receiveLibraryTimeoutConfig: { lib: "0x0000000000000000000000000000000000000000", expiry: BigInt(0), }, // Optional Send Configuration // @dev Controls how the `from` chain sends messages to the `to` chain. sendConfig: { executorConfig: { maxMessageSize: 99, // The configured Executor address on BSC executor: "0x0000000000000000000000000000000000000000", }, ulnConfig: { // The number of block confirmations to wait on BSC before emitting the message from the source chain (BSC). confirmations: BigInt(42), // The address of the DVNs you will pay to verify a sent message on the source chain (BSC). // The destination tx will wait until ALL `requiredDVNs` verify the message. requiredDVNs: [], // The address of the DVNs you will pay to verify a sent message on the source chain (BSC). // The destination tx will wait until the configured threshold of `optionalDVNs` verify a message. optionalDVNs: [ "0x_POLYHEDRA_DVN_ADDRESS_ON_BSC", "0x_LAYERZERO_DVN_ADDRESS_ON_BSC", ], // The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified. optionalDVNThreshold: 2, }, }, // Optional Receive Configuration // @dev Controls how the `from` chain receives messages from the `to` chain. receiveConfig: { ulnConfig: { // The number of block confirmations to expect from the `to` chain (Sepolia). confirmations: BigInt(42), // The address of the DVNs your `receiveConfig` expects to receive verifications from on the `from` chain (BSC). // The `from` chain's OApp will wait until the configured threshold of `requiredDVNs` verify the message. requiredDVNs: [], // The address of the `optionalDVNs` you expect to receive verifications from on the `from` chain (BSC). // The destination tx will wait until the configured threshold of `optionalDVNs` verify the message. optionalDVNs: [ "0x_POLYHEDRA_DVN_ADDRESS_ON_BSC", "0x_LAYERZERO_DVN_ADDRESS_ON_BSC", ], // The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified. optionalDVNThreshold: 2, }, }, // highlight-start // Optional Enforced Options Configuration // @dev Controls how much gas to use on the `to` chain, which the user pays for on the source `from` chain. enforcedOptions: [ { msgType: 1, // depending on OAppOptionType3 optionType: ExecutorOptionType.LZ_RECEIVE, gas: 65000, // gas limit in wei for EndpointV2.lzReceive value: 0, // msg.value in wei for EndpointV2.lzReceive }, { msgType: 1, optionType: ExecutorOptionType.NATIVE_DROP, amount: 0, // amount of native gas token in wei to drop to receiver address receiver: "0x0000000000000000000000000000000000000000", }, { msgType: 2, optionType: ExecutorOptionType.LZ_RECEIVE, index: 0, gas: 65000, // gas limit in wei for EndpointV2.lzReceive value: 0, // msg.value in wei for EndpointV2.lzReceive }, { msgType: 2, optionType: ExecutorOptionType.COMPOSE, index: 0, // index of EndpointV2.lzCompose message gas: 50000, // gas limit in wei for EndpointV2.lzCompose value: 0, // msg.value in wei for EndpointV2.lzCompose }, ], // highlight-end }, }, ], ``` This will call `OApp.setEnforcedOptions` assuming your OApp has inherited from `OAppOptionsType3.sol`: ```solidity wrap theme={null} // LayerZero/V2/oapp/contracts/oapp/interfaces/IOAppOptionsType3.sol struct EnforcedOptionParam { uint32 eid; // Endpoint ID uint16 msgType; // Message Type bytes options; // Additional options } function setEnforcedOptions(EnforcedOptionParam[] calldata _enforcedOptions) external; ``` ### Adding `delegate` ```typescript wrap theme={null} // layerzero.config.ts contracts: [ { contract: sepolia, config: { delegate: '0x0000000000000000000000000000000000000000', }, }, { contract: bsc, config: { delegate: '0x0000000000000000000000000000000000000000', }, }, ]; ``` ### Adding `owner` ```typescript wrap theme={null} // layerzero.config.ts contracts: [ { contract: sepolia, config: { owner: '0x0000000000000000000000000000000000000000', }, }, { contract: bsc, config: { owner: '0x0000000000000000000000000000000000000000', }, }, ]; ``` To transfer ownership, you will need to run a separate command: ```bash wrap theme={null} npx hardhat lz:ownable:transfer-ownership --oapp-config layerzero.config.ts ``` Once you transfer ownership, you can no longer call `OApp.setDelegate` and `OApp.setEnforcedOptions`. You should ensure all other configurations have been set to your liking before transferring ownership. ## Applying Changes Wiring your contracts will set the `peer` address for your OApp or OFT and initialize the desired configuration in your `layerzero.config.ts`. ### Wiring Contracts The CLI Tool makes this one step easier by enabling you to wire and configure your contract pathways with a single command: ```bash wrap theme={null} $ npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Before wiring your contracts, you should review your `layerzero.config.ts` to ensure that you have specified accurately the configuration you want to set. Wiring your contracts will set the `peer` address for your OApp or OFT and initialize the desired configuration in your `layerzero.config.ts`. The CLI Tool makes this one step easier by enabling you to wire and configure your contract pathways with a single command: ```bash wrap theme={null} $ npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Before wiring your contracts, you should review your `layerzero.config.ts` to ensure that you have specified accurately the configuration you want to set. ### Checking `setPeers` To check if your contracts have correctly been set to communicate with one another, you can run: ```bash wrap theme={null} npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ### Checking Pathway `config` To confirm your OApp's configuration has been set as intended, you can run: ```bash wrap theme={null} $ npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` ### Checking `executor` To see your OApp's configured executor, you can run: ```bash wrap theme={null} npx hardhat lz:oapp:config:get:executor ``` ### Checking `enforcedOptions` To see your OApp's configured execution gas has been set as intended, you can run: ```bash wrap theme={null} npx hardhat lz:oapp:enforced-opts:get --oapp-config layerzero.config.ts ``` ### Checking Pathway `defaults` To see what the default configuration is for any pathway, run: ```bash wrap theme={null} npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` ### Wiring via Safe multisig If your contracts are owned by a Safe multisig wallet, you must define the multisig's `safeUrl` and `safeAddress` per chain in your `hardhat.config.ts` file to enable the submission of wire transactions for multisig approval. `safeUrl` refers to the URL of the [Safe Transaction Service](https://docs.safe.global/core-api/api-safe-transaction-service) for a given network. For the endpoints deployed by Safe themselves on popular networks, you can find the URLs in the [Safe Transaction Service API Reference](https://docs.safe.global/core-api/transaction-service-reference/mainnet). #### Step 1: Configure your Safe multisig In your hardhat config, add `safeConfig` to your networks, with your network specific `safeUrl` and `safeAddress` mapped accordingly: ```javascript wrap theme={null} // hardhat.config.ts networks: { // Include configurations for other networks as needed fuji: { /* ... */ // Network-specific settings safeConfig: { safeUrl: 'http://something', // URL of the Safe Transaction Service for the network safeAddress: 'address' // Address of the Safe wallet for the network } } } ``` #### Step 2: Use your safe config When wiring, pass the `--safe` flag in your wire command. ```bash wrap theme={null} $ npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts --safe ``` This command initiates the wiring process under the multisig setup, pushing transactions to the specified multisig wallet for necessary approvals. Ensure your development tools are up to date to utilize this feature, as it relies on the latest versions of the required dependencies. # Debugging LayerZero Errors Source: https://docs.layerzero.network/v2/get-started/create-lz-oapp/debugging Debug and decode LayerZero custom errors using CLI tools. List protocol errors and decode error selectors for faster troubleshooting. Step-by-step instructio... The LayerZero sample project provides powerful tools for listing and decoding custom errors from the protocol and your OApp. Using the CLI tool, you can identify errors at the protocol level, debug, and resolve issues quickly during development and deployment. ### Commands To list all the custom errors defined in the LayerZero protocol and your project, run: ```bash wrap theme={null} npx hardhat lz:errors:list ``` To decode custom error data based on the error selector, run: ```bash wrap theme={null} npx hardhat lz:errors:decode ``` The output will provide information about the custom error name, which you can compare against the error list. # Deploying LayerZero Contracts Source: https://docs.layerzero.network/v2/get-started/create-lz-oapp/deploying Deploy LayerZero contracts to multiple chains using the CLI and hardhat-deploy plugin. Select chains and verify deployments easily. Deploy across multiple bl... The LayerZero CLI tool uses the [hardhat-deploy](https://www.npmjs.com/package/hardhat-deploy) plugin to deploy contracts on multiple chains. After adding your `MNEMONIC` or `PRIVATE_KEY` to your dotenv file and adding networks in your `hardhat.config.ts`, run the following command to deploy your LayerZero contracts: ```bash wrap theme={null} npx hardhat lz:deploy ``` ### Selecting Chains You will be prompted to select which chains to deploy to: ```bash wrap theme={null} info: Compiling you hardhat project Nothing to compile ? Which networks would you like to deploy? › Instructions: ↑/↓: Highlight option ←/→/[space]: Toggle selection [a,b,c]/delete: Filter choices enter/return: Complete answer Filtered results for: Enter something to filter ◉ fuji ◉ amoy ◉ sepolia ``` If you wish to deploy to all blockchain networks selected, simply hit enter to continue deployment. To deselect a chain for deployment, highlight the chain and toggle the selection using the space bar or arrow keys: ```bash wrap theme={null} Filtered results for: Enter something to filter ◉ fuji ◯ amoy ◉ sepolia ``` ### Adding Deploy Script Tags Afterwards you'll be prompted to choose which deploy script tags to use. By default, each CLI example contains a starter deploy script, with the deploy script tag being the contract name: ```typescript wrap theme={null} deploy.tags = [contractName]; ``` The generic message passing standard for creating [Omnichain Applications (OApps)](../../developers/evm/oapp/overview): ```bash wrap theme={null} info: Compiling you hardhat project Nothing to compile ✔ Which networks would you like to deploy? › bsc_testnet, amoy, sepolia ? Which deploy script tags would you like to use? › MyOApp ``` An ERC20 extended with core bridging logic from OApp, creating an [Omnichain Fungible Token (OFT)](../../developers/evm/oft/quickstart): ```bash wrap theme={null} info: Compiling you hardhat project Nothing to compile ✔ Which networks would you like to deploy? › bsc_testnet, amoy, sepolia ? Which deploy script tags would you like to use? › MyOFT ``` Variant of OFT for adapting deployed ERC20 tokens as Omnichain Fungible Tokens, creating an [OFT Adapter](../../developers/evm/oft/quickstart): ```bash wrap theme={null} info: Compiling you hardhat project Nothing to compile ✔ Which networks would you like to deploy? › bsc_testnet, amoy, sepolia ? Which deploy script tags would you like to use? › MyOFTAdapter ``` An ERC721 extended with core bridging logic from OApp, creating an [Omnichain Non-Fungible Token (ONFT)](../../developers/evm/onft/quickstart): ```bash wrap theme={null} info: Compiling you hardhat project Nothing to compile ✔ Which networks would you like to deploy? › bsc_testnet, amoy, sepolia ? Which deploy script tags would you like to use? › MyONFT ``` You will need to add a new deploy script for any new contracts added to the repo. ### Running the Deployer After selecting either all or a specific deploy script, the deployer will those contracts on your specified chains. ```bash wrap theme={null} warn: Will use all deployment scripts ✔ Do you want to continue? … yes Network: amoy Deployer: 0x0000000000000000000000000000000000000000 Network: fuji Deployer: 0x0000000000000000000000000000000000000000 Network: sepolia Deployer: 0x0000000000000000000000000000000000000000 Deployed contract: MyOFT, network: amoy, address: 0x0000000000000000000000000000000000000000 Deployed contract: MyOFT, network: fuji, address: 0x0000000000000000000000000000000000000000 Deployed contract: MyOFT, network: sepolia, address: 0x0000000000000000000000000000000000000000 info: ✓ Your contracts are now deployed ``` You should see an output in your `./deployments` folder, or have one generated, containing your contracts: ```typescript wrap theme={null} contracts / // your contracts folder deploy / // hardhat-deploy scripts deployments / // your hardhat-deploy deployments amoy / // network name defined in hardhat.config.ts MyOFT.json; // deployed-contract json fuji / MyOFT.json; sepolia / MyOFT.json; test / // unit-tests, both hardhat and foundry enabled foundry.toml; // normal foundry.toml for remappings and project configuration hardhat.config.ts; // standard hardhat.config.ts, with layerzero endpoint mappings layerzero.config.ts; // special LayerZero config file (more on this later) ``` Your contract deployments can now be configured in your `layerzero.config.ts`! # Quickstart - Create Your First Omnichain App Source: https://docs.layerzero.network/v2/get-started/create-lz-oapp/start Build your first crosschain app with LayerZero. Step-by-step guide to send messages between blockchains using create-lz-oapp CLI. Build omnichain applicatio... This guide will walk you through the process of sending a simple crosschain message using LayerZero, designed to be a beginner's first step into the world of omnichain applications. This example will utilize a simplified OApp contract to demonstrate the basic principles of sending and receiving messages across different blockchains. ## Introduction LayerZero enables seamless communication between different blockchain networks. With LayerZero, you can have an interaction on one blockchain (say, **Ethereum**) automatically trigger a reaction on another (like **Arbitrum**), all without relying on a central authority to relay that trigger. Diagram showing crosschain messaging between Network A and Network B, with an arrow indicating the message flow via LayerZero Send and Receive Diagram showing crosschain messaging between Network A and Network B, with an arrow indicating the message flow via LayerZero Send and Receive This guide will walk you through the process of setting up and using a simplified OApp contract to send messages across chains. ## Prerequisites Before getting started, make sure you have: * Node.js and NPM installed * Basic understanding of Solidity and smart contracts * Testnet funds for deploying contracts ## Creating an OApp ### Project Setup LayerZero provides `create-lz-oapp`, a CLI Toolkit designed to streamline the process of building, testing, deploying and configuring omnichain applications (OApps). `create-lz-oapp` is an npx package that creates a `Node.js` project with both the Hardhat and Foundry development frameworks installed, allowing developers to build from any LayerZero Contract Standards. To start, create a new project: ```bash wrap theme={null} npx create-lz-oapp@latest ``` Following this, a simple project creation wizard will guide you through setting up a project template. Choose `OApp` as your example starting point when prompted and a package manager of your choice. This will initialize a repo with example contracts, crosschain unit tests for sample contracts, custom LayerZero configuration files, deployment scripts, and more. ### OApp Smart Contract Review the `MyOApp.sol` contract to see how it implements the `OApp` contract standard. No need to change anything in this file at this point, but it's good to know how sending and receiving messages works. ```solidity wrap theme={null} // contracts/MyOApp.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { OApp, MessagingFee, Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import { MessagingReceipt } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppSender.sol"; contract MyOApp is OApp { constructor(address _endpoint, address _delegate) OApp(_endpoint, _delegate) Ownable(_delegate) {} // highlight-next-line // This is where the message will be stored after it is received on the destination chain // highlight-next-line string public data = "Nothing received yet."; /** * @notice Sends a message from the source chain to a destination chain. * @param _dstEid The endpoint ID of the destination chain. * @param _message The message string to be sent. * @param _options Additional options for message execution. * @dev Encodes the message as bytes and sends it using the `_lzSend` internal function. * @return receipt A `MessagingReceipt` struct containing details of the message sent. */ function send( uint32 _dstEid, // highlight-next-line // The message to be sent to the destination chain // highlight-next-line string memory _message, bytes calldata _options ) external payable returns (MessagingReceipt memory receipt) { bytes memory _payload = abi.encode(_message); receipt = _lzSend(_dstEid, _payload, _options, MessagingFee(msg.value, 0), payable(msg.sender)); } /** * @notice Quotes the gas needed to pay for the full omnichain transaction in native gas or ZRO token. * @param _dstEid Destination chain's endpoint ID. * @param _message The message. * @param _options Message execution options (e.g., for sending gas to destination). * @param _payInLzToken Whether to return fee in ZRO token. * @return fee A `MessagingFee` struct containing the calculated gas fee in either the native token or ZRO token. */ function quote( uint32 _dstEid, string memory _message, bytes memory _options, bool _payInLzToken ) public view returns (MessagingFee memory fee) { bytes memory payload = abi.encode(_message); fee = _quote(_dstEid, payload, _options, _payInLzToken); } /** * @dev Internal function override to handle incoming messages from another chain. * @dev _origin A struct containing information about the message sender. * @dev _guid A unique global packet identifier for the message. * @param payload The encoded message payload being received. * * @dev The following params are unused in the current implementation of the OApp. * @dev _executor The address of the Executor responsible for processing the message. * @dev _extraData Arbitrary data appended by the Executor to the message. * * Decodes the received payload and processes it as per the business logic defined in the function. */ function _lzReceive( Origin calldata /*_origin*/, bytes32 /*_guid*/, bytes calldata payload, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { data = abi.decode(payload, (string)); } } ``` ### Configuration Update your `hardhat.config.ts` file to include the networks you want to deploy to: ```typescript wrap theme={null} networks: { 'avalanche-testnet': { eid: EndpointId.AVALANCHE_V2_TESTNET, url: process.env.RPC_URL_FUJI || 'https://rpc.ankr.com/avalanche_fuji', accounts, }, 'amoy-testnet': { eid: EndpointId.AMOY_V2_TESTNET, url: process.env.RPC_URL_AMOY || 'https://polygon-amoy-bor-rpc.publicnode.com', accounts, }, } ``` ### TIP: Choose Less Congested Networks Deploying to Sepolia can be unreliable due to high gas prices and high network congestion. Avalanche and Polygon testnets are more stable and predictable. If you need gas for these test networks, you can try one of these faucets: [Quicknode](https://faucet.quicknode.com/drip), [Chainlink](https://faucets.chain.link/). Rename `.env.example` file to `.env` and update it with needed configurations: ```js wrap theme={null} PRIVATE_KEY = your_private_key; // Required RPC_URL_FUJI = your_fuji_rpc; // Optional but recommended RPC_URL_AMOY = your_amoy_rpc; // Optional but recommended ``` At a minimum, you need to have the `PRIVATE_KEY`. RPC URLs are optional, but strongly recommended. If you don't provide them, public RPCs will be used, but public RPCs can be unreliable or slow, leading to long waiting times for transactions to be confirmed or, at worst, cause your transactions to fail. ## Deploying Contracts Before deploying, fund the address you're deploying from with the corresponding chains' native tokens. In this case, you need to have AVAX on Avalanche and POL on Polygon testnets. Deploy your contracts using the LayerZero CLI: ```bash wrap theme={null} npx hardhat lz:deploy ``` You will be presented with a list of networks to deploy to. If you have updated your `hardhat.config.ts` according to instructions above, you should have two networks already selected (`amoy-tesnet` and `avalanche-testnet`). If everything is set up correctly, you should see output similar to this: ``` info: Compiling your hardhat project Nothing to compile ✔ Which networks would you like to deploy? › amoy-testnet, avalanche-testnet ✔ Which deploy script tags would you like to use? … info: Will deploy 2 networks: amoy-testnet, avalanche-testnet warn: Will use all deployment scripts ✔ Do you want to continue? … yes Network: amoy-testnet Deployer: 0x498098ca1b7447fC5035f95B80be97eE16F82597 Network: avalanche-testnet Deployer: 0x498098ca1b7447fC5035f95B80be97eE16F82597 Deployed contract: MyOApp, network: avalanche-testnet, address: 0xC7c2c92b55342Df0c7F51D4dE3f02167466FacCC Deployed contract: MyOApp, network: amoy-testnet, address: 0x0538A4ED0844583d876c29f80fB97c0f747968ce info: ✓ Your contracts are now deployed ``` `MyOApp` contract is now deployed to both networks. Deployer and deployed contract addresses will be different for your project. Note the deployed contract addresses, we will need them later. ### Configuration and wiring Now we are ready to connect (wire) the contracts across chains. For that, we need to configure the `layerzero.config.ts` file to tell which chains should be wired and able to talk to each other. In our case, it's only two chains, but you can have as many as you want. Modify your `layerzero.config.ts` file to include the chains you deployed to: ```typescript wrap theme={null} // layerzero.config.ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OAppOmniGraphHardhat, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; const fujiContract: OmniPointHardhat = { eid: EndpointId.AVALANCHE_V2_TESTNET, contractName: 'MyOApp', }; const amoyContract: OmniPointHardhat = { eid: EndpointId.AMOY_V2_TESTNET, contractName: 'MyOApp', }; const config: OAppOmniGraphHardhat = { contracts: [ { contract: fujiContract, }, { contract: amoyContract, }, ], // highlight-start connections: [ { from: fujiContract, to: amoyContract, }, { from: amoyContract, to: fujiContract, }, ], // highlight-end }; export default config; ``` Now we can wire the contracts using: ```bash wrap theme={null} npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` This script will check all the configurations for each pathway, ask you if you would like to preview the transactions, show the transaction details before execution, and execute the transactions when you confirm. The final output will look like this: ``` info: Successfully sent 2 transactions info: ✓ Your OApp is now configured ``` To verify that the contracts are wired correctly, you can run: ```bash wrap theme={null} npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` This will output the peers for each contract, showing the contracts that are able to send and receive messages to each other. ``` ┌───────────────────┬───────────────────┬──────────────┐ │ from → to │ avalanche-testnet │ amoy-testnet │ ├───────────────────┼───────────────────┼──────────────┤ │ avalanche-testnet │ ∅ │ ✓ │ ├───────────────────┼───────────────────┼──────────────┤ │ amoy-testnet │ ✓ │ ∅ │ └───────────────────┴───────────────────┴──────────────┘ ✓ - Connected ⤫ - Not Connected ∅ - Ignored ``` Seems like everything is wired correctly. Time to send the first crosschain message! ## Sending Your First Message Now, you need to prepare a transaction that sends a message across the configured LayerZero channel. Using the contract instance that you deployed on Avalanche, you will call the `send` function on the contract, providing the required parameters: the source network, destination network and the message. To make it easier, let's create a hardhat task to do that. Create a new file `tasks/sendMessage.ts` and add the following code: ```typescript wrap theme={null} // tasks/sendMessage.ts import {task} from 'hardhat/config'; import {HardhatRuntimeEnvironment} from 'hardhat/types'; import {Options} from '@layerzerolabs/lz-v2-utilities'; export default task('sendMessage', 'Send a message to the destination chain') .addParam('dstNetwork', 'The destination network name (from hardhat.config.ts)') .addParam('message', 'The message to send') .setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => { const {message, dstNetwork} = taskArgs; const [signer] = await hre.ethers.getSigners(); // Get destination network's EID const dstNetworkConfig = hre.config.networks[dstNetwork]; const dstEid = dstNetworkConfig.eid; // Get current network's EID const srcNetworkConfig = hre.config.networks[hre.network.name]; const srcEid = srcNetworkConfig?.eid; console.log('Sending message:'); console.log('- From:', signer.address); console.log('- Source network:', hre.network.name, srcEid ? `(EID: ${srcEid})` : ''); console.log('- Destination:', dstNetwork || 'unknown network', `(EID: ${dstEid})`); console.log('- Message:', message); const myOApp = await hre.deployments.get('MyOApp'); const contract = await hre.ethers.getContractAt('MyOApp', myOApp.address, signer); // Add executor options with gas limit const options = Options.newOptions().addExecutorLzReceiveOption(200000, 0).toBytes(); // Get quote for the message console.log('Getting quote...'); const quotedFee = await contract.quote(dstEid, message, options, false); console.log('Quoted fee:', hre.ethers.utils.formatEther(quotedFee.nativeFee)); // Send the message console.log('Sending message...'); const tx = await contract.send(dstEid, message, options, {value: quotedFee.nativeFee}); const receipt = await tx.wait(); console.log('🎉 Message sent! Transaction hash:', receipt.transactionHash); console.log( 'Check message status on LayerZero Scan: https://testnet.layerzeroscan.com/tx/' + receipt.transactionHash, ); }); ``` We also need to import the task in our `hardhat.config.ts` file: ```typescript wrap theme={null} // hardhat.config.ts // (...) import {EndpointId} from '@layerzerolabs/lz-definitions'; import './tasks/sendMessage'; // Import the task ``` Now you can send a crosschain message, for example from Avalanche to Amoy, using: ```bash wrap theme={null} npx hardhat sendMessage --network avalanche-testnet --dst-network amoy-testnet --message "Hello Omnichain World (sent from Avalanche)" ``` This will output the transaction hash and a link to the LayerZero Scan to verify the message. ``` Sending message: - From: 0x498098ca1b7447fC5035f95B80be97eE16F82597 - Source network: avalanche-testnet (EID: 40106) - Destination: amoy-testnet (EID: 40267) - Message: Hello Omnichain World (sent from Avalanche) Getting quote... Quoted fee: 0.004605311339306711 Sending message... 🎉 Message sent! Transaction hash: 0x47bd60f2710c2ec5a496c55c9763bd87fd4c599b541ad1287540fce9852ede65 Check message status on LayerzeRo Scan: https://testnet.layerzeroscan.com/tx/0x47bd60f2710c2ec5a496c55c9763bd87fd4c599b541ad1287540fce9852ede65 ``` Congratulations! You've just sent your first crosschain message using LayerZero. Now let's have a closer look at the message and how it was received on the destination chain. ## Verifying Receipts The message will be stored in the `data` variable of the `MyOApp` contract on the destination chain. Remember how we set the `data` variable to `"Nothing received yet."` in the `MyOApp.sol` contract? ```solidity wrap theme={null} // contracts/MyOApp.sol // (...) string public data = "Nothing received yet."; ``` Now this `data` variable will be updated on the destination chain with the message we sent. We can verify this by calling the `data` getter function on the `MyOApp` contract on the destination chain, but first, let's have a look at the transaction on the LayerZero Scan. Click on the [LayerZero Scan link](https://testnet.layerzeroscan.com/tx/0x47bd60f2710c2ec5a496c55c9763bd87fd4c599b541ad1287540fce9852ede65) in the output of the transaction to get all the details of the message we just sent. LayerZero Scan transaction details page showing a delivered crosschain message with numbered annotations highlighting: (1) Status as Delivered, (2) Message Payload, (3) Transaction Fee, (4) OApp Configuration for sender and receiver, and (5) Destination Omnichain Application address There's a lot of useful information here. Let's focus on a few key details: 1. **Status**: The transaction status is `Delivered`. If you're checking the status of the message immediately after sending it, it might still be in `Inflight` status. Just wait a few seconds and it should be automatically updated. 2. **Message Payload**: All the parameters of our crosschain message are included here, including the message itself, encoded as bytes. 3. **Transaction Fee**: This is how much we paid to send the message crosschain. 4. **OApp Configuration**: This is the configuration of the `MyOApp` contract both on the source and destination chains. We used a lot of the default configurations, but you can customize them to your needs later on. 5. **Destination Omnichain Application**: This is the address of the `MyOApp` contract on the destination chain. You can click on the globe icon next to it to see the contract on the destination chain. You can click around the transaction details to learn more about the message passing process. When going to the destination chain (step 5 above), and clicking the "Contract" button and then "Read" button, you can see the message in the `data` variable of the `MyOApp` contract. PolygonScan contract interface showing the Read Contract tab with the data field displaying the received message: Hello Omnichain World (sent from Avalanche) highlighted in yellow We're on Polygon Amoy, and we have successfully received the message from Avalanche Fuji. Mission accomplished! ## Important Notes * Always ensure you have sufficient gas tokens on both source and destination chains * Double check endpoint IDs and contract addresses when setting peers * Monitor LayerZero Scan for message status ## Next Steps You have now successfully set up and used a simplified OApp contract to send a message across two different blockchains using LayerZero. This guide serves as a foundational example of the capabilities of LayerZero's crosschain messaging. From here, you can explore more advanced features and build more complex omnichain applications. ### Explore Contract Standards * [**Omnichain Token**](../../developers/evm/oft/quickstart): Create an Omichain Fungible Token that works across chains. * [**Omnichain NFT**](../../developers/evm/onft/quickstart): Build an Omnichain Non-Fungible Token (ONFT) collection that works across chains. * [**Omnichain Read**](../../developers/evm/lzread/overview): Read external state from other chains and perform calculations, using LayerZero Read. # Migrating from a Single-DVN Configuration Source: https://docs.layerzero.network/v2/get-started/migrating-from-single-dvn Operational guide for OApps that currently use a single required DVN and need to migrate to a multi-DVN production configuration. This guide walks an OApp that currently ships a single required DVN through the operational steps of adding a second required DVN. It is written for owners of live production deployments and assumes you cannot afford to lose in-flight messages during the migration. **Single-DVN production configurations are not safe.** A compromise of the one verifier results in unrestricted forged messages on the pathway. Plan to migrate every production pathway you operate. See [Production DVN Configuration](/v2/concepts/modular-security/production-dvn-configuration) for the full security rationale and target configuration tiers. ## Before you start You need to know, for **every pathway** in your mesh: 1. **Both endpoint IDs** — source and destination chain. 2. **Your OApp address** on each chain. 3. **The Send Library** address used on the source chain (`getSendLibrary`). 4. **The Receive Library** address used on the destination chain (`getReceiveLibrary`). 5. **The current DVN configuration** on each side (`getConfig` for both `sendConfig` and `receiveConfig`). Capture the exact `requiredDVNCount`, `requiredDVNs`, `optionalDVNCount`, `optionalDVNThreshold`, `optionalDVNs`, and `confirmations`. 6. **Available DVNs on each chain** (see [DVN Addresses](/v2/deployments/dvn-addresses)). The DVN you choose as your secondary must be deployed on **both** the source chain and the destination chain. You can capture (5) with the script in [EVM DVN and Executor Configuration → Getting the Default Config](/v2/developers/evm/configuration/dvn-executor-config) (or the equivalent VM-specific page). If a pathway you operate has only one DVN currently deployed on one of the two chains, you cannot migrate to multi-DVN yet on that pathway. Your options are: * Run your own DVN (see [Build a DVN](/v2/workers/off-chain/build-dvns)). * Wait until a third-party provider deploys (track on [DVN Addresses](/v2/deployments/dvn-addresses)). * Defer the chain until multi-DVN coverage exists. ## How DVN configuration applies to in-flight messages This is the most-misunderstood part of a migration. Read it twice. A pathway from Chain A → Chain B has two configurations that must agree: * **Chain A `sendConfig`** controls which DVNs the source library *pays* to verify each outbound message. * **Chain B `receiveConfig`** controls which DVNs the destination library *requires* to have verified before delivery. When a message is sent, only the DVNs in Chain A's `sendConfig` at that moment are notified and paid. They produce attestations that flow to Chain B. Delivery succeeds only if the DVNs in Chain B's `receiveConfig` at the moment of delivery have all attested. Implications for migration: * If you change Chain B's `receiveConfig` to require both `[LZLabs, DVN2]` **before** Chain A's `sendConfig` includes `DVN2`, every in-flight message and every new message stalls. `DVN2` never witnessed those send events, so `DVN2`'s attestation never appears. * If you change Chain A's `sendConfig` to `[LZLabs, DVN2]` **first**, then Chain A pays both DVNs to attest from that moment forward. Chain B's `receiveConfig` still only requires `LZLabs`, so messages still deliver. `DVN2` builds up attestations in the background. * Once you can confirm `DVN2` is attesting reliably for the pathway, update Chain B's `receiveConfig` to require both. New messages from that point will require both attestations; in-flight messages sent before the `sendConfig` update will fail (because `DVN2` did not see them) and need to be re-sent or recovered manually. The safe migration is therefore **send-side first, drain in-flight, then receive-side**. ## Migration sequence The sequence below assumes a single bidirectional pathway A↔B. For larger meshes, repeat the entire sequence for each direction of each pathway. Update both directions of a pathway in a single maintenance window where possible. ### Step 1 — Choose your secondary DVN Pick a DVN that is: * Deployed on **both** Chain A and Chain B (verify in [DVN Addresses](/v2/deployments/dvn-addresses)). * **Actively attesting the specific source-chain → destination-chain pathway**, not just present at the contract level. Provider deployment ≠ active pathway coverage; confirm with the provider directly or check the LayerZero metadata API for the pathway pair before committing. * Using a **different verification method** where possible (different node infrastructure, different RPC providers, ideally a different verification proof type). If only LayerZero Labs and one other provider are available on a pathway, that pair is your only multi-DVN option until a third provider deploys. Record the DVN's address on each chain (they will differ — DVN providers deploy a separate contract per chain). ### Step 2 — Pause sends if you can If your OApp supports a governance pause, **pause sends on Chain A** at the start of the migration window. This stops adding to the in-flight queue and lets the existing queue drain cleanly during Step 4, preventing the post-migration in-flight failure case. If you do not have a pause primitive, proceed to Step 3 and accept that any messages still in flight when Step 5 lands may need manual recovery — specifically, messages sent before Step 3 that have not yet been delivered when Chain B's `receiveConfig` is updated. ### Step 3 — Update Chain A's `sendConfig` Submit a `setConfig` transaction on Chain A to set the new `UlnConfig` with `requiredDVNCount: 2`, `requiredDVNs: [LZLabs, DVN2]` (sorted in ascending address order, as required by the contract). Verify with `getConfig` that the new value is on-chain. DVN addresses in `requiredDVNs` must be **sorted in ascending address order**. The contract reverts on unsorted or duplicate arrays. Sort before encoding. ### Step 4 — Drain in-flight Wait for at least `confirmations` × source-chain block time + a buffer (recommend 2× the typical end-to-end delivery time). The relevant boundary is **Step 3** (when `sendConfig` changed), not this step: * Messages sent **before Step 3** are attested only by DVN1, so they must complete delivery before Step 5 lands. The wait in this step ensures pre-Step-3 stragglers are not orphaned by the receive-side update. * Messages sent **after Step 3** are attested by both DVNs and will pass the new `receiveConfig` requirement once it lands. ### Step 5 — Update Chain B's `receiveConfig` Submit a `setConfig` transaction on Chain B to set the new `UlnConfig` with the matching `requiredDVNCount: 2` and matching DVNs (each chain has its own DVN address — make sure you use Chain B's deployment of the secondary DVN, not Chain A's). Verify with `getConfig` that the new value is on-chain. ### Step 6 — Verify end-to-end Send a small, low-value test message through the pathway. Confirm via [LayerZero Scan](https://layerzeroscan.com) that: * Both DVNs attested. * The message was committed and executed on Chain B. * The committed config matches the new `requiredDVNs` set. ### Step 7 — Resume sends and post-confirm Unpause sends if you paused in Step 2. Watch [LayerZero Scan](https://layerzeroscan.com) for delivery failures on the migrated pathway for at least 24 hours after the last pathway in your mesh completes Step 6. ### Step 8 — Repeat for the reverse direction Apply Steps 3–7 with A and B swapped. A pathway is only as secure as its weakest direction; an attacker who can forge B → A while A → B is hardened still wins. ## Mesh-wide considerations If you operate `N` chains in a mesh, you have `N × (N-1)` directional pathways. Three strategies, in order of operational complexity: ### Round-robin per pathway Migrate one pathway at a time. Lowest-risk operationally — each pathway's migration is fully completed and verified before the next begins. High overhead for large meshes. ### Per-source-chain batch Update all `sendConfig`s on Chain A in one transaction; wait for the drain window; then update all `receiveConfig`s on the partner chains. Lower overhead than round-robin, but requires careful coordination across the partner chains. ### Two-phase mesh Set every `sendConfig` mesh-wide first; wait one drain window; then set every `receiveConfig` mesh-wide. Lowest user disruption if your governance can sequence the transactions reliably. For a contiguous mesh of more than 5 chains, this approach is generally preferred. ## Coordinating with peer-chain owners If your OApp is jointly operated with another team (for example, an OFT bridged to a chain whose OApp deployment is owned by a partner), **both teams must update both sides**. You cannot unilaterally migrate a pathway whose receive side is owned by another team. Contact peer-chain owners with the new `UlnConfig` you intend to set and the maintenance window before you submit Step 3. ## Rollback If you discover an issue **before Step 5 lands** (only `sendConfig` has been updated), roll back Chain A's `sendConfig` to the original value. The pathway returns to its original 1-of-1 state. Messages sent between Step 3 and the rollback are over-attested by DVN2 but Chain B's `receiveConfig` only requires DVN1, so they deliver normally — no manual recovery needed. If you discover an issue **after Step 5 lands** (both `sendConfig` and `receiveConfig` updated, e.g. Step 6 verification fails), roll back **receive-side first**: drop Chain B's `receiveConfig` back to require only DVN1, then roll back Chain A's `sendConfig`. The intermediate state — Chain A producing both attestations while Chain B requires only one — works normally; outbound messages continue to deliver. The reverse order (rolling back `sendConfig` first while `receiveConfig` still requires both DVNs) leaves Chain A producing only DVN1 attestation, which Chain B will reject, blocking all messages until you complete the receive-side rollback. ## Verification checklist (after every pathway migration) * [ ] `getConfig(oApp, sendLib, dstEid, configType=2)` on the source chain returns `requiredDVNCount: 2` (or higher) with the intended DVN addresses. * [ ] `getConfig(oApp, receiveLib, srcEid, configType=2)` on the destination chain returns the matching configuration. * [ ] `confirmations` matches both sides. * [ ] DVN addresses are sorted in ascending order on both sides. * [ ] A test message has been sent and delivered with both DVN attestations visible on LayerZero Scan. * [ ] On-chain `getConfig` returns a configuration distinct from the protocol default shown by the [Default Config Checker](https://layerzeroscan.com/tools/defaults). * [ ] You have repeated the above for the reverse direction. ## See also * [Production DVN Configuration](/v2/concepts/modular-security/production-dvn-configuration) — risk tiers and target configurations * [Integration Checklist](/v2/tools/integration-checklist) — pre-launch gate * [Security Stack (DVNs)](/v2/concepts/modular-security/security-stack-dvns) — concept reference * [DVN Addresses](/v2/deployments/dvn-addresses) — available providers per chain * [Default Config Checker](https://layerzeroscan.com/tools/defaults) — verify resolved config per pathway # Get Started with LayerZero Source: https://docs.layerzero.network/v2/get-started/overview Start building omnichain apps with LayerZero. Choose your VM target: EVM, Solana, Aptos, or Hyperliquid for crosschain development. Step-by-step instruction... LayerZero enables **omnichain messaging** - sending data and instructions between different blockchains. ## Developer setup Build on Ethereum, Optimism, Arbitrum, and other EVM-compatible chains using Solidity. Get the chain deployment details for LayerZero contracts. ## Choose a network Build on Ethereum, Optimism, Arbitrum, and other EVM-compatible chains using Solidity. Build on Solana using Rust and the Anchor framework for high-performance applications. Build on Aptos using Move language with formal verification and parallel execution. Build on Hyperliquid, a high-performance L1 optimized for trading and DeFi applications. ## Start building Build crosschain applications that can send and receive messages between different blockchains. Pull data from other chains into your smart contracts using LayerZero Read. Transfer ERC20s across different blockchain networks using Omnichain Fungible Tokens. Transfer SPL tokens on Solana using Omnichain Fungible Tokens in Rust and the Anchor framework. Transfer Aptos fungible assets across different blockchain networks using Omnichain Fungible Tokens in Move. Transfer NFTs across different blockchain networks using Omnichain Non-Fungible Tokens. Compose multiple LayerZero operations in a single transaction and trigger additional calls. # LayerZero Sample Projects Source: https://docs.layerzero.network/v2/get-started/sample-projects Explore sample projects built with LayerZero. Find OApp, OFT, and crosschain examples to accelerate your omnichain development. Build omnichain tokens with ... Explore the library of sample projects using LayerZero. # LayerZero Security Audits Source: https://docs.layerzero.network/v2/resources/audits LayerZero protocol security audits. Access third-party audit reports and security documentation for LayerZero V2 contracts. Secure crosschain messaging on L... # Awesome LayerZero Source: https://docs.layerzero.network/v2/resources/awesome-layerzero Curated list of LayerZero resources, tools, and projects. Discover community contributions and integrations for omnichain development. Crosschain developmen... # LayerZero Whitepaper Source: https://docs.layerzero.network/v2/resources/whitepaper LayerZero V2 whitepaper. Technical documentation covering protocol architecture, security model, and omnichain messaging design. Crosschain development with... # create-lz-oapp CLI guide Source: https://docs.layerzero.network/v2/tools/create-lz-oapp-cli/guide Use create-lz-oapp CLI guide with LayerZero V2. Developer tools for building and debugging omnichain applications. LayerZero enables crosschain messaging. ## Introduction The `create-lz-oapp` CLI is a command-line tool for scaffolding LayerZero Omnichain Applications (OApps). This CLI toolkit simplifies the process of building, testing, deploying, and configuring omnichain applications by providing a structured project template and essential development tools. With `create-lz-oapp`, you can quickly bootstrap a new LayerZero project with both Hardhat and Foundry development frameworks pre-configured, along with example contracts, crosschain unit tests, LayerZero configuration files, and deployment scripts. ## Installation & Usage Create a new LayerZero OApp project with a single command: ```bash wrap theme={null} npx create-lz-oapp@latest ``` This will launch an interactive project creation wizard that guides you through setting up your omnichain application. ## CLI Options The `create-lz-oapp` CLI supports the following options: ### Version Information ```bash wrap theme={null} -V, --version ``` Output the current version number of the CLI tool. ### CI Mode ```bash wrap theme={null} --ci ``` Run the CLI in CI (Continuous Integration) mode, which operates in non-interactive mode. This is useful for automated deployments and scripts where user interaction is not available. * **Default**: `false` ### Project Directory ```bash wrap theme={null} -d, --destination ``` Specify the target directory where the new project should be created. If not provided, the CLI will use the current directory or prompt for a location. ### Example Project Template ```bash wrap theme={null} -e, --example ``` Choose which example project template to use as the starting point for your application. #### Always Available Examples These examples are always available: | Example Name | Description | | ------------- | ---------------------------------------------------------- | | `oapp` | OApp: Basic Omnichain Application for crosschain messaging | | `oft` | OFT: Omnichain Fungible Token implementation | | `oft-adapter` | OFTAdapter: Adapter for existing ERC20 tokens | | `onft721` | ONFT721: Omnichain Non-Fungible Token (ERC721) | #### Feature-Flagged Examples These examples are available only when specific environment variables are set: | Example Name | Description | Required Environment Variable | | ------------------------ | ---------------------------------- | -------------------------------------------- | | `lzapp-migration` | EndpointV1 Migration | `LZ_ENABLE_MIGRATION_EXAMPLE` | | `onft721-zksync` | ONFT721 zksolc | `LZ_ENABLE_ZKSOLC_EXAMPLE` | | `oft-upgradeable` | UpgradeableOFT | `LZ_ENABLE_UPGRADEABLE_EXAMPLE` | | `native-oft-adapter` | NativeOFTAdapter | `LZ_ENABLE_NATIVE_EXAMPLE` | | `oft-alt` | OFTAlt | `LZ_ENABLE_ALT_EXAMPLE` | | `mint-burn-oft-adapter` | MintBurnOFTAdapter | `LZ_ENABLE_MINTBURN_EXAMPLE` | | `oapp-read` | lzRead View/Pure Functions Example | `LZ_ENABLE_READ_EXAMPLE` | | `view-pure-read` | lzRead Public Variables Example | `LZ_ENABLE_READ_EXAMPLE` | | `uniswap-read` | lzRead UniswapV3 Quote | `LZ_ENABLE_READ_EXAMPLE` | | `oft-solana` | OFT (Solana) | `LZ_ENABLE_SOLANA_OFT_EXAMPLE` | | `oapp-solana` | OApp (Solana) | `LZ_ENABLE_SOLANA_OAPP_EXAMPLE` | | `oft-initia` | OFT (Initia) | `LZ_ENABLE_EXPERIMENTAL_INITIA_EXAMPLES` | | `oft-adapter-initia` | OFT Adapter (Initia) | `LZ_ENABLE_EXPERIMENTAL_INITIA_EXAMPLES` | | `oft-aptos-move` | OFT (Aptos Move) | `LZ_ENABLE_EXPERIMENTAL_MOVE_VM_EXAMPLES` | | `oft-adapter-aptos-move` | OFT Adapter (Aptos Move) | `LZ_ENABLE_EXPERIMENTAL_MOVE_VM_EXAMPLES` | | `oapp-aptos-move` | OApp (Aptos Move) | `LZ_ENABLE_EXPERIMENTAL_MOVE_VM_EXAMPLES` | | `oft-hyperliquid` | OFT + Composer (Hyperliquid) | `LZ_ENABLE_EXPERIMENTAL_HYPERLIQUID_EXAMPLE` | | `omni-call` | EVM OmniCall | `LZ_ENABLE_EXPERIMENTAL_OMNI_CALL_EXAMPLE` | ### Log Level ```bash wrap theme={null} --log-level ``` Set the verbosity level for CLI output and logging information. * **Available choices**: `"error"`, `"warn"`, `"info"`, `"http"`, `"verbose"`, `"debug"`, `"silly"` * **Default**: `"info"` ### Package Manager ```bash wrap theme={null} -p, --package-manager ``` Choose which Node.js package manager to use for dependency management in your project. * **Available choices**: `"npm"`, `"pnpm"`, `"bun"` ## Environment Variables for Feature-Flagged Examples To access feature-flagged examples, you can set the corresponding environment variables inline with the command. These examples provide access to experimental, specialized, or advanced features. ### Setting Environment Variables **On Unix/macOS/Linux:** ```bash wrap theme={null} LZ_ENABLE_READ_EXAMPLE=1 npx create-lz-oapp@latest -e oapp-read ``` **On Windows (Command Prompt):** ```cmd wrap theme={null} set "LZ_ENABLE_READ_EXAMPLE=1" && npx create-lz-oapp@latest -e oapp-read ``` **On Windows (PowerShell):** ```powershell wrap theme={null} $env:LZ_ENABLE_READ_EXAMPLE=1; npx create-lz-oapp@latest -e oapp-read ``` ## Example Usage ### Basic Interactive Setup ```bash wrap theme={null} npx create-lz-oapp@latest ``` ### Non-Interactive Setup with Options ```bash wrap theme={null} npx create-lz-oapp@latest --ci -d ./my-oapp-project -e oapp -p pnpm --log-level verbose ``` ### Create an OFT Project ```bash wrap theme={null} npx create-lz-oapp@latest -e oft -d ./my-oft-token -p pnpm ``` ### Create an ONFT721 Project ```bash wrap theme={null} npx create-lz-oapp@latest -e onft721 -d ./my-nft-project -p pnpm ``` ### Using Feature-Flagged Examples To use feature-flagged examples, include the required environment variable in the command: ```bash wrap theme={null} # Enable lzRead examples and create an lzRead project LZ_ENABLE_READ_EXAMPLE=1 npx create-lz-oapp@latest -e oapp-read -d ./my-read-project -p pnpm ``` ```bash wrap theme={null} # Enable Solana examples and create a Solana OFT project LZ_ENABLE_SOLANA_OFT_EXAMPLE=1 npx create-lz-oapp@latest -e oft-solana -d ./my-solana-oft -p pnpm ``` ```bash wrap theme={null} # Enable upgradeable examples in CI mode LZ_ENABLE_UPGRADEABLE_EXAMPLE=1 npx create-lz-oapp@latest --ci -e oft-upgradeable -d ./upgradeable-oft -p bun ``` ## Next Steps After creating your project with `create-lz-oapp`, you'll have a fully structured omnichain application ready for development. The generated project includes: * Example smart contracts implementing LayerZero standards * Deployment scripts and configuration The project structure will vary depending on the example template you selected, with each template providing the appropriate contracts, tests, and configuration for that specific use case (OApp, OFT, ONFT, etc.). Refer to the generated project's README file for specific instructions on how to configure, test, and deploy your omnichain application. # Recreating Deployments Source: https://docs.layerzero.network/v2/tools/create-lz-oapp-cli/recreating-deployments Use Recreating Deployments with LayerZero V2. Developer tools for building and debugging omnichain applications. LayerZero enables crosschain messaging. This guide is intended for situations where you might have lost access to the original project code, or the deployment was made outside of a project initialized by [create-lz-oapp](./guide). It explains how to reconstruct the expected deployments layout so you can use the Hardhat helper tasks. Under the hood, create-lz-oapp uses [Hardhat Deploy](https://github.com/wighawag/hardhat-deploy) v1 to manage deployments. All EVM deployments follow Hardhat Deploy's conventions. ## Invariants Note the following when using a project created via `create-lz-oapp` or when using any of the Hardhat helper tasks: * For EVM chains, the network name set in `hardhat.config.ts` must match the folder names under `/deployments` * For Solana, the deployment subfolder should either be `/solana-mainnet` or `/solana-testnet` (`solana-testnet` refers to Solana Devnet) ## Recreate Deployments For this example, we'll recreate deployments for an **OFT** that was deployed on **Arbitrum Sepolia** and **Solana Devnet**. Before following the steps, you need to at least have the address of the OFT on Arbitrum Sepolia, and the OFT Store address on Solana Devnet (not the Mint Address). 1. Create a new project using the [create-lz-oapp CLI](./guide). For an EVM-only OFT, you can choose `OFT` when prompted. For an OFT that is both on EVM and Solana, choose `OFT (Solana)`. 2. Create a `deployments` folder in the root of the repo. The eventual structure would look like this: ```text wrap theme={null} /deployments /arbitrum-sepolia .chainId MyOFT.json /solana-testnet OFT.json ``` > For the EVM chain deployments, if you previously deployed using Hardhat Deploy, simply copy over the contents of the deployments folder from your previous project. 3. Under `/deployments`, for the EVM chain (Arbitrum Sepolia in this example), create an `/arbitrum-sepolia` folder which contains: * `/arbitrum-sepolia/.chainId` - this file should contain the chain ID for the network, and **not** the Endpoint ID. Chain IDs can be found in the table [here](/v2/deployments/deployed-contracts). * `/arbitrum-sepolia/MyOFT.json` - this is where the OFT's address is set. The only key that is necessary in the JSON file is `address`. You can see a sample of that below, insert your OFT address into the `address` field. The required ABI will be handled by step 6. ```json wrap theme={null} { "address": "" } ``` 4. For Solana, create a `/solana-testnet` folder which contains: * `/solana-testnet/OFT.json` - in here are several addresses required by the Solana OFT. Below is an example of a full file with all the necessary keys: ```json wrap theme={null} { "programId": "", "mint": "", "mintAuthority": "", "escrow": "", "oftStore": "" } ``` > Given the Solana OFT Store address, you can run `npx hardhat lz:oft:store:debug --oft-store --eid ` to view debug information that contains the relevant Solana addresses. 5. Modify `hardhat.config.ts` and `layerzero.config.ts` accordingly. * in this example, the network name in `hardhat.config.ts` should be `arbitrum-sepolia` to match the folder name under `/deployments`. * For how to configure `layerzero.config.ts`, refer to the [Simple Config Generator](/v2/tools/simple-config) page. 6. Run `npm run compile:hardhat` to ensure relevant artifacts that are required by Hardhat helper tasks involving the EVM OFT are generated. For example, the `send` helper task when sending from an EVM chain requires the `OFT` artifact's ABI. 7. Run `npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts` and it should suggest transactions for your recreated deployments. ```bash wrap theme={null} npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` # LayerZero Endpoint V2 Examples and Developer Tooling Source: https://docs.layerzero.network/v2/tools/devtools Use Endpoint Examples and Developer Tooling with LayerZero V2. Developer tools for building and debugging omnichain applications. LayerZero enables... The Devtools repository’s **Examples** directory contains ready‑to‑use, audited LayerZero Endpoint V2 smart contracts for use on multiple chains. For each example, you can find a README with relevant installation steps, deployment tasks, and configuration logic to get started using these LayerZero boilerplate contracts. ## LayerZero V2 Contract Examples Below you can find all of the supported smart contract examples in [LayerZero Devtools](https://github.com/LayerZero-Labs/devtools). ### LayerZero Endpoint V2 Solidity Contract Examples (EVM) These contracts work out of the box on all EVM equivalent chains. * **OApp** — Omnichain message passing boilerplate > See: `examples/oapp` * **OFT** — Omnichain Fungible Token, mint and burn style ERC20 contract > See: `examples/oft` * **OFT Adapter** - Omnichain Fungible Token, lockbox style contract for ERC20 interface > See: `examples/oft-adapter` * **ONFT721** — Omnichain Non‑Fungible Token standard for ERC721 NFTs > See: `examples/onft721` * **OApp Read** - simple LayerZero Read template for reading a public state variable > See: `examples/oapp-read` * **Read View Pure** - simple LayerZero Read template for reading more complex data structures and functions > See: `examples/view-pure-read` ### LayerZero Endpoint V2 Solidity Contract Example Variants (EVM) These contract examples have niche changes for specific VM or application-specific requirements. * **Mint and Burn OFT Adapter** - use a deployed ERC20 token's `mint` and `burn` methods when debiting and crediting the OFT contract > See: `examples/mint-burn-oft-adapter` * **Native OFT Adapter** — turn a chain's native gas token into an Omnichain Fungible Token > See: `examples/native-oft-adapter` * **Upgradeable OFT** - an upgradeable OFT example using the Transparent Upgradeable Proxy pattern > See: `examples/oft-upgradeable` * **ONFT721 zkSync** - a variant repo of the ONFT721 example that shows how to deploy to zkSync elastic chains > See: `examples/onft721-zksync` * **Uniswap V3 Read** - a more advanced LayerZero Read template for reading non-view or pure functions > See: `examples/uniswap-read` ### LayerZero Endpoint V2 Solana Program Examples These programs work out of the box on SVM equivalent chains. * **OFT Solana** — a Solana program that conforms to the Omnichain Fungible Token standard using the SPL/Token2022 standard > See: `examples/oft-solana` ### LayerZero Endpoint V2 Solana Program Example Variants These program examples have niche changes for specific VM or application-specific requirements. * **LzApp-Migration** — a Solana program that conforms to the Omnichain Fungible Token standard for Endpoint V1 using the SPL/Token2022 standard > See: `examples/lzapp-migration` ### LayerZero Endpoint V2 Aptos Move Examples These programs work out of the box on Aptos Move equivalent chains. * **OApp Aptos Move** — Omnichain message passing boilerplate for Aptos VM > See: `examples/oapp-aptos-move` * **OFT Aptos Move** — Omnichain Fungible Token, mint and burn style contract using Aptos' Fungible Asset standard > See: `examples/oft-aptos-move` * **OFT Adapter** - Omnichain Fungible Token, lockbox style contract using Aptos' Fungible Asset standard > See: `examples/oft-adapter-aptos-move` ### LayerZero Endpoint V2 Aptos Move Example Variants These module examples have niche changes for specific VM or application-specific requirements. * **OFT Initia** - equivalent to Aptos Move OFT, except setup for the Initiad SDK. > See: `examples/oft-initia` * **OFT Adapter initia** - equivalent to Aptos Move OFT Adapter, except setup for the Initiad SDK. > See: `examples/oft-adapter-initia` # LayerZero Endpoint Metadata Source: https://docs.layerzero.network/v2/tools/endpoint-metadata Step-by-step guide to layerzero endpoint metadata using LayerZero V2. Build and deploy omnichain applications with crosschain messaging. Follow step-by-step... The LayerZero Endpoint Metadata provides a comprehensive JSON snapshot of all the key information needed to build and analyze crosschain applications. This metadata includes details such as deployments, tokens, RPC endpoints, chain information, and more for each supported chain. ## Overview * **Location:**\ The metadata is typically available at: ``` https://metadata.layerzero-api.com/v1/metadata ``` * **Structure:**\ The JSON object is organized by chain keys (for example, `"ethereum"`, `"bsc"`, `"polygon"`, etc.). Each top-level key corresponds to a chain and maps to an object that contains various sub-fields, including: * **Deployments:** Information about bridging contracts for **LayerZero V1** (such as `endpoint`, `relayerV2`, and `ultraLightNodeV2`) and for **LayerZero V2** (`endpointV2`, `executor`, `SendUln302`, etc.). * **RPCs:** A list of RPC endpoints for interacting with the chain. * **Chain Details:** Core data including `chainType`, `nativeChainId`, and details of the native currency. * **DVNs:** A dictionary of Decentralized Verifier Networks, used for ensuring the integrity of crosschain messages. * **Tokens:** A mapping of token addresses deployed using LayerZero to details such as symbol, decimals, and, optionally, pegging information. * **Address to OApp:** A lookup for known DApps by their onchain addresses. * **Other Fields:** Including `environment` (e.g., `"mainnet"` or `"testnet"`), `blockExplorers`, and `chainName`. ## Use Cases Developers and applications can leverage this metadata to: * **Dynamically configure applications:**\ Automatically set bridging addresses, tokens, and RPC endpoints based on the current network configuration. * **Display chain information:**\ Provide end users with up-to-date details like block explorer links, native currency information, and more. * **Validate local configurations:**\ Ensure that your application’s onchain references match the official metadata. ## Typical Metadata Fields Each chain’s metadata object usually includes: | **Field** | **Description** | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------- | | `environment` | Indicates the network environment, typically `"mainnet"` or `"testnet"`. | | `blockExplorers[]` | An array of objects (e.g., `{"url": "https://polygonscan.com"}`) that provide block explorer URLs. | | `rpcs[]` | An array of objects with RPC endpoint URLs (e.g., `{"url": "https://rpc.ftm.tools"}`). | | `chainDetails` | An object with detailed chain data (such as `chainType`, `nativeChainId`, `nativeCurrency`, etc.). | | `deployments[]` | An array that describes bridging contract deployments (like `endpointV2`, `relayerV2`, etc.). | | `dvns` | A dictionary of Data Validation Nodes (keyed by address), including details like version and canonical name. | | `tokens` | A dictionary keyed by token contract addresses, with each entry providing `symbol`, `decimals`, and optionally `peggedTo` data. | | `addressToOApp` | A mapping of onchain addresses to known DApps (each with an `id` and `canonicalName`). | | `chainName` | A human-readable name for the chain (often matching the top-level key). | This metadata is a vital resource for ensuring your application interacts with the correct chain configurations and remains in sync with official deployments. # Integration Checklist Source: https://docs.layerzero.network/v2/tools/integration-checklist Use Integration Checklist with LayerZero V2. Developer tools for building and debugging omnichain applications. LayerZero enables crosschain messaging. The checklist below is designed to help prepare a project that integrates LayerZero V2 [OApps](../concepts/glossary#oapp-omnichain-application) for an external audit or Mainnet deployment. Use it as a **pre‑production gate** for your omnichain application. ## Quick Checklist Use this high‑level checklist first, then refer to the detailed sections below. ### Critical (Must Complete) * **Peers set on all pathways (bidirectional)**\ A↔B and B↔A peers configured and verified on every chain. * **DVN configuration set on all pathways**\ Required and optional DVNs explicitly configured per pathway. * **Executor configuration set on all pathways**\ Max message size, executor address, and related parameters configured. * **Enforced options configured for gas/value**\ `enforcedOptions` set so users pay enough gas for destination execution. * **Mock and test functions removed**\ No leftover debug or example functions in production deployments. * **Ownership and delegate addresses verified**\ OApp owner, delegate, and upgrade admins set to the correct addresses. ### Recommended (Best Practices) * **Using latest LayerZero packages**\ Contracts imported from the latest published packages, not copied source. * **Libraries explicitly set (no reliance on defaults)**\ Send/receive libraries set per pathway instead of using protocol defaults. * **Message safety checks implemented**\ One action per message or robust handling for bundled actions. * **`msg.value` checks in `lzReceive`/`lzCompose`**\ Encoded and validated to prevent underfunded execution or unexpected state. ## 0. Introduction LayerZero applications operate over **directional pathways** between chains. Each direction (A→B and B→A) is configured and verified separately, and both must be correct for reliable omnichain behavior. At a high level: * On the **source chain (Chain A)**, `OApp(A)` calls `EndpointV2(A)` to construct and dispatch a [packet](../concepts/glossary#packet). * On the **destination chain (Chain B)**, `EndpointV2(B)` verifies the packet, inserts it into the [channel](../concepts/glossary#channel-lossless-channel), and calls `OApp(B).`[lzReceive](../concepts/glossary#lzreceive). Throughout this checklist, treat each **A→B** and **B→A** pathway as a separate unit of review. Configuration, peers, DVNs, and executors must be validated in **both directions**. ### Pathway Model & Mental Map A LayerZero application operates over directional pathways: **Path A → B**: 1. **[Source Chain](../concepts/glossary#source-chain) (Chain A)**: `OApp(A)` calls `EndpointV2(A)` → constructs & dispatches [packet](../concepts/glossary#packet). 2. **[Destination Chain](../concepts/glossary#destinationchain) (Chain B)**: `EndpointV2(B)` verifies, inserts [packet](../concepts/glossary#packet) into [channel](../concepts/glossary#channel-lossless-channel), and calls `OApp(B).`[lzReceive](../concepts/glossary#lzreceive). **Important**: A → B configuration must be checked separately from B → A. Pathways are **directional**. ### Critical Pathway Checks Use **[EndpointV2](../concepts/glossary#endpoint)** and **OApp** methods as documented. #### On Chain A (Source) — EndpointV2(A) 1. **Send Library in Use** `getSendLibrary(oApp, dstEid)` → confirms which send library is active. 2. **[Executor](../concepts/glossary#executor) & [DVN](../concepts/glossary#dvn-decentralized-verifier-network) Configuration (Send‑Side)** `getConfig(oApp, sendLib, dstEid, configType)` 1. `configType = 1`: Executor config (max message size, executor address). 2. `configType = 2`: [ULN](../concepts/glossary#uln-ultra-light-node)/DVN config (confirmations, required/optional DVNs). 3. **[Delegate](../concepts/glossary#delegate) Check** `delegates(oApp)` → verifies the delegate authorized to configure endpoint settings. #### On Chain B (Destination) — EndpointV2(B) 1. **Receive Library in Use** `getReceiveLibrary(oApp, srcEid)` → confirms which receive library is expected. 2. **DVN Configuration (Receive‑Side)** `getConfig(oApp, recvLib, srcEid, 2)` → ULN config (confirmations + DVN sets). 3. **Initialization Gate** `initializable(origin, receiver)` → Endpoint check if path can be initialized. Falls back to OApp’s allowInitializePath if no lazyNonce is present. 4. **Optional Diagnostic Checks** `verifiable(origin, receiver)` or `inboundPayloadHash(...)` for debugging message states. #### On OApp Contracts (Both Chains) 1. **Peer Mapping** `peers(eid)` → verifies that each OApp is correctly mapped to its counterpart on the remote chain. 2. **Initialization Override** `allowInitializePath(origin)` → ensures the OAppReceiver provides a default implementation. If using `ILayerZeroReceiver` directly, you must implement this method to control initialization permissions. ### Defaults in LayerZero Protocol LayerZero maintains **default configurations** at the Endpoint level. These serve as **fallbacks** if an OApp has not explicitly called `setSendLibrary`, `setReceiveLibrary`, or `setConfig`. 1. A default configuration may: 1. Be a working config (with active DVNs + Executor). 2. Be a **dead config** (e.g., DVNs not listening → hard revert on send). 3. Be **misconfigured** (Executor not set or not connected, even if pathway appears live). 2. **Review Implication:** 1. Do not assume defaults are safe for production. 2. Always check explicitly: `getSendLibrary`, `getReceiveLibrary`, and `getConfig`. If these resolve to defaults, confirm whether the defaults are valid for the intended pathway. 3. Unintentional fallbacks to defaults are a common cause of blocked or failing pathways. When the Config Checker flags a value as falling back to a protocol default, pin it explicitly: * **Required DVNs** (`default-required-dvns`) — call `setConfig` on the send and receive libraries with an explicit, ascending-sorted `requiredDVNs` array and a matching `requiredDVNCount`. See [Set Security and Executor Configurations on Every Pathway](#set-security-and-executor-configurations-on-every-pathway). * **Optional DVNs / threshold** (`default-optional-dvns`) — set `optionalDVNs` and `optionalDVNThreshold` in the same `setConfig` call to lock your [X-of-Y-of-N](/v2/concepts/protocol/message-security#configurable-channellevel-security-xofyofn) posture; leaving them at `0` / `[]` inherits the default. * **Libraries** (`default-libraries`) — call `setSendLibrary` / `setReceiveLibrary` with the intended ULN version so a protocol upgrade cannot move you onto a new library without your consent. See [Set Libraries on Every Pathway](#set-libraries-on-every-pathway). * **Confirmations** (`default-confirmations`) — set `confirmations` in the ULN `setConfig` to your production floor on both sides; the default can change without notice. See [Confirmation depth](/v2/concepts/modular-security/production-dvn-configuration#confirmation-depth). ## 1. OApp Implementation ### Use the Latest Version of LayerZero Packages Always use the latest version of LayerZero packages. Avoid copying contracts directly from LayerZero repositories. You can find the latest packages on each contract's home page. ### Avoid Hardcoding LayerZero Endpoint IDs Use admin-restricted setters to configure [endpoint IDs](../concepts/glossary#endpoint-id) instead of hardcoding them. ### Set Peers on Every Pathway To ensure successful one-way messages between chains, it's essential to establish peer configurations on both the source and destination chains. Both chains' OApps perform peer verification before executing the message on the destination chain, ensuring secure and reliable crosschain communication. ```solidity wrap theme={null} // The real endpoint ids will vary per chain, and can be found under "Supported Chains" uint32 aEid = 1; uint32 bEid = 2; MyOApp aOApp; MyOApp bOApp; // Call on both sides per pathway aOApp.setPeer(bEid, addressToBytes32(address(bOApp))); bOApp.setPeer(aEid, addressToBytes32(address(aOApp))); ``` If using a custom OApp implementation that is not a child contract of the LayerZero OApp Standard, implement the receive side check for initializing the OApp's pathway. The Receive Library will call `allowInitializePath` when a message is received, and if true, it will initialize the pathway for message passing. ```solidity wrap theme={null} // LayerZero V2 OAppReceiver.sol (implements ILayerZeroReceiver.sol) /** * @notice Checks if the path initialization is allowed based on the provided origin. * @param origin The origin information containing the source endpoint and sender address. * @return Whether the path has been initialized. * * @dev This indicates to the endpoint that the OApp has enabled msgs for this particular path to be received. * @dev This defaults to assuming if a peer has been set, its initialized. * Can be overridden by the OApp if there is other logic to determine this. */ function allowInitializePath(Origin calldata origin) public view virtual returns (bool) { return peers[origin.srcEid] == origin.sender; } ``` #### Peer Address Validation Verify every peer is a real, deployed, 20-byte EVM address (or the correct encoding for a non-EVM remote). V2 stores peers as `bytes32`; for an EVM remote, the upper 12 bytes must be zero and the lower 20 bytes carry the address. Sentinel forms (`0x…01`, `0x…dead…`, `0x…ff`) typically indicate placeholder data left over from scaffolding or a wrong-VM copy-paste. **Do:** * Call `setPeer(eid, bytes32(uint256(uint160(remoteAddress))))` with the actual deployed remote OApp address. * For non-EVM remotes (Solana program IDs, Aptos object addresses), use the encoding documented in the per-VM configuration page. * Re-validate peers after every redeploy of the remote contract — a stale peer points at a contract that no longer exists. **Don't:** * Leave a placeholder peer like `0x…dead`, `0x…01`, or all-`f` in production. Messages sent to a sentinel will be permanently undeliverable. * Pad an EVM address into the upper bytes (`bytes32(uint256(uint160(addr)) << 96)`) — this corrupts the encoding and the receiver will fail peer verification. A peer set to a sentinel or wrong-VM encoding silently breaks one direction of the channel. The sender pays gas, the DVN attests, and `lzReceive` reverts on every nonce until the peer is corrected. Source-side nonces accumulate as "stuck" with no automatic recovery. ##### Validate it yourself ```bash wrap theme={null} PEER=$(cast call "$OAPP_A" "peers(uint32)(bytes32)" "$EID_B" --rpc-url "$RPC_A") echo "Raw peer (bytes32): $PEER" # 1. For an EVM remote, the upper 12 bytes must be zero if echo "$PEER" | grep -qE '^0x0{24}[0-9a-fA-F]{40}$'; then echo "OK: lower-20-bytes peer ($(echo $PEER | sed 's/^0x0\{24\}/0x/'))" else echo "FAIL: peer has non-zero upper bytes — corrupted/wrong-VM encoding" fi # 2. Sentinel-address check ADDR_LOW=$(echo "$PEER" | sed 's/^0x0\{24\}/0x/' | tr 'A-F' 'a-f') case "$ADDR_LOW" in 0x0000000000000000000000000000000000000000) echo "FAIL: peer is the zero address — pathway is unpeered (never set, or explicitly cleared with setPeer(eid, bytes32(0)))" ;; 0x0000000000000000000000000000000000000001) echo "FAIL: sentinel 0x…01" ;; 0xffffffffffffffffffffffffffffffffffffffff) echo "FAIL: sentinel all-ones" ;; 0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead) echo "FAIL: sentinel dead pattern" ;; esac ``` #### Verify Peer Reciprocity Peers must be reciprocal. The pathway A→B requires `OApp(A).peers(eidB) == bytes32(OApp(B))` **and** `OApp(B).peers(eidA) == bytes32(OApp(A))`. If only one side is set, messages can leave the source but cannot be accepted at the destination — or vice versa. **Do:** * After deploying or updating a remote, call `setPeer` on both sides in the same operational batch. * Treat peer reciprocity as a pre-flight check in CI for every pathway in your mesh. * Use the LayerZero CLI's `lz oapp wire` (or equivalent) which sets reciprocal peers atomically. **Don't:** * Update only one side of a peer change and rely on "we'll do the other side later." A single-sided update is a known cause of stuck channels. * Set the same remote address on both sides if the OApps were deployed at different addresses — peers are per-(local-OApp, remote-EID), not per-chain. A non-reciprocal peer is a half-open channel: one direction works, the other rejects every message at `_lzReceive` peer verification. The source-side `outboundNonce` keeps incrementing while no nonce on the destination is ever accepted. ##### Validate it yourself ```bash wrap theme={null} LOCAL_PEER=$(cast call "$OAPP_A" "peers(uint32)(bytes32)" "$EID_B" --rpc-url "$RPC_A") REMOTE_PEER=$(cast call "$OAPP_B" "peers(uint32)(bytes32)" "$EID_A" --rpc-url "$RPC_B") # Expected: lower 20 bytes of LOCAL_PEER == OAPP_B, and lower 20 bytes of REMOTE_PEER == OAPP_A. EXPECTED_LOCAL=$(cast --to-uint256 "$OAPP_B") EXPECTED_REMOTE=$(cast --to-uint256 "$OAPP_A") [ "$LOCAL_PEER" = "$EXPECTED_LOCAL" ] && echo "A→B peer reciprocal ✓" || echo "A→B peer FAIL: got $LOCAL_PEER want $EXPECTED_LOCAL" [ "$REMOTE_PEER" = "$EXPECTED_REMOTE" ] && echo "B→A peer reciprocal ✓" || echo "B→A peer FAIL: got $REMOTE_PEER want $EXPECTED_REMOTE" ``` #### One OApp per Chain A single OApp deployment maintains exactly one canonical address per chain. If your mesh advertises more than one OApp address on the same chain, peers across the rest of the mesh will silently route to whichever address happens to be configured on each pathway — making part of the supply or message flow inaccessible from some chains. The one legitimate exception is the **lockbox adapter + plain OFT** pattern on the same chain (covered under [Check Use-Case Contracts](#check-use-case-contracts)), which is a distinct use-case contract, not a duplicate OApp. **Do:** * Maintain a single canonical deployment artifact per (OApp, chain) pair. * When migrating to a new contract address, drain and decommission the old deployment before announcing the new address — do not let both run in parallel as peers of the mesh. * Enumerate every OApp address on each chain from your own deployment manifest and confirm exactly one entry per chain (except for the documented adapter exception). **Don't:** * Run two production OApp deployments on the same chain and expect peers across the mesh to converge — they cannot. * Treat a forgotten test deployment as harmless if it still has peers set; it can intercept messages from chains that point at its address. Multiple OApps per chain fragment liquidity (for OFTs) and message flow (for general OApps). There is no on-chain mechanism that reconciles the two — recovery requires manually rewriting peers across every chain in the mesh. ##### Validate it yourself This is a mesh-shape check, not a per-pathway call. Enumerate OApp addresses from your own deployment manifest and confirm there is exactly one entry per chain. For LayerZero's view of your mesh, use the [LayerZero Scan tools](https://layerzeroscan.com/tools/defaults) to cross-check what the protocol sees against what you intend to ship. ### Set Libraries on Every Pathway It is recommended that OApps explicitly set the intended libraries. ```solidity wrap theme={null} EndpointV2.setSendLibrary(aOApp, bEid, newLib) EndpointV2.setReceiveLibrary(aOApp, bEid, newLib, gracePeriod) EndpointV2.setReceiveLibraryTimeout(aOApp, bEid, lib, gracePeriod) ``` If libraries are not set, the OApp will fallback to the default libraries set by LayerZero Labs. ```solidity wrap theme={null} /// @notice The Send Library is the Oapp specified library that will be used to send the message to the destination /// endpoint. If the Oapp does not specify a Send Library, the default Send Library will be used. /// @dev If the Oapp does not have a selected Send Library, this function will resolve to the default library /// configured by LayerZero /// @return lib address of the Send Library /// @param _sender The address of the Oapp that is sending the message /// @param _dstEid The destination endpoint id function getSendLibrary(address _sender, uint32 _dstEid) public view returns (address lib) { lib = sendLibrary[_sender][_dstEid]; if (lib == DEFAULT_LIB) { lib = defaultSendLibrary[_dstEid]; if (lib == address(0x0)) revert Errors.LZ_DefaultSendLibUnavailable(); } } ``` ### Set Security and Executor Configurations on Every Pathway You must configure Decentralized Validator Networks (DVNs) manually on all chain pathways for your OApp. LayerZero maintains a neutral stance and does not presuppose any security assumptions on behalf of deployed OApps. This approach requires you to define and implement security considerations that align with your application’s requirements. ```solidity wrap theme={null} EndpointV2.setConfig(aOApp, sendLibrary, sendConfig) EndpointV2.setConfig(aOApp, receiveLibrary, receiveConfig) ``` Follow the Protocol Configuration documentation to configure DVNs for each chain pathway. * [EVM](../developers/evm/configuration/dvn-executor-config) * [Solana](../developers/solana/configuration/dvn-executor-config) * [Aptos Move](../developers/aptos-move/configuration/dvn-executor-config) If no configuration is set, the OApp will fallback to the default settings set by LayerZero Labs. ```solidity wrap theme={null} // @dev get the executor config and if not set, return the default config function getExecutorConfig(address _oapp, uint32 _remoteEid) public view returns (ExecutorConfig memory rtnConfig) { ExecutorConfig storage defaultConfig = executorConfigs[DEFAULT_CONFIG][_remoteEid]; ExecutorConfig storage customConfig = executorConfigs[_oapp][_remoteEid]; uint32 maxMessageSize = customConfig.maxMessageSize; rtnConfig.maxMessageSize = maxMessageSize != 0 ? maxMessageSize : defaultConfig.maxMessageSize; address executor = customConfig.executor; rtnConfig.executor = executor != address(0x0) ? executor : defaultConfig.executor; } ``` Additional considerations: **Do:** * Use more than one DVN for each production pathway instead of relying on a single DVN. Use [Production DVN Configuration](../concepts/modular-security/production-dvn-configuration) to pick the right tier for your exposure. * Include at least one required DVN that is **not** operated by LayerZero Labs. Most preset configurations that ship a real default include LayerZero Labs as a required DVN, and the rest are placeholder Dead DVNs that you must replace before going live; production deployments should not concentrate trust in a single operator. * Keep DVN configurations consistent on both sides of every pathway (send and receive). * Ensure DVN and Executor contracts implement the expected interfaces for your deployment. * Verify DVN and Executor addresses against [V2 Contracts](../deployments/deployed-contracts) and [DVN Providers](../deployments/dvn-addresses). * Configure an Executor explicitly. The default Executor in every preset configuration is operated by LayerZero Labs; for high-value pathways, evaluate a [custom Executor](../concepts/permissionless-execution/executors#executor-concentration-a-single-point-of-failure-for-liveness). * If you currently ship a single-DVN configuration, follow [Migrating from a Single-DVN Configuration](../get-started/migrating-from-single-dvn). **Don’t:** * Configure only one DVN for a pathway and treat it as production‑ready. * Configure two required DVNs that are both operated by the same entity; this does not provide the operator-diversity that multi-DVN is designed to deliver. * Assume that mismatched DVN configurations are safe just because messages appear to be delivering (for example, when the receive‑side configuration is less strict than the send‑side). * Rely on the default Executor without considering its liveness implications for your application. ### Set Delegate on Every OApp It is recommended that OApps review and explicitly set the delegate for each deployment. ```solidity wrap theme={null} EndpointV2.setDelegate(delegate) ``` ### Check Initialization Logic is Valid on Every OApp Ensure that `EndpointV2` can initialize the OApp on every chain. ```solidity wrap theme={null} function _initializable( Origin calldata _origin, address _receiver, uint64 _lazyInboundNonce ) internal view returns (bool) { return _lazyInboundNonce > 0 || // allowInitializePath already checked ILayerZeroReceiver(_receiver).allowInitializePath(_origin); } function initializable(Origin calldata _origin, address _receiver) external view returns (bool) { return _initializable(_origin, _receiver, lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender]); } ``` ## 2. Custom Business Logic via LayerZero Interfaces ### Check Message Safety **Do:** * Design messages so that a single, clearly scoped action happens per cross‑chain message wherever possible. * If you bundle multiple actions, ensure they cannot fail mid‑sequence and leave partial state. * Consider **Instant Finality Guarantee (IFG)** for use cases with strict state‑safety requirements. **Don’t:** * Pack unrelated or high‑risk state changes into a single message without robust failure handling. * Assume that all downstream calls will succeed just because the message is verified. ### Check Mock and Test Functions Are Removed When example contracts are used as boilerplates, ensure that both any mock or test function existing or added is removed in the production deployments. ### Check Enforced Gas and Value **Do:** * Profile destination gas and value requirements for each message type on each pathway. * Use `enforcedOptions` so senders pay enough gas/value for reliable execution at the destination. * Refer to [transaction pricing guidance](../concepts/protocol/transaction-pricing#gas-profiling-considerations) when setting limits. **Don’t:** * Rely on “best guess” gas limits or leave options unset for production pathways. * Assume that executors will always provide the same `msg.value` you requested if you don’t verify it in your code. ```solidity wrap theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { OApp, Origin, MessagingFee } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; // highlight-next-line import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; contract MyOApp is OApp, OAppOptionsType3 { /// @notice Message types that are used to identify the various OApp operations. /// @dev These values are used in things like combineOptions() in OAppOptionsType3. uint16 public constant SEND = 1; constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) Ownable(_owner) {} // ... contract continues } ``` ```solidity wrap theme={null} EnforcedOptionParam[] memory aEnforcedOptions = new EnforcedOptionParam[](1); // Send gas for lzReceive (A -> B). aEnforcedOptions[0] = EnforcedOptionParam({eid: bEid, msgType: SEND, options: OptionsBuilder.newOptions().addExecutorLzReceiveOption(50000, 0)}); // gas limit, msg.value aOApp.setEnforcedOptions(aEnforcedOptions); ``` See more on Solana [OFT Message Execution Options](../developers/solana/oft/overview#message-execution-options). ### EVM-Specific #### Check `_lzReceive` Security 1. If using `OAppReceiver` (inherited by `OApp` and `OFT`), `msg.sender != endpoint` and `_origin.srcEid != expectedOApp` checks are already enforced in [`OAppReceiver.lzReceive`](../concepts/glossary#lzreceive) (endpoint-only access, peer validation). 2. If implementing directly from `ILayerZeroReceiver`, you must implement these checks and initialization safeguards. #### Check `lzCompose` Security Unlike child contracts with the `OAppReceiver.lzReceive` method, the [`ILayerZeroComposer.lzCompose`](../concepts/glossary#lzcompose) does not have built-in checks. Add these checks for the source `oApp` and `endpoint` before any custom state change logic: ```solidity wrap theme={null} // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import { ILayerZeroComposer } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroComposer.sol"; /// @title ComposedReceiver /// @dev A contract demonstrating the minimum ILayerZeroComposer interface necessary to receive composed messages via LayerZero. contract ComposedReceiver is ILayerZeroComposer { /// @notice Stores the last received message. string public data = "Nothing received yet"; /// @notice Store LayerZero addresses. address public immutable endpoint; address public immutable oApp; /// @notice Constructs the contract. /// @dev Initializes the contract. /// @param _endpoint LayerZero Endpoint address /// @param _oApp The address of the OApp that is sending the composed message. constructor(address _endpoint, address _oApp) { endpoint = _endpoint; oApp = _oApp; } /// @notice Handles incoming composed messages from LayerZero. /// @dev Decodes the message payload and updates the state. /// @param _oApp The address of the originating OApp. /// @param /*_guid*/ The globally unique identifier of the message. /// @param _message The encoded message content. function lzCompose( address _oApp, bytes32 /*_guid*/, bytes calldata _message, address, bytes calldata ) external payable override { // Perform checks to make sure composed message comes from correct OApp. // highlight-start require(_oApp == oApp, "!oApp"); require(msg.sender == endpoint, "!endpoint"); // highlight-end // Decode the payload to get the message (string memory message, ) = abi.decode(_message, (string, address)); data = message; } } ``` ### Enforce `msg.value` in `_lzReceive` and `lzCompose` If you specify in the executor `_options` a certain `msg.value`, it is not guaranteed that the message will be executed with these exact parameters because any caller can execute a verified message. In certain scenarios depending on the encoded message data, this can result in a successful message being delivered, but with a state change different than intended. Encode the `msg.value` inside the message on the sending chain, and then decode it in the `lzReceive` or `lzCompose` and compare with the actual `msg.value`. ```solidity wrap theme={null} // LayerZero V2 OmniCounter.sol example function value(bytes calldata _message) internal pure returns (uint256) { return uint256(bytes32(_message[VALUE_OFFSET:])); } function _lzReceive( Origin calldata _origin, bytes32 _guid, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { _acceptNonce(_origin.srcEid, _origin.sender, _origin.nonce); uint8 messageType = _message.msgType(); if (messageType == MsgCodec.VANILLA_TYPE) { //////////////////////////////// IMPORTANT ////////////////////////////////// /// if you request for msg.value in the options, you should also encode it /// into your message and check the value received at destination (example below). /// if not, the executor could potentially provide less msg.value than you requested /// leading to unintended behavior. Another option is to assert the executor to be /// one that you trust. ///////////////////////////////////////////////////////////////////////////// // highlight-next-line require(msg.value >= _message.value(), "OmniCounter: insufficient value"); count++; } } ``` This requires encoding the `msg.value` as part of the `_message` on the source chain, and extracting it from the encoded message. ## 3. LayerZero OFT/ONFT Implementation ### Check Use-Case Contracts **Do:** * Use plain OFT/ONFT implementations ([OFT](../concepts/glossary#oft-omnichain-fungible-token) or [ONFT](../concepts/glossary#onft-omnichain-non-fungible-token)) for new omnichain tokens on every chain. * For existing tokens with mint and burn capabilities, use a mint‑and‑burn adapter such as `MintAndBurnOFTAdapter` on existing chains, plus plain OFT/ONFT implementations on new chains. * For existing tokens without mint/burn capabilities, use a lockbox [adapter](../concepts/glossary#oft-adapter) such as `OFTAdapter` or `ONFT721Adapter` on the original chain, with plain OFT/ONFT on new chains. * For native gas tokens (for example, ETH or BNB), use a native lockbox adapter such as `NativeOFTAdapter`. **Don’t:** * Mix multiple lockbox adapters for the same OFT deployment (see warning below). * Treat adapter choice as interchangeable across chains without considering the underlying token’s capabilities. **There can only be one lockbox OFT Adapter used in an OFT deployment.** Multiple OFT Adapters break omnichain unified liquidity by effectively creating token pools. If you create OFT Adapters on multiple chains, you have no way to guarantee finality for token transfers due to the fact that the source chain has no knowledge of the destination pool's supply (or lack of supply). This can create race conditions where if a sent amount exceeds the available supply on the destination chain, those sent tokens will be permanently lost. ### Check Shared Decimals [Shared Decimals](../concepts/glossary#shared-decimals) must be consistent across all OFT deployments, or amount conversion will vary by orders of magnitude and allow double spending. ### Check Local Decimals Every chain's OFT token enforces its own [local decimals](../concepts/glossary#local-decimals), which ultimately cap how much supply can exist on that chain (for example, Solana balances are stored as `u64`). You must ensure that the OFT token on all chains can hold the same max supply value. Failing to do so may result in failed crosschain transactions due to overflow issues. For detailed guidance, see [Deciding the number of local decimals for your Solana OFT](../developers/solana/technical-reference/solana-guidance#deciding-the-number-of-local-decimals-for-your-solana-oft) for an example of how the local decimals value affects the max supply ceiling. ### Check Minter and Burner Permissions When using mint-and-burn Adapters such as `MintAndBurnOFTAdapter`, ensure that the Adapter has the required roles to mint and burn the underlying token through the specified interface. ### Check Structured Codecs Use type-safe bytes codec for message encoding. Use custom codecs only if necessary and if your app requires deep optimization. Examples: * [EVM OFT](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/oapp/contracts/oft/libs/OFTMsgCodec.sol). * [Solana OFT](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/solana/programs/programs/oft/src/msg_codec.rs). ### Solana-Specific #### Avoid Enforcing Options Value to Initialize Accounts OFT sends to Solana to uninitialized token accounts **require additional options value** to [pay for ATA creation](../developers/solana/oft/overview#setting-options-inbound-to-solana). The first transfer of a specific token to a recipient will require value, but any subsequent transaction will not. **Static enforced options value should be avoided** to deal with it, as it'd keep overpaying after the first send. Nonetheless, enforcing options for regular gas consumption and other value requirements is still recommended in Solana. Examples: * First OFT send [transaction](https://testnet.layerzeroscan.com/tx/4jatt3yWnyzYcdatJkMziKvw5seJkFFobjVKfJ1Qv5pbSgpLHHdeoeGSCMec6WcUruS5D5tBfNwiuymWRDapGweY) to a Solana recipient. Note that the value received is non-zero, as it is used to pay for ATA creation of the token recipient. * Second OFT send [transaction](https://testnet.layerzeroscan.com/tx/2jtLoZPXBDAYhwJYVWMp7THjeTT2EVvuy8LnxyhLcKJ8VGgdgwiD2cpheJG2bzdStk76Y8H8wCUq13Ho1t87WY3p) to Solana recipient. Note that the SOL value sent is zero, as ATA is already created for the token recipient. ## 4. Authority & Ownership Transfers ### Check OApp Ownership Ensure the OApp owner is set or transferred to the intended address — and that the owner is a contract, not an EOA. **Do:** * Set `owner()` to a multisig (e.g., Safe), governance module, or timelock that requires more than one signer. * Document the signing policy alongside the deployment artifact. * Re-verify ownership after every chain rollout. **Don't:** * Leave `owner()` as a single externally-owned account (EOA). A single key compromise lets the attacker rewrite peers, libraries, and DVN/Executor config on every pathway. * Treat "we'll rotate later" as acceptable for mainnet — the rotation window is the exposure window. An EOA owner is a single point of failure for every pathway the OApp exposes. A compromise can re-point `peers`, swap libraries, or replace DVNs with attacker-controlled addresses; messages already in flight will be verified against the attacker's stack. Check [Solana reference](../developers/solana/technical-reference/solana-guidance#transferring-oft-ownership-on-solana). #### Validate it yourself ```bash wrap theme={null} OWNER=$(cast call "$OAPP_A" "owner()(address)" --rpc-url "$RPC_A") CODE=$(cast code "$OWNER" --rpc-url "$RPC_A") if [ "$CODE" = "0x" ]; then echo "FAIL: owner $OWNER is an EOA — move to a multisig or governance contract" else echo "OK: owner $OWNER is a contract" fi ``` Re-use the `$RPC_A` / `$OAPP_A` variables defined in [Self-Validation with `cast`](../developers/evm/configuration/dvn-executor-config#self-validation-with-cast). ### Check OApp Delegate Ensure the OApp delegate at the EndpointV2 is set or transferred to the intended address — and that the delegate, like the owner, is a contract, not an EOA. The delegate must be transferred before transferring ownership, as only the OApp owner can set the delegate. **Do:** * Set the delegate to a multisig or governance contract. * If the delegate differs from the owner, confirm the split is intentional — a separate delegate can modify DVN, library, and executor configuration **without** the owner's signature. * Treat the delegate as protocol-config root authority; review its signers with the same rigor as the owner's. **Don't:** * Leave the delegate as an EOA. The delegate can call `setSendLibrary`, `setReceiveLibrary`, and `setConfig` — a single key compromise can rewrite the entire protocol stack on that chain. * Assume "no delegate set" is safe; the zero address can be unrecoverable on some pathway configurations. An EOA delegate has the same blast radius as an EOA owner. A delegate compromise rewrites DVN/library/executor configuration silently; messages keep flowing through the attacker's stack until you notice. #### Validate it yourself ```bash wrap theme={null} DELEGATE=$(cast call "$ENDPOINT_A" "delegates(address)(address)" "$OAPP_A" --rpc-url "$RPC_A") CODE=$(cast code "$DELEGATE" --rpc-url "$RPC_A") if [ "$CODE" = "0x" ]; then echo "FAIL: delegate $DELEGATE is an EOA — a single key compromise can rewrite DVN/library/executor config" else echo "OK: delegate $DELEGATE is a contract" fi ``` #### Owner-Delegate Mismatch If owner and delegate are set to different addresses, confirm the split is intentional. The delegate can modify protocol-level configuration (libraries, DVNs, executor) without the owner's signature — splitting these roles widens the authority surface. **Do:** * Default to `owner == delegate` unless you have a specific reason to split them. * If you do split, ensure the delegate is held by an entity at least as trusted as the owner — for example, the same multisig members under a faster-rotation policy. * Document the split rationale next to the deployment artifact so an auditor can see why the two roles diverge. **Don't:** * Set the delegate to a more permissive group than the owner. * Leave a split that originated as a deployment expedient ("we used a deployer EOA temporarily") in place after launch. An attacker who compromises the delegate can re-route messages through their own DVN/library stack while the owner address remains untouched — a long-tail compromise that does not show up in owner monitoring. ##### Validate it yourself ```bash wrap theme={null} OWNER=$(cast call "$OAPP_A" "owner()(address)" --rpc-url "$RPC_A") DELEGATE=$(cast call "$ENDPOINT_A" "delegates(address)(address)" "$OAPP_A" --rpc-url "$RPC_A") if [ "$OWNER" = "$DELEGATE" ]; then echo "OK: owner == delegate ($OWNER)" else echo "MISMATCH: owner=$OWNER delegate=$DELEGATE" echo "Confirm this split is intentional — a delegate can modify protocol config without the owner's signature." fi ``` ### Check Upgradeable Contracts Admin Ensure proxy admin for upgradeable contracts or upgrade authority is set or transferred to the intended addresses. ### EVM-Specific #### Check Upgradeable Contracts Implementation Initialization Ensure implementation contracts for EVM upgradeable contracts disable initializers in the constructor. ```solidity wrap theme={null} contract MyOFTUpgradeable is OFTUpgradeable { constructor(address _lzEndpoint) OFTUpgradeable(_lzEndpoint) { _disableInitializers(); } function initialize(string memory _name, string memory _symbol, address _delegate) public initializer { __OFT_init(_name, _symbol, _delegate); __Ownable_init(_delegate); } } ``` ## 5. Testing Your Configuration After completing the checklist, validate your setup end‑to‑end: 1. **Send a test message A→B** * Use your OApp’s send function on Chain A. * Confirm the message appears and is delivered on a LayerZero explorer (for example, LayerZero Scan). 2. **Verify execution on Chain B** * Check destination chain logs/events and state changes in `OApp(B)`. 3. **Send a test message B→A** * Repeat the same steps in the opposite direction to validate bidirectional configuration. 4. **Test failure scenarios** * Intentionally underfund gas/value (in a test environment) to confirm your error handling and `enforcedOptions` work as intended. 5. **Repeat for every pathway** * For each new chain or pathway you add, repeat the full A→B and B→A test sequence. If any test fails, map the failure back to the relevant section in this checklist (peers, DVNs, executors, options, or ownership) and re‑verify the configuration. ## Usage Notes * This checklist is **production-focused**: it ensures pathway correctness, contract readiness, and monitoring preparedness. * It is **not a substitute for an audit**, but provides: * A systematic way to review OApp state. * Clear visibility into configuration consistency across chains. * Guidance on what Scan or external dashboards should surface automatically. * OFT/ONFT checks are categorized separately to avoid conflating with protocol-level messaging. ### References * [EVM Interactive Contract Playground](../developers/evm/contracts-playground) * [Production Deployment Checklist (Upgradeable OFT Example)](https://github.com/LayerZero-Labs/devtools/tree/main/examples/oft-upgradeable#production-deployment-checklist) # LayerZero Scan Swagger API Source: https://docs.layerzero.network/v2/tools/layerzeroscan/api Technical reference for LayerZero Scan Swagger API. Complete API documentation, function signatures, and implementation details for LayerZero V2. API documen... The LayerZero Scan API lets you programmatically look up crosschain messages and transactions. You can search by message ID, transaction hash, GUID, wallet address, or OApp address. Results include message state, source and destination transaction details, DVN verification status, and pathway configuration. Use it to track bridging activity, check message progress, or feed LayerZero data into your own dashboards. ## Available Methods The API exposes endpoints under a versioned path (e.g., `/v1/`), including: * **`/messages/latest`**\ Get the most recent messages. * **`/messages/pathway/{pathwayId}`**\ Retrieve messages associated with a specific pathway. * **`/messages/tx/{tx}`**\ Lookup messages using a transaction hash. * **`/messages/status/{status}`**\ List messages filtered by their current status. * **`/messages/month/{date}`**\ Get messages or statistics for a given month. * **`/messages/oapp/{eid}/{address}`**\ Fetch messages by endpoint ID and OApp address. * **`/messages/guid/{guid}`**\ Lookup messages by their unique GUID. * **`/messages/wallet/{srcAddress}`**\ Retrieve messages initiated by a specific wallet address. * **`/openapi`**\ Access the full OpenAPI specification for further integration details. For interactive testing, refer to the Swagger UI for your network: * **Testnet:** [https://scan-testnet.layerzero-api.com/v1/swagger](https://scan-testnet.layerzero-api.com/v1/swagger) * **Mainnet:** [https://scan.layerzero-api.com/v1/swagger](https://scan.layerzero-api.com/v1/swagger) ## Response status reference Each message returned by the API has status fields at multiple levels: a top-level message status, and sub-statuses for the source, verification, and destination stages. These tell you exactly where a message is in its lifecycle. For a high-level overview of what each message status means and how to debug it, see [Message Statuses Overview](/v2/concepts/troubleshooting/debugging-messages#message-statuses-overview). ### Message status The `status.name` field on each message object: | Status | Description | | ---------------------- | ----------------------------------------------------------- | | `INFLIGHT` | Waiting for source confirmation, verification, or execution | | `CONFIRMING` | Destination transaction submitted, waiting for finality | | `DELIVERED` | Delivered and executed on the destination chain | | `FAILED` | Delivered but execution failed on the destination chain | | `BLOCKED` | Cannot progress due to configuration issues | | `PAYLOAD_STORED` | Payload stored on destination, awaiting manual execution | | `APPLICATION_BURNED` | Burned by the receiving application | | `APPLICATION_SKIPPED` | Skipped by the receiving application | | `UNRESOLVABLE_COMMAND` | Command cannot be resolved (lzRead only) | | `MALFORMED_COMMAND` | Command is malformed (lzRead only) | ### Source status The `source.status` field tracks the source transaction through finality: | Status | Description | | ---------------------------- | ------------------------------------------------------------------------------------------ | | `WAITING` | Waiting for the source transaction to appear on-chain | | `VALIDATING_TX` | Transaction found; waiting for block confirmations to reach the configured finality window | | `SUCCEEDED` | Transaction confirmed on-chain after the configured finality window | | `WAITING_FOR_HASH_DELIVERED` | Waiting for transaction hash delivery (internal processing) | | `UNRESOLVABLE_COMMAND` | Command cannot be resolved (lzRead only) | | `MALFORMED_COMMAND` | Command is malformed (lzRead only) | ### Verification status The `verification.dvn.status` field tracks DVN quorum progress: | Status | Description | | ---------------- | ------------------------------------------------ | | `WAITING` | Waiting for DVN verifications | | `QUORUM_REACHED` | Required DVN quorum reached, pending commit | | `SUCCEEDED` | Verifications committed on the destination chain | The `verification.sealer.status` field tracks the commit (sealing) of DVN verifications on the destination chain: | Status | Description | | --------------- | ------------------------------------------------ | | `WAITING` | Waiting for verifications to be committed | | `VALIDATING_TX` | Commit transaction submitted | | `SUCCEEDED` | Verifications committed on the destination chain | | `FAILED` | Commit transaction failed | Individual DVN statuses are under `verification.dvn.dvns[address].status`: | Status | Description | | ------------------------ | -------------------------------------- | | `WAITING` | DVN has not yet submitted verification | | `VALIDATING_TX` | DVN verification transaction submitted | | `SUCCEEDED` | DVN verification confirmed | | `WAITING_FOR_ULN_CONFIG` | Waiting for ULN configuration | | `FAILED` | DVN verification failed | ### Destination status The `destination.status` field tracks `lzReceive` execution on the destination chain: | Status | Description | | -------------------------------- | ------------------------------------------------ | | `WAITING` | Waiting for execution | | `VALIDATING_TX` | Execution transaction submitted, confirming | | `SUCCEEDED` | Executed on the destination chain | | `FAILED` | Execution failed on the destination chain | | `SIMULATION_REVERTED` | `lzReceive` or `lzCompose` simulation reverted | | `PAYLOAD_STORED` | Payload stored, requires manual execution | | `RESOLVED_PAYLOAD_SIZE_NOT_PAID` | Resolved payload size fee not paid (lzRead only) | The `destination.nativeDrop.status` field tracks native token drops: | Status | Description | | --------------- | --------------------------------- | | `WAITING` | Native drop not yet executed | | `VALIDATING_TX` | Native drop transaction submitted | | `SUCCEEDED` | Native drop executed | | `FAILED` | Native drop failed | | `N/A` | No native drop for this message | The `destination.lzCompose.status` field tracks composed message execution: | Status | Description | | -------------------------------- | ---------------------------------- | | `WAITING` | `lzCompose` not yet called | | `VALIDATING_TX` | `lzCompose` transaction submitted | | `SUCCEEDED` | `lzCompose` executed successfully | | `FAILED` | `lzCompose` execution failed | | `SIMULATION_REVERTED` | `lzCompose` simulation reverted | | `N/A` | No compose for this message | | `WAITING_FOR_COMPOSE_SENT_EVENT` | Waiting for the compose sent event | ### Checking transaction finality To check whether a source transaction has reached finality, query the message by transaction hash and look at `source.status`: ```bash theme={null} curl -X 'GET' \ 'https://scan.layerzero-api.com/v1/messages/tx/{txHash}' \ -H 'accept: application/json' ``` The `source.status` field progresses as: ``` WAITING → VALIDATING_TX → SUCCEEDED ``` * **`VALIDATING_TX`**: The transaction has been found on-chain but the configured block confirmations have not been reached yet. DVNs will not begin verification until this completes. * **`SUCCEEDED`**: The required confirmations have passed. The transaction is finalized and the message is progressing through verification and execution. For OFTs, once `source.status` is `VALIDATING_TX` or later, the token debit (burn or lock) has occurred and the `PacketSent` event has been emitted. See [OFT atomicity](/v2/concepts/troubleshooting/debugging-messages#message-lifecycle) for more detail. # Get messagesguid Source: https://docs.layerzero.network/v2/tools/layerzeroscan/mainnet/messages/get-messagesguid /openapi/scan-mainnet.json get /messages/guid/{guid} Get messages by guid. # Get messageslatest Source: https://docs.layerzero.network/v2/tools/layerzeroscan/mainnet/messages/get-messageslatest /openapi/scan-mainnet.json get /messages/latest Get latest messages # Get messagesoapp Source: https://docs.layerzero.network/v2/tools/layerzeroscan/mainnet/messages/get-messagesoapp- /openapi/scan-mainnet.json get /messages/oapp/{eid}/{address} Get messages by endpoint id and oapp address. # Get messagespathway Source: https://docs.layerzero.network/v2/tools/layerzeroscan/mainnet/messages/get-messagespathway /openapi/scan-mainnet.json get /messages/pathway/{pathwayId} Get messages by pathway. # Get messagesstatus Source: https://docs.layerzero.network/v2/tools/layerzeroscan/mainnet/messages/get-messagesstatus /openapi/scan-mainnet.json get /messages/status/{status} Get messages by status. # Get messagestx Source: https://docs.layerzero.network/v2/tools/layerzeroscan/mainnet/messages/get-messagestx /openapi/scan-mainnet.json get /messages/tx/{tx} Get messages by transaction hash. # Get messageswallet Source: https://docs.layerzero.network/v2/tools/layerzeroscan/mainnet/messages/get-messageswallet /openapi/scan-mainnet.json get /messages/wallet/{srcAddress} Get messages by the wallet address the message originated from. # Get openapi Source: https://docs.layerzero.network/v2/tools/layerzeroscan/mainnet/openapi/get-openapi /openapi/scan-mainnet.json get /openapi OpenAPI specs. # LayerZero Scan Overview Source: https://docs.layerzero.network/v2/tools/layerzeroscan/overview Step-by-step guide to layerzero scan overview using LayerZero V2. Build and deploy omnichain applications with crosschain messaging. Follow step-by-step dev... LayerZero Scan is an explorer for observing and debugging crosschain messaging activity on LayerZero. Here’s how to get started with navigating it: ## What Is LayerZero Scan? **LayerZero Scan** is designed to display crosschain messaging details such as: * Transaction hashes and bridging events across multiple chains * Source and destination chain info * Status of messages (in-flight, delivered, failed) * Onchain addresses (contracts, wallets) participating in bridging The interface consolidates data from multiple blockchains to provide a single view of crosschain messaging. ## Key Sections in LayerZero Scan 1. **Search Bar** * Allows you to search by crosschain transaction hash, contract address, or user address. * If you have either the source or destination transaction, you can directly see that message’s status across source and destination. 2. **Recent Transactions / Messages** * Displays the most recent crosschain messages. * For each message, you can see: * The **source chain** and **destination chain** * A short snippet of addresses involved * A **timestamp** of when it was sent 3. **Detailed Message View** * When you select a transaction or message, you’ll see a breakdown of: * **Gas usage** * **Bridging fees** * **Source Tx Hash** (links to the chain’s native block explorer, e.g., Etherscan) * **Destination Tx Hash** (if it’s already executed) 4. **Address / Contract Page** * Searching for an address (or contract) shows all crosschain messages that address is involved in. * Great for debugging bridging from a specific user or checking on a particular protocol’s bridging activity. 5. **Default Configurations Per Chain Pathway** LayerZero Scan now includes a feature to check the default configuration settings for each chain pathway. This section lets you view and verify key settings that govern how messages are routed across chains. The default configuration display includes: * **From/To:** The source and destination chains for the pathway. * **Send Library:** The default library used for sending messages. * **Receive Library:** The default library used for receiving messages. * **DVN 1 & DVN 2:** The default Decentralized Verifier Networks used for message verification. * **Executor:** The default executor responsible for processing messages. * **Send Confirmations / Receive Confirmations:** The number of confirmations required on each side. A **Reset** option is provided to revert any custom configurations back to these defaults. | From/To | Send Library | Receive Library | DVN 1 | DVN 2 | Executor | Send Confirmations | Receive Confirmations | | ---------- | ------------ | --------------- | ----- | ----- | ---------- | ------------------ | --------------------- | | ETH/Solana | Library A | Library B | DVN A | DVN B | Executor X | 5 | 3 | 6. **Statistics / Additional Tabs** * Depending on the version, you might see stats like total messages, volume, or top bridging pairs. ## Why Use LayerZero Scan? * **Visibility**: View exactly how a crosschain transaction or bridging message was routed. * **Debugging**: If your crosschain message fails, you’ll see error statuses or partial deliveries. * **Confirming**: Verify that your crosschain transaction has reached finality. See [Message Statuses](/v2/concepts/troubleshooting/debugging-messages#confirming) and the [Scan API status reference](./api#response-status-reference) for details. ## Next Steps * If you want to automate data retrieval from these crosschain events, check out the [`LayerZero Scan API`](./api) or the [`Endpoint Metadata`](../endpoint-metadata) for programmatic solutions. ### Terms of Use By using the LayerZero Scan API, you agree to the [**LayerZero Scan API Terms of Use**](./terms). # LayerZero Scan API Terms of Use Source: https://docs.layerzero.network/v2/tools/layerzeroscan/terms Use Scan API Terms of Use with LayerZero V2. Developer tools for building and debugging omnichain applications. LayerZero enables crosschain messaging. **Terms of Use** Last Updated: July 23, 2025 ## 1. **Introduction** Welcome to [**LayerZeroScan API Mainnet**](https://scan.layerzero-api.com/v1/swagger) and [**LayerZeroScan API Testnet**](https://scan-testnet.layerzero-api.com/v1/swagger) (collectively, the "Scan API"), provided by LayerZero Labs Ltd. ("**LayerZero**", "**we**", "**our**", or "**us**"). The Scan API hosts the public-facing Swagger (OpenAPI) documentation for the LayerZeroScan application programming interface (API). The Scan API provides an API interface and related documentation to query and interact with blockchain related data indexed and made available on LayerZeroScan. The Scan API is designed primarily for developers and technical users. By accessing or using this Scan API, you agree to be bound by these Terms of Use (the “**Terms**”) and our Privacy Policy. If you do not agree to these Terms, you are not authorized to access or use the Scan API and should not use the Scan API. Please read these Terms carefully, as they include important information about your legal rights. You are solely responsible for determining whether your access to and use of the Scan API complies with the Terms as well as any applicable laws and regulations in your jurisdiction. For purposes of these Terms, “**you**” and “**your**” means you as the user of the Scan API. If you access or use the Scan API on behalf of a company or other entity then “you” includes both you in an individual capacity and that entity, and you represent and warrant that: (a) you are an authorized representative of the entity with the authority to bind the entity to these Terms; and (b) you agree to these Terms on the entity’s behalf, as well as on your individual behalf. PLEASE NOTE: THE "DISPUTE RESOLUTION" SECTION OF THESE TERMS CONTAINS AN ARBITRATION CLAUSE THAT REQUIRES DISPUTES TO BE ARBITRATED ON AN INDIVIDUAL BASIS, AND PROHIBITS CLASS ACTION CLAIMS. IT AFFECTS HOW DISPUTES BETWEEN YOU AND LAYERZERO ARE RESOLVED. BY ACCEPTING THESE TERMS, YOU AGREE TO BE BOUND BY THIS ARBITRATION PROVISION. PLEASE READ IT CAREFULLY. ## 2. **Modification of these Terms** LayerZero reserves the right, in its sole discretion, to modify these Terms from time to time. If any modifications are made, you will be notified by an update to the “Last Updated” date at the top of these Terms. All modifications will be effective when they are posted, or such later date as may be specified in the updated Terms, and your continued access or use of the Scan API after any modifications have become effective will serve as confirmation of your acceptance of those modifications. If you do not agree with any modifications to these Terms, you must immediately stop accessing or using the Scan API. ## 3. **Eligibility and Permitted Use** To access or use the Scan API, you must be able to form a legally binding contract with us. Accordingly, you represent that you are at least 18 years old or the age of majority in your jurisdiction and have the full right, power, and authority to enter into and comply with the terms and conditions of these Terms on behalf of yourself and any company or legal entity for which you may access or use the Scan API. You further represent that you are not (a) the subject of any economic or trade sanctions administered or enforced by any governmental authority, including any person designated on any list of prohibited or restricted parties by any governmental authority, including, without limitation, the European Union (“EU”) Consolidated List of Persons, Groups, and Entities, the United Kingdom (“UK”) Consolidated List of Financial Sanctions Targets (including as extended to the British Virgin Islands by statutory instrument), the United States (“U.S.”) Treasury Department’s list of Specially Designated Nationals, and any other lists or sanctions programs managed by the Office of Foreign Assets Control (“OFAC”) of the U.S. Department of the Treasury; (b) located in, incorporated in, or otherwise organized or established in, or resident of, any country, territory, or jurisdiction that is the subject of comprehensive country-wide, territory-wide, or regional economic sanctions or embargoes or has been designated as “terrorist supporting” by the United Nations (“UN”) or any governmental authority of the EU, UK (including as extended to the British Virgin Islands by statutory instrument), the British Virgin Islands, or the U.S., including the OFAC of the U.S. Treasury Department or the Office of Financial Sanctions (“OFSI”) of HM Treasury of the UK; (c) owned or controlled by such persons or entities described in (a)-(b); or (d) accessing or using the Scan API on behalf of persons or entities described in (a)-(c). You acknowledge and agree that you are solely responsible for complying with all applicable laws of the jurisdiction you are a resident of, or located or accessing the Scan API from, and you represent that your access and use of the Scan API will fully comply with all applicable laws and regulations. By using the Scan API you represent and warrant that you meet these requirements, will not use the Scan API for any illegal activity or to engage in the “Prohibited Activities” as set forth below, and will not access or use the Scan API to conduct, promote, or otherwise facilitate any illegal activity. You further represent and warrant that you are not, will not, and will not attempt to access or use the Scan API via a virtual private network or any other similar means intended to circumvent the restrictions set forth herein. Subject to your strict compliance with these Terms, LayerZero grants you a **limited, non-exclusive, non-transferable, non-sublicensable, revocable license** to access and use the Scan API solely for your personal, non-commercial use, in each case in accordance with these Terms. If any software, content, or other materials owned or controlled by us are distributed or made available to you as part of your use of the Scan API, we hereby grant you a personal, non-assignable, non-sublicensable, non-transferrable, and non-exclusive license to download, access, and/or display such software, content, and/or materials provided to you as part of the Scan API, in each case for the sole purpose of enabling you to use the Scan API as permitted by these Terms. These licenses are provided solely to enable you to use and enjoy the benefit of the Scan API as intended by LayerZero and as permitted by these Terms. These licenses will terminate immediately if you breach any provision of these Terms or upon any termination or suspension of your access to the Scan API. You agree to review, understand, and comply with any and all applicable licenses, usage guidelines, terms of service, and technical documentation that are provided, directly or indirectly, through the Scan API, bundled with or referenced in any API(s), SDK(s), or libraries made available, directly or indirectly, through the Scan API, and/or otherwise published or made available by LayerZero from time to time, including, but not limited to, any updates, amendments, or successor versions of such materials. Your obligations hereunder shall include, but are not limited to: (A) respecting any constraints on data access, processing, storage, or redistribution as outlined in such documentation; (B) adhering to any permitted use cases, restrictions, or disclaimers associated with particular datasets or features; (C) following implementation requirements or recommendations specified in technical documents to ensure interoperability, security, and proper attribution; and (D) monitoring for and incorporating updates to the documentation or terms, as continued use of the Scan API after such updates constitutes acceptance of the revised materials. Failure to comply with any applicable licenses, guidelines, or documentation may result in immediate suspension or termination of access to the Scan API, and may subject you to legal liability. You agree to properly attribute any data or content retrieved via the Scan API to LayerZero. Your attribution obligations shall include, but are not limited to: (i) crediting LayerZero and the Scan API as the source of the data or content in any application, product, service, publication, or display that incorporates, visualizes, or redistributes such data or content; (ii) including any mandatory attribution statements, watermarks, logos, or links as specified from time to time; and (iii) preserving data and content integrity and refraining from modifying, omitting, or misrepresenting any portion of the data or content in any way, including, but not limited to, by presenting the data or content in a way that may be misleading or falsely suggest sponsorship, endorsement, or association with LayerZero. Failure to comply with attribution requirements may result in suspension or termination of access to the Scan API, and may constitute a violation of applicable intellectual property laws or licensing terms. You further agree to comply fully with any authentication, API key management, and rate limiting requirements as established by LayerZero from time to time. This obligation shall include, but is not limited to, strict adherence with all rate limits or usage quotas imposed by LayerZero, including those published in the API documentation or enforced via technical measures, and avoiding any automated or excessive request activity that may degrade, disrupt, or interfere with the performance or availability of the Scan API to other users. LayerZero reserves the right to monitor API usage and enforce limits through throttling, suspension, or revocation of access where misuse, abuse, or violations are detected. Violations of this section may also result in legal action, especially if such actions compromise the security, integrity, or availability of the Scan API. Your access and use of the Scan API may be interrupted from time to time for any or for no reason, including, without limitation, in the event of the malfunction of equipment, periodic updating, maintenance, or repair of the Scan API or other actions that LayerZero, in its sole discretion, may elect to take. WITHOUT PREJUDICE TO ANY OTHER RIGHTS OF LAYERZERO UNDER THESE TERMS, LAYERZERO RESERVES THE RIGHT TO, AT ITS SOLE DISCRETION AND WITHOUT PRIOR NOTICE, SUSPEND, LIMIT, OR TERMINATE ACCESS TO OR USE OF THE SCAN API AT ANY TIME, FOR ANY REASON OR NO REASON. YOU AGREE THAT LAYERZERO SHALL HAVE NO LIABILITY TO YOU OR ANY THIRD PARTY FOR ANY INABILITY TO ACCESS OR USE THE SCAN API, OR FOR ANY SUSPENSION OR TERMINATION OF ACCESS TO OR USE OF THE SCAN API. All rights not expressly granted to you under these Terms are reserved by LayerZero and its licensors. ## 4. **Proprietary Rights** The Scan API, including, without limitation, its “look and feel” (e.g., text, graphics, images, logos), content, functionality, APIs, documentation, data, features, software, trademarks, service marks, copyrights, patents, and designs as well as any other proprietary content, information, and material (collectively, the “**Scan API IP**”), are the exclusive property of LayerZero and its related entities and are protected under copyright, trademark, and other intellectual property laws. You agree that LayerZero, its related entities, and/or its licensors exclusively own all right, title, and interest in and to the Scan API IP (including any and all intellectual property rights therein) and you agree not to take any action(s) inconsistent with such ownership interests. You agree not to remove, alter, or obscure any copyright, trademark, service mark, or other proprietary rights notices incorporated in or accompanying the Scan API. LayerZero reserves all rights in connection with the Scan API and its content. ## 5. **Additional Rights** LayerZero reserve the following rights: (a) with or without prior notice to you, to modify, substitute, eliminate, or add to the Scan API; (b) to review, modify, filter, disable, delete, and remove the Scan API, including any and all content and information associated with it; and (c) to cooperate with any law enforcement agency, court order, government investigation or order, or third party requesting and requiring, directing that we disclose information or content that you provide. ## 6. **Prohibited Activities** You agree not to engage in, or attempt to engage in, or do any of the following categories of prohibited activities in connection with your access and/or use of the Scan API, unless applicable laws or regulations prohibit these restrictions or you have our written permission to do so: (a) modify, copy, distribute, transmit, display, perform, reproduce, duplicate, publish, license, create derivative works from, or offer for sale any information contained on, or obtained from or through, the Scan API, except for temporary files that are automatically cached by your web browser for display purposes, or as otherwise expressly permitted in these Terms; (b) remove, alter, or obscure, any copyright, trademark, service mark, trade name, slogan, logo, image, or other proprietary notation displayed on or through the Scan API; (c) any activity that infringes on or violates any copyright, trademark, service mark, patent, right of publicity, right of privacy, or other proprietary or intellectual property rights under the law; (d) use automation software (bots), hacks, modifications (mods) or any other unauthorized third-party software designed to access, use, or modify the Scan API; (e) access or use the Scan API in any manner that could disable, overburden, damage, disrupt, degrade, or impair the Scan API or its performance, or interfere with any other party’s access to or use of the Scan API, including, without limitation, by exceeding any rate limit(s) imposed, or use any device, software, or routine that causes the same; (f) attempt to gain unauthorized access to, interfere with, damage, or disrupt the Scan API, or the computer systems or networks connected to the Scan API; (g) circumvent, remove, alter, deactivate, degrade, or thwart any technological measure or content protections of the Scan API or any of the computer systems, wallets, accounts, protocols, or networks connected to the Scan API; (h) use any robot, spider, crawlers, or other automatic device, process, software, or queries that intercepts, “mines,” scrapes, or otherwise accesses the Scan API to monitor, extract, copy, or collect information or data from or through the Scan API, or engage in any manual process to do the same; (i) introduce any viruses, trojan horses, worms, logic bombs, or other materials that are malicious or technologically harmful into our systems; (j) any activity that seeks to interfere with or compromise the integrity, security, or proper functioning of any computer, server, network, personal device, or other information technology system, including (but not limited to) the deployment of viruses and denial of service attacks; (k) impersonate any other person or entity using the Scan API, including by falsely stating, implying, or otherwise misrepresenting your affiliation with any person or entity; (l) any activity that seeks to defraud us or any other person or entity, including (but not limited to) providing any false, inaccurate, or misleading information in order to unlawfully obtain the property of another; (m) violate any applicable law, rule, or regulation of a relevant jurisdiction in connection with your access to or use of the Scan API, including, without limitation, any restrictions or regulatory requirements of Canada, the United States, or the British Virgin Islands; or (n) access or use the Scan API in any way not expressly permitted by these Terms. ## 7. **Feedback** You acknowledge and expressly agree that any contribution by you of any bug report, comment, idea, enhancement and/or enhancement request, recommendation, proposal, correction, suggestion for improvement(s), or other feedback of any kind, in any forum, with respect to the Scan API (**“Feedback”**) shall become the sole and exclusive property of LayerZero and does not and will not give or grant you any right, title, or interest in or to the Scan API, Scan API IP, or any such Feedback. You agree that LayerZero may use and disclose Feedback in any manner and for any purpose whatsoever without further notice or compensation to you, and without retention by you of any proprietary or other right or claim. You hereby irrevocably assign to LayerZero any and all right, title, and interest (including, but not limited to, any patent, copyright, trade secret, trademark, show-how, know-how, moral rights, and any and all other intellectual property right) that you may have in and to any and all Feedback, and, to the extent that any rights in and to Feedback cannot be assigned (including without limitation any moral rights), you hereby agree to waive such rights. To the extent that any Feedback is not assignable, you hereby grant to LayerZero a fully paid up, royalty-free, worldwide, perpetual, exclusive, irrevocable, sublicensable (with the right to sublicense through multiple tiers), and transferable right and license to use, create derivative works of, reproduce, re-format, perform, display, adapt, modify, distribute, and commercialize, or otherwise commercially or non-commercially exploit in any manner, any and all such Feedback for any purpose, including, but not limited to, by incorporating any Feedback into the Scan API. ## 8. **Privacy** All information collected on the Scan API is subject to our Privacy Policy. By using the Scan API, you consent to all actions taken by us with respect to any collection and/or use of your information in compliance with the Privacy Policy which further describes how we handle the information you provide to us when you use the Scan API. For an explanation of our privacy practices, visit our Privacy Policy located on the Scan API. Without limiting anything in the Privacy Policy, you acknowledge and agree that when you use the Scan API you may be interacting with public blockchains, which provide transparency into your transactions. LayerZero does not control and is not responsible for any information you make public on any public blockchain by taking actions through the Scan API. ## 9. **Third Party Services and Materials** The Scan API may display, include, reference, link to (including links to third-party websites), or otherwise make available services, products, promotions, content, data, information, resources, applications, or any other material(s) made available by a third-party or by third-parties (“**Third-Party Services and Materials**”). All Third-Party Services and Materials are made available solely as a convenience, and LayerZero does not own, control, or endorse any Third-Party Services and Materials. You agree that your access and use of such Third-Party Services and Materials is governed solely by the terms and conditions of such Third-Party Services and Materials, as applicable. LayerZero is not responsible or liable for, and makes no representations as to any aspect of such Third-Party Services and Materials, including, without limitation, their content, availability, or the manner in which they handle, protect, manage, or process data, or any interaction between you and the provider of such Third-Party Services and Materials. Any statements and/or opinions expressed by or through any Third-Party Services and Materials by any third-party or third-parties is/are solely the opinion(s) and the responsibility of the person or entity providing those materials. LayerZero is not responsible for examining or evaluating the content, accuracy, completeness, availability, timeliness, validity, copyright compliance, legality, decency, quality, or any other aspect of such Third-Party Services and Materials or websites. You irrevocably waive any claim against LayerZero with respect to such Third-Party Services and Materials. LayerZero is not liable for any damage or loss caused or alleged to be caused by or in connection with your enablement, access, or use of any such Third-Party Services and Materials, or your reliance on the privacy practices, data security processes, or other policies of such Third-Party Services and Materials. Third-Party Services and Materials and links to other websites are provided solely as a convenience to you and you access and/or use them at your own risk. ## 10. **Not Registered with FinCEN or Any Agency; No Advice Given** LayerZero is not registered with the Financial Crimes Enforcement Network as a money services business or in any other capacity. You understand and acknowledge that we are not a marketplace facilitator, a financial institution, broker, exchange, clearing house, or creditor, nor do we broker trading orders on your behalf or match orders for buyers and sellers of securities. While we provide infrastructure to transmit crosschain messages via the LayerZero Protocol, we are not involved in executing or settling trades (all such activity takes place directly between users entirely on public distributed blockchains). You acknowledge and agree that all transfers, liquidity pooling, farming, staking, or any other actions you may undertake via the Scan API (or via any other platform based on information available on the Scan API) are unsolicited. This means you have not received, nor relied upon, any investment advice from us regarding such actions. Furthermore, we do not assess the suitability of any such actions for you. You alone are responsible for determining whether any investment, investment strategy, or related transaction is appropriate for you based on your personal investment objectives, financial circumstances, and risk tolerance. The Scan API does not provide, and does not purport to provide, any financial, investment, legal, or tax advice or services. Use of the Scan API should not be construed as an offer or solicitation to buy or sell any financial instrument or as a recommendation to engage in any transaction. You should consult with qualified professionals before making any financial decisions. The Scan API is intended solely as a technical interface to decentralized systems, and no fiduciary relationship is created between you and the Scan API or LayerZero. ## 11. **Scan API for General Information Purposes Only** All information provided on or through the Scan API is made available solely for general informational purposes. LayerZero does not warrant the accuracy, completeness, or usefulness of this information. Any reliance you place on such information is strictly at your own risk. LayerZero disclaims all liability and responsibility arising from any reliance placed on such materials by you or any other visitor to the Scan API, or by anyone who may be informed of any of its contents. ## 12. **No Warranties** THE SCAN API IS PROVIDED ON AN "AS-IS" AND "AS-AVAILABLE" BASIS, WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, OR NON-INFRINGEMENT. TO THE FULLEST EXTENT PERMITTED BY LAW, LAYERZERO WILL NOT BE LIABLE FOR ANY DAMAGES OF ANY KIND ARISING OUT OF OR RELATED TO YOUR ACCESS TO OR USE OF THE SCAN API, INCLUDING, BUT NOT LIMITED TO, ANY DIRECT, INDIRECT, INCIDENTAL, PUNITIVE, EXEMPLARY, SPECIAL, OR CONSEQUENTIAL DAMAGES, EVEN IF LAYERZERO HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. YOU ACKNOWLEDGE AND AGREE THAT YOUR ACCESS TO AND USE OF THE SCAN API WILL BE AT YOUR SOLE RISK, AND THAT LAYERZERO SHALL NOT BE LIABLE FOR ANY LOSS OR DAMAGE THAT MAY RESULT FROM YOUR ACCESS TO OR USE OF THE SCAN API, INCLUDING, BUT NOT LIMITED TO: YOUR INABILITY TO ACCESS OR USE THE SCAN API; MODIFICATION, SUSPENSION, OR TERMINATION OF THE SCAN API; ERRORS, OMISSIONS, INTERRUPTIONS, DELAYS, OR TRANSMISSION FAILURES; UNAUTHORIZED ACCESS TO OR ALTERATION OF ANY TRANSMISSION OR DATA; ANY TRANSACTION OR AGREEMENT ENTERED INTO THROUGH THE SCAN API; ANY ACTIVITIES, CONDUCT, CONTENT, OR COMMUNICATIONS OF THIRD PARTIES; OR ANY DATA OR MATERIAL OBTAINED FROM A THIRD PARTY SOURCE ON OR THROUGH THE SCAN API. LAYERZERO MAKES NO WARRANTIES OR REPRESENTATIONS REGARDING THE ACCURACY, COMPLETENESS, OR RELIABILITY OF THE SCAN API OR ANY CONTENT LINKED TO OR ACCESSED THROUGH IT. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, LAYERZERO EXPRESSLY DISCLAIMS ANY LIABILITY FOR ANY (1) ERRORS, MISTAKES, INACCURACIES, OR OMISSIONS IN ANY CONTENT OR MATERIALS, (2) PERSONAL INJURY OR PROPERTY DAMAGE, OF ANY NATURE WHATSOEVER, RESULTING FROM YOUR ACCESS TO AND/OR USE OF THE SCAN API, (3) UNAUTHORIZED ACCESS TO OR USE OF LAYERZERO’S SECURE SERVERS AND/OR ANY AND ALL PERSONAL OR FINANCIAL INFORMATION STORED THEREIN, (4) INTERRUPTION, SUSPENSION, CESSATION, OR TERMINATION OF TRANSMISSION TO OR FROM THE SCAN API, (5) BUGS, VIRUSES, TROJAN HORSES, OR OTHER HARMFUL CODE THAT MAY BE TRANSMITTED TO OR THROUGH THE SCAN API BY ANY THIRD PARTY, AND (6) LOSS OR DAMAGE INCURRED AS A RESULT OF THE USE OF ANY CONTENT OR MATERIALS MADE AVAILABLE THROUGH THE SCAN API, WHETHER POSTED, TRANSMITTED, OR OTHERWISE DISSEMINATED. IF YOU ARE DISSATISFIED WITH THE SCAN API, YOU AGREE THAT YOUR SOLE AND EXCLUSIVE REMEDY SHALL BE FOR YOU TO DISCONTINUE YOUR USE OF THE SCAN API. CERTAIN JURISDICTIONS DO NOT PERMIT THE EXCLUSION OR LIMITATION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES; AS SUCH, THE FOREGOING LIMITATIONS AND EXCLUSIONS MAY NOT APPLY TO YOU TO THE EXTENT PROHIBITED BY APPLICABLE LAW. ## 13. **Non-Custodial and No Fiduciary Duties** The Scan API is a non-custodial application. This means that you alone are responsible for managing and securing the private cryptographic keys associated with your digital asset wallets. These Terms are not intended to, and do not, create or impose any fiduciary duties on us. To the maximum extent permitted by applicable law, you acknowledge and agree that we owe no fiduciary duties or liabilities to you or to any other party. Any such duties or liabilities that may otherwise exist at law or in equity are hereby fully disclaimed and waived. You further agree that our only duties and obligations are those explicitly set forth in these Terms. ## 14. **Assumption of Risk** By accessing and using the Scan API, you represent that you are financially and technically sophisticated enough to understand the inherent risks associated with using cryptographic and blockchain-based systems, and that you have a working knowledge of the usage and intricacies of blockchain technologies, cryptocurrencies, and other digital assets, storage mechanisms, and blockchain-based software systems to be able to assess and evaluate the risks and benefits of the Scan API contemplated hereunder, and will bear the risks thereof, including loss of all amounts paid or stored, and the risk that the cryptocurrencies and other digital assets may have little or no value. You understand that blockchain-based transactions are irreversible. You acknowledge that there are inherent risks associated with using or interacting with public blockchains and blockchain technology. There is no guarantee that such technology will be available or not subject to errors, hacking, or other security risks. Blockchain protocols may also be subject to sudden changes in operating rules, including forks, and it is your responsibility to make yourself aware of upcoming operating changes. You acknowledge and agree that there are risks associated with purchasing and holding cryptocurrency. These include, but are not limited to, risk of losing access to cryptocurrency or digital assets due to slashing; loss of private key(s); custodial error or purchaser or user error; risk of mining, staking, or blockchain-related attacks; risk of hacking and security weaknesses; risk of unfavorable regulatory intervention in one or more jurisdictions; risk related to token taxation; risk of personal information disclosure; risk of uninsured losses; volatility risks; and unanticipated risks. You further understand that the markets for digital assets are highly volatile due to factors including (but not limited to) adoption, speculation, technology, security, and regulation. You acknowledge and accept that the cost and speed of transacting with cryptographic and blockchain-based systems are variable and may increase dramatically at any time. You further acknowledge and accept the risk that your digital assets may lose some or all of their value and that you may suffer loss due to the fluctuation of prices of tokens. You understand that anyone can create a token, including fake versions of existing tokens and tokens that falsely claim to represent projects, and acknowledge and accept the risk that you may mistakenly trade those or other tokens. You further acknowledge that we are not responsible for any of these variables or risks and cannot be held liable for any resulting losses that you experience while accessing or using the Scan API. Accordingly, you understand and agree to assume full responsibility for all of the risks of accessing and using the Scan API. You further acknowledge and agree that your access and use of the Scan API may be interrupted from time to time for any or for no reason, including, without limitation, in the event of the malfunction of equipment, periodic updating, maintenance or repair of the Scan API or other actions that LayerZero, in its sole discretion, may elect to take. You agree that we shall have no liability to you arising from or related to any inability to access or use the Scan API. ## 15. **Third-Party Beneficiaries** You and LayerZero acknowledge and agree that LayerZero’s affiliates, subsidiaries, related companies, service providers, and its and their officers, directors, supervisors, consultants, advisors, agents, representatives, partners, employees, and licensors are third party beneficiaries of these Terms. ## 16. **Release of Claims** You expressly agree that you assume all risks in connection with your access and use of the Scan API. You further expressly waive and release us from any and all liability, claims, causes of action, or damages arising from or in any way relating to your access or use of the Scan API. If you are a California resident, you waive the benefits and protections of California Civil Code § 1542, which provides: "\[a] general release does not extend to claims that the creditor or releasing party does not know or suspect to exist in his or her favor at the time of executing the release and that, if known by him or her, would have materially affected his or her settlement with the debtor or released party." ## 17. **Indemnity** You agree to hold harmless, release, defend, and indemnify us, our affiliates, subsidiaries, related companies, service providers, and its and their officers, directors, employees, contractors, and agents from and against all claims, damages, obligations, losses, liabilities, costs, and expenses (including attorneys’ fees and costs) arising out of or in connection with: (a) your access and use, or misuse, of the Scan API; (b) your violation of any term or condition of these Terms, the right of any third party, or any other applicable law, rule, or regulation; (c) your dishonesty, negligence, fraudulence, or willful misconduct; and (d) any other party's access and use, or misuse, of the Scan API with your assistance or using any device or account that you own or control. ## 18. **Limitation of Liability** TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL LAYERZERO, OR ITS OFFICERS, DIRECTORS, EMPLOYEES, CONTRACTORS, AGENTS, AFFILIATES, OR SUBSIDIARIES, BE LIABLE TO YOU FOR ANY INDIRECT, PUNITIVE, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR EXEMPLARY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA, OR OTHER INTANGIBLE LOSSES, ARISING OUT OF OR RELATING TO YOUR ACCESS TO OR USE OF THE SCAN API, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. LAYERZERO SHALL NOT BE LIABLE FOR ANY DAMAGE, LOSS, OR INJURY RESULTING FROM HACKING, TAMPERING, OR OTHER UNAUTHORIZED ACCESS TO OR USE OF THE SCAN API OR THE INFORMATION CONTAINED THEREIN. WITHOUT LIMITING THE FOREGOING, LAYERZERO ASSUMES NO LIABILITY OR RESPONSIBILITY FOR ANY: (A) ERRORS, MISTAKES, INACCURACIES, OR OMISSIONS IN ANY CONTENT OR MATERIALS; (B) PERSONAL INJURY OR PROPERTY DAMAGE, OF ANY NATURE WHATSOEVER, RESULTING FROM ANY ACCESS TO OR USE OF THE SCAN API; (C) UNAUTHORIZED ACCESS TO OR USE OF ANY SECURE SERVER OR DATABASE UNDER OUR CONTROL, OR ANY DATA STORED THEREIN; (D) INTERRUPTION OR CESSATION OF ANY FUNCTION RELATED TO THE SCAN API; (E) BUGS, VIRUSES, TROJAN HORSES, OR OTHER HARMFUL CODE THAT MAY BE TRANSMITTED VIA THE SCAN API; (F) ANY CONTENT MADE AVAILABLE THROUGH THE SCAN API, OR ANY LOSS OR DAMAGE ARISING FROM ITS USE; OR (G) THE DEFAMATORY, OFFENSIVE, OR UNLAWFUL CONDUCT OF ANY THIRD PARTY. IN NO EVENT SHALL LAYERZERO’S, OR ANY OF ITS OFFICERS, DIRECTORS, EMPLOYEES, CONTRACTORS, AGENTS, AFFILIATES, OR SUBSIDIARIES, TOTAL AGGREGATE LIABILITY TO YOU FOR ANY AND ALL CLAIMS, PROCEEDINGS, LIABILITIES, OBLIGATIONS, DAMAGES, LOSSES, OR COSTS EXCEED THE AMOUNT YOU PAID TO US IN EXCHANGE FOR ACCESS TO AND USE OF THE SCAN API, OR USD\$50.00, WHICHEVER IS GREATER. THIS LIMITATION OF LIABILITY APPLIES REGARDLESS OF THE FORM OF ACTION, WHETHER BASED IN CONTRACT, TORT, NEGLIGENCE, STRICT LIABILITY, OR OTHERWISE, AND EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH LIABILITY. CERTAIN JURISDICTIONS DO NOT PERMIT THE EXCLUSION OR LIMITATION OF CERTAIN WARRANTIES OR LIABILITIES. ACCORDINGLY, CERTAIN OF THE FOREGOING DISCLAIMERS AND LIMITATIONS MAY NOT APPLY TO YOU TO THE EXTENT PROHIBITED BY APPLICABLE LAW. THIS LIMITATION OF LIABILITY SHALL APPLY TO THE FULLEST EXTENT PERMITTED BY LAW. ## 19. **Dispute Resolution** a. **READ THIS SECTION CAREFULLY** – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT AND TO HAVE A JURY HEAR YOUR CLAIMS. IT CONTAINS PROCEDURES FOR MANDATORY BINDING ARBITRATION AND A CLASS ACTION WAIVER. b. **Informal Process First.** You and LayerZero agree that in the event of any dispute, claim, or controversy arising out of or relating to these Terms or the breach, termination, enforcement, interpretation, or validity thereof or the access and use of the Scan API (individually, a “Dispute”, and collectively, the “**Disputes**”) between you and LayerZero, you must contact us by sending an email to [notices@layerzerolabs.org](mailto:notices@layerzerolabs.org). You and LayerZero agree to make a good faith sustained effort to resolve any Dispute before resorting to more formal means of resolution, including without limitation, any court action. Both you and LayerZero agree that this dispute resolution procedure is a condition precedent which must be satisfied before initiating any arbitration against the other party. Nothing in this clause shall prevent a party from seeking interim or provisional relief where it is reasonably necessary to do so. c. Mandatory Arbitration of Disputes. If the informal dispute resolution process should fail to produce a satisfactory result within sixty (60) days of your email, or if any Dispute or portion thereof remains unresolved following such process, we each agree that such Dispute will be resolved by binding, individual arbitration pursuant to the following provisions of this “Dispute Resolution” clause, and not in a class, representative, or consolidated action or proceeding. You and LayerZero agree that British Virgin Islands law governs the interpretation and enforcement of these Terms. This arbitration provision shall survive termination of these Terms. e. **Exceptions.** As limited exceptions to the provisions of this “Dispute Resolution” clause: (i) we both may seek to resolve a Dispute in the Magistrate’s Court of the British Virgin Islands if it qualifies; and (ii) we each retain the right to seek injunctive or other equitable relief from a court to prevent (or enjoin) the infringement or misappropriation of our intellectual property rights. Conducting Arbitration and Arbitration Rules. Any Disputes arising out of or relating to these Terms, including the existence, validity, interpretation, performance, breach, or termination thereof, or any Dispute regarding non-contractual obligations arising out of or relating to them, shall be referred to and finally resolved by binding arbitration to be administered by the BVI International Arbitration Centre (“BVI IAC”) in accordance with the BVI IAC Arbitration Rules (the “Arbitration Rules”) in force as at the date of these Terms, which Arbitration Rules are deemed to be incorporated by reference into these Terms. The arbitration shall be conducted in the English language and the seat of arbitration shall be in Road Town, Tortola, British Virgin Islands. The arbitration shall be determined by a sole arbitrator to be appointed in accordance with the Arbitration Rules. The decision of the sole arbitrator shall be in writing and shall be final and binding upon both parties without any right of appeal, and judgment upon any award thus obtained may be entered in or enforced by any court having jurisdiction thereof. No action at law or in equity based upon any claim arising out of or in relation to these Terms shall be instituted in any court of any jurisdiction, except as specifically permitted herein. Each party waives any right it may have to assert the doctrine of forum non conveniens, to assert that it is not subject to the jurisdiction of such arbitration or courts, or to object to venue, to the extent any proceeding is brought in accordance herewith. f. **Arbitration Costs.** Responsibility for payment of all filing, administration, and arbitrator fees will be governed by the Arbitration Rules. We each agree that the prevailing party in arbitration will be entitled to an award of attorneys’ fees and expenses to the extent provided under applicable law. g. **Injunctive and Declaratory Relief.** Except as provided in the “Exceptions” section above, the arbitrator shall determine all issues of liability on the merits of any claim asserted by either party and may award declaratory or injunctive relief only in favor of the individual party seeking relief and only to the extent necessary to provide relief warranted by that party’s individual claim. h. **Class Action and Jury Trial Waiver.** **YOU AND LAYERZERO AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS ACTION, COLLECTIVE ACTION, PRIVATE ATTORNEY GENERAL ACTION, OR OTHER REPRESENTATIVE PROCEEDING.** Further, if the parties’ Dispute is resolved through arbitration, the arbitrator may not consolidate another person’s claims with your claims, and may not otherwise preside over any form of a representative or class proceeding. If this specific provision is found to be unenforceable, then the entirety of this Dispute Resolution section shall be null and void. You and we both agree to waive the right to demand a trial by jury. i. **Severability.** With the exception of any of the provisions in the immediately preceding paragraph of these Terms (“**Class Action Waiver**”), if an arbitrator or court of competent jurisdiction decides that any part of these Terms is invalid or unenforceable, the other parts of these Terms will still apply. ## 20. **Injunctive Relief** You agree that a breach of these Terms will cause irreparable injury to LayerZero for which monetary damages would not be an adequate remedy and LayerZero shall be entitled to equitable relief in addition to any remedies it may have hereunder or at law without a bond, other security or proof of damages. ## 21. **Force Majeure** We will not be liable or responsible to you, nor be deemed to have defaulted under or breached these Terms, for any failure or delay in fulfilling or performing any of our obligations under these Terms, when and to the extent such failure or delay is caused by or results from any events beyond our ability to control, including acts of God; flood, fire, earthquake, epidemics, pandemics, tsunami, explosion, war, invasion, hostilities (whether war is declared or not), terrorist threats or acts, riot or other civil unrest, government order, law, or action, embargoes or blockades, strikes, labor stoppages or slowdowns or other industrial disturbances, shortage of adequate or suitable Internet connectivity, telecommunication breakdown or shortage of adequate power or electricity, cyberattacks, Protocol-level disruptions, chain-level failures, governance attacks, and other similar events beyond our control. ## 22. **Miscellaneous** If any provision of these Terms shall be unlawful, void or for any reason unenforceable, then that provision shall be deemed severable from these Terms and shall not affect the validity and enforceability of any remaining provisions. These Terms and the licenses granted hereunder may be assigned by LayerZero but may not be assigned by you without the prior express written consent of LayerZero. LayerZero’s failure to enforce any right or provision of these Terms will not be considered a waiver of such right or provision. The waiver of any such right or provision will be effective only if in writing and signed by a duly authorized representative of LayerZero. Except as expressly set forth in these Terms, the exercise by either party of any of its remedies under these Terms will be without prejudice to its other remedies under these Terms or otherwise. The section headings used herein are for reference only and shall not be read to have any legal effect. ## 23. **Governing Law** You agree that the laws of the British Virgin Islands, without regard to principles of conflict of laws, govern these Terms and any Dispute between you and us. You further agree that the Scan API shall be deemed to be based solely in the British Virgin Islands, and that although the Scan API may be available in other jurisdictions, its availability does not give rise to general or specific personal jurisdiction in any forum outside the British Virgin Islands. You agree that the courts of the British Virgin Islands are the proper forum for any appeals of an arbitration award or for court proceedings in the event that the binding arbitration clause of these Terms is found to be unenforceable. ## 24. **Entire Agreement** These Terms, and the Privacy Policy, constitute the entire agreement between you and us with respect to the subject matter hereof. These Terms supersedes any and all prior or contemporaneous written and oral agreements, communications and other understandings (if any) relating to the subject matter of the terms. The information provided on the Scan API is not intended for distribution to or use by any person or entity in any jurisdiction or country where such distribution or use would be contrary to law or regulation or which would subject us to any registration requirement within such jurisdiction or country. Accordingly, those persons who choose to use or access the Scan API from other locations do so on their own initiative and are solely responsible for compliance with local laws, if and to the extent local laws are applicable. # Get messagesguid Source: https://docs.layerzero.network/v2/tools/layerzeroscan/testnet/messages/get-messagesguid /openapi/scan-testnet.json get /messages/guid/{guid} Get messages by guid. # Get messageslatest Source: https://docs.layerzero.network/v2/tools/layerzeroscan/testnet/messages/get-messageslatest /openapi/scan-testnet.json get /messages/latest Get latest messages # Get messagesoapp Source: https://docs.layerzero.network/v2/tools/layerzeroscan/testnet/messages/get-messagesoapp- /openapi/scan-testnet.json get /messages/oapp/{eid}/{address} Get messages by endpoint id and oapp address. # Get messagespathway Source: https://docs.layerzero.network/v2/tools/layerzeroscan/testnet/messages/get-messagespathway /openapi/scan-testnet.json get /messages/pathway/{pathwayId} Get messages by pathway. # Get messagesstatus Source: https://docs.layerzero.network/v2/tools/layerzeroscan/testnet/messages/get-messagesstatus /openapi/scan-testnet.json get /messages/status/{status} Get messages by status. # Get messagestx Source: https://docs.layerzero.network/v2/tools/layerzeroscan/testnet/messages/get-messagestx /openapi/scan-testnet.json get /messages/tx/{tx} Get messages by transaction hash. # Get messageswallet Source: https://docs.layerzero.network/v2/tools/layerzeroscan/testnet/messages/get-messageswallet /openapi/scan-testnet.json get /messages/wallet/{srcAddress} Get messages by the wallet address the message originated from. # Get openapi Source: https://docs.layerzero.network/v2/tools/layerzeroscan/testnet/openapi/get-openapi /openapi/scan-testnet.json get /openapi OpenAPI specs. # Connect Your AI Tools to LayerZero Docs Source: https://docs.layerzero.network/v2/tools/mcp/ide-setup Step-by-step guide to connect Cursor, VS Code, Claude, ChatGPT, Windsurf, JetBrains, Zed, and other AI tools to LayerZero documentation via MCP. Connect your AI tools to the LayerZero documentation MCP server so they can search the docs in real-time while generating responses. **MCP server URL:** ``` https://docs.layerzero.network/mcp ``` ## IDEs ### Option 1: One-click install Visit any page on [docs.layerzero.network](https://docs.layerzero.network) and click the contextual menu button (top-right). Select **Connect to Cursor** to install the MCP server automatically. ### Option 2: Manual configuration Add the server to your Cursor MCP config file: **Global (all projects):** `~/.cursor/mcp.json` **Project-level:** `/.cursor/mcp.json` ```json theme={null} { "mcpServers": { "layerzero-docs": { "url": "https://docs.layerzero.network/mcp" } } } ``` MCP tools are only available in Cursor's **Agent mode**, not in Ask or normal chat mode. ### Option 1: One-click install Visit any page on [docs.layerzero.network](https://docs.layerzero.network) and click the contextual menu button (top-right). Select **Connect to VS Code** to install the MCP server automatically. ### Option 2: Manual configuration Create or edit `.vscode/mcp.json` in your project root: ```json theme={null} { "servers": { "layerzero-docs": { "type": "http", "url": "https://docs.layerzero.network/mcp" } } } ``` VS Code uses `"servers"` as the top-level key (not `"mcpServers"`). Requires VS Code **1.99+** with GitHub Copilot. MCP tools only appear in Copilot **Agent Mode** (not inline or standard chat). Add the server to the Windsurf MCP config: **Config file:** `~/.codeium/windsurf/mcp_config.json` ```json theme={null} { "mcpServers": { "layerzero-docs": { "serverUrl": "https://docs.layerzero.network/mcp" } } } ``` You can also add it through the UI: open Windsurf Settings and navigate to **Cascade > MCP Servers**. Windsurf uses `"serverUrl"` for remote MCP servers, not `"url"`. Supported in IntelliJ IDEA, WebStorm, PyCharm, and other JetBrains IDEs (version **2025.2+**). 1. Open **Settings > Tools > AI Assistant > Model Context Protocol (MCP)**. 2. Click the **+** button. 3. Select **HTTP** as the connection type. 4. Paste the configuration: ```json theme={null} { "mcpServers": { "layerzero-docs": { "url": "https://docs.layerzero.network/mcp" } } } ``` 5. Click **OK**, then **Apply**. Requires the **AI Assistant plugin** to be installed and active. ## AI assistants Run a single command to add the server: ```bash wrap theme={null} claude mcp add --transport http layerzero-docs https://docs.layerzero.network/mcp ``` Verify it was added: ```bash wrap theme={null} claude mcp list ``` **Scope options:** | Flag | Scope | | :---------------- | :--------------------------------------------------- | | `--scope user` | Available across all your projects | | `--scope project` | Shared via `.mcp.json` at project root (committable) | Use `/mcp` inside a Claude Code session to check server status. ### Option 1: Connectors UI 1. Open Claude Desktop and go to **Settings > Connectors**. 2. Click **Add custom connector**. 3. Enter `LayerZero Docs` as the name and paste the URL: ``` https://docs.layerzero.network/mcp ``` 4. Click **Add**. 5. In a conversation, click the **attachments** button (plus icon) and select the LayerZero Docs connector. Requires a Claude **Pro**, **Max**, **Team**, or **Enterprise** plan. ### Option 2: Config file with mcp-remote Edit your Claude Desktop config file: | Platform | Path | | :------- | :---------------------------------------------------------------- | | macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` | | Windows | `%APPDATA%\Claude\claude_desktop_config.json` | | Linux | `~/.config/Claude/claude_desktop_config.json` | ```json theme={null} { "mcpServers": { "layerzero-docs": { "command": "npx", "args": [ "mcp-remote@latest", "https://docs.layerzero.network/mcp" ] } } } ``` The config file method requires **Node.js** to be installed since it uses the `mcp-remote` proxy. 1. Open [chatgpt.com](https://chatgpt.com) and go to **Settings > Apps & Connectors > Advanced settings**. 2. Toggle on **Developer Mode**. 3. In the **Connectors** section, click to create a new connector. 4. Paste the MCP server URL: ``` https://docs.layerzero.network/mcp ``` 5. Save and start a new chat. Requires a ChatGPT **Pro**, **Team**, **Business**, **Enterprise**, or **Edu** plan. Only available on the web version. Add the server to your Codex config: **Global:** `~/.codex/config.toml` **Project-level:** `/.codex/config.toml` ```toml theme={null} [mcp_servers.layerzero-docs] url = "https://docs.layerzero.network/mcp" ``` Or use the CLI: ```bash wrap theme={null} codex mcp add layerzero-docs --url https://docs.layerzero.network/mcp ``` Use `/mcp` inside a Codex session to check server status. Codex CLI supports **Streamable HTTP** only, not SSE. If you encounter connection issues, confirm the server transport is compatible. ## Other MCP clients Edit your Zed settings file (open via **Zed > Settings > Open Settings** or `Cmd+,`): ```json theme={null} { "context_servers": { "layerzero-docs": { "command": { "path": "npx", "args": ["-y", "mcp-remote", "https://docs.layerzero.network/mcp"] } } } } ``` Zed uses the `mcp-remote` wrapper for remote MCP servers. Requires **Node.js** to be installed. A green indicator dot in the Agent Panel confirms the server is connected. Add the server URL to your MCP client's configuration: ``` https://docs.layerzero.network/mcp ``` The server uses **HTTP transport** (Streamable HTTP). Most MCP clients auto-detect the transport type from the URL. ## Verifying the connection After setup, test that your AI tool can reach the LayerZero docs: 1. Open your AI tool and start a new conversation or agent session. 2. Ask a question about LayerZero, for example: * *"What is the OFT standard in LayerZero?"* * *"How do I configure DVN security for my OApp?"* * *"What chains does LayerZero support?"* 3. The AI should pull answers directly from the docs and include links to relevant pages. If the AI does not search the docs, check that: * The MCP server is enabled and connected (check your tool's MCP settings panel). * You are in **Agent mode** (Cursor, VS Code) rather than standard chat. * The server URL is exactly `https://docs.layerzero.network/mcp`. # MCP Server for LayerZero Docs Source: https://docs.layerzero.network/v2/tools/mcp/overview Connect AI tools directly to LayerZero documentation using the Model Context Protocol (MCP). Search docs in real-time from Cursor, VS Code, Claude, ChatGPT, and more. MCP (Model Context Protocol) is an open protocol that standardizes how AI tools connect to external data sources. LayerZero documentation includes a hosted MCP server that any compatible AI tool can query in real-time. ## MCP server ``` https://docs.layerzero.network/mcp ``` Any MCP-compatible AI tool (Claude Code, Cursor, VS Code Copilot, Windsurf, ChatGPT, Codex, and others) can connect to this server and search LayerZero documentation while generating responses. The AI determines when to search based on conversation context -- no manual triggering needed. ## How MCP differs from web search | | MCP | Web Search | | :---------- | :----------------------------------- | :------------------------------------ | | Source | Current indexed docs directly | Whatever search engines have crawled | | Freshness | Updates with the latest deployment | May be stale | | Noise | Only LayerZero documentation content | Includes SEO-ranked unrelated results | | Integration | Inline during AI response generation | Separate step | MCP gives the AI direct access to the latest version of the docs. It does not consume context until the AI actually calls the search tool, and it only searches when the query is relevant -- it will not search every connected server for every question. ## Quick setup Use the contextual menu on any docs page, or configure manually. One command to add the server to your CLI. Step-by-step setup for every supported IDE and AI tool. ## What it exposes The MCP server exposes a single **search tool** (`SearchLayerZero`) that AI applications can query. The tool accepts a `query` string and returns matching documentation pages with titles, links, and content snippets. The server uses **Streamable HTTP** transport and does not require session management -- each request is independent. ## Rate limits | Scope | Limit | | :---------------- | :-------------------- | | Per user (IP) | 200 requests / hour | | Per site (domain) | 1,000 requests / hour | ## Contextual menu Every page on the LayerZero docs includes a contextual menu (floating `Copy Page` button) with direct integrations for AI tools: * **Copy page** -- copies the current page as Markdown for pasting into any AI tool. * **Copy MCP server URL** -- copies `https://docs.layerzero.network/mcp` to your clipboard. * **Connect to Cursor** -- opens Cursor and installs the MCP server automatically. * **Connect to VS Code** -- opens VS Code and installs the MCP server automatically. * **Open in Claude / ChatGPT / Perplexity** -- opens a conversation with the current page as context. The fastest way to get started is to visit any docs page and use the contextual menu to connect your IDE directly. ## Next steps See the [IDE Setup Guide](/v2/tools/mcp/ide-setup) for step-by-step instructions for every supported tool. # Migrate to Simple Config Source: https://docs.layerzero.network/v2/tools/migrate-to-simple-config Use Migrate to Simple Config with LayerZero V2. Developer tools for building and debugging omnichain applications. LayerZero enables crosschain messaging. The LayerZero Simple Config Generator provides a streamlined approach to configuring your OApp connections across supported VMs (EVM, Solana, etc.). This guide will help you migrate from manual configuration to the Simple Config approach. **Current Support**: The Simple Config Generator currently supports EVM chains and Solana. Aptos support is not yet available. **Production deployments should use multiple required DVNs from independent operators.** The examples below use `` as a placeholder so the config does not silently resolve to a single-DVN configuration. Replace it with a real DVN name from [DVN Addresses](/v2/deployments/dvn-addresses) before wiring. See the [Integration Checklist](/v2/tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway) for production DVN guidance. ## Why Migrate to Simple Config? The Simple Config Generator offers several advantages over manual configuration: * **Reduced complexity**: Fewer configuration parameters to manage * **Automatic bidirectional connections**: Define one pathway, get both directions * **Streamlined defaults**: Sensible structure for DVN and executor configuration that you must review and override per pathway (see Warning below) * **Cross-VM compatibility**: Works seamlessly with EVM chains, Solana, and other supported VMs * **Less error-prone**: Automated configuration generation reduces manual errors * **VM-agnostic**: Same configuration approach works across different virtual machines ## Before Migration: Manual Configuration In the traditional manual approach, you would need to: 1. Define each connection direction separately 2. Manually specify send and receive libraries 3. Configure ULN settings for each direction 4. Set up executor configurations 5. Define enforced options for each pathway 6. Handle VM-specific configurations separately Here's an example of manual configuration for EVM chains: ```typescript wrap theme={null} connections: [ // ETH <--> ARB PATHWAY: START { from: ethereumContract, to: arbitrumContract, }, { from: arbitrumContract, to: ethereumContract, }, // ETH <--> ARB PATHWAY: END ]; // Then define config settings for each direction connections: [ { from: ethereumContract, to: arbitrumContract, config: { sendLibrary: contractsConfig.ethereum.sendLib302, receiveLibraryConfig: { receiveLibrary: contractsConfig.ethereum.receiveLib302, gracePeriod: BigInt(0), }, sendConfig: { executorConfig: { maxMessageSize: 10000, executor: contractsConfig.ethereum.executor, }, ulnConfig: { confirmations: BigInt(15), requiredDVNs: [ contractsConfig.ethereum.horizenDVN, contractsConfig.ethereum.polyhedraDVN, contractsConfig.ethereum.lzDVN, ], optionalDVNs: [], optionalDVNThreshold: 0, }, }, receiveConfig: { ulnConfig: { confirmations: BigInt(20), requiredDVNs: [ contractsConfig.ethereum.lzDVN, contractsConfig.ethereum.horizenDVN, contractsConfig.ethereum.polyhedraDVN, ], optionalDVNs: [], optionalDVNThreshold: 0, }, }, enforcedOptions: [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 65000, value: 0, }, { msgType: 2, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 65000, value: 0, }, { msgType: 2, optionType: ExecutorOptionType.COMPOSE, index: 0, gas: 50000, value: 0, }, ], }, }, // Repeat for the reverse direction... ]; ``` ## After Migration: Simple Config With the Simple Config Generator, the same configuration becomes much simpler and works across all VMs: ```typescript wrap theme={null} import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {OAppEnforcedOption, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const ethereumContract: OmniPointHardhat = { eid: EndpointId.ETHEREUM_V2_MAINNET, contractName: 'MyOFT', }; const arbitrumContract: OmniPointHardhat = { eid: EndpointId.ARBITRUM_V2_MAINNET, contractName: 'MyOFT', }; const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 65000, value: 0, }, ]; export default async function () { const connections = await generateConnectionsConfig([ [ ethereumContract, // Chain A contract arbitrumContract, // Chain B contract [['LayerZero Labs', ''], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [15, 20], // [A to B confirmations, B to A confirmations] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain A enforcedOptions, Chain B enforcedOptions ], ]); return { contracts: [{contract: ethereumContract}, {contract: arbitrumContract}], connections, }; } ``` ## Migration Steps ### 1. Install Required Dependencies ```bash wrap theme={null} pnpm add -D @layerzerolabs/metadata-tools ``` ### 2. Update Your Configuration File Replace your manual `layerzero.config.ts` with the Simple Config approach: ```typescript wrap theme={null} import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {OAppEnforcedOption, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; // Define your contracts const contractA: OmniPointHardhat = { eid: EndpointId.CHAIN_A_ENDPOINT_ID, contractName: 'YourContract', }; const contractB: OmniPointHardhat = { eid: EndpointId.CHAIN_B_ENDPOINT_ID, contractName: 'YourContract', }; // Define enforced options (gas settings for destination chain execution) const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 80000, value: 0, }, ]; export default async function () { const connections = await generateConnectionsConfig([ [ contractA, // Chain A contract contractB, // Chain B contract [['LayerZero Labs', ''], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [1, 1], // [A to B confirmations, B to A confirmations] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain A enforcedOptions, Chain B enforcedOptions ], ]); return { contracts: [{contract: contractA}, {contract: contractB}], connections, }; } ``` For EVM + Solana configurations: ```typescript wrap theme={null} import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {OAppEnforcedOption, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const evmContract: OmniPointHardhat = { eid: EndpointId.ETHEREUM_V2_MAINNET, contractName: 'MyOFT', }; const solanaContract: OmniPointHardhat = { eid: EndpointId.SOLANA_V2_MAINNET, address: 'YourSolanaAddress', // Required for Solana contracts }; const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 80000, value: 0, }, ]; const SOLANA_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 200000, value: 2039280, // SPL token account rent value in lamports }, ]; export default async function () { const connections = await generateConnectionsConfig([ [ evmContract, // EVM contract solanaContract, // Solana contract [['LayerZero Labs', ''], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [1, 1], // [EVM to Solana confirmations, Solana to EVM confirmations] [SOLANA_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Solana enforcedOptions, EVM enforcedOptions ], ]); return { contracts: [{contract: evmContract}, {contract: solanaContract}], connections, }; } ``` ### 3. Update Your Deployment Scripts Replace your manual wire commands with the Simple Config approach: ```bash wrap EVM Only theme={null} npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` ```bash wrap EVM + Solana theme={null} # Initialize Solana config (first time only) npx hardhat lz:oft:solana:init-config --oapp-config layerzero.config.ts # Wire all connections npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` ## Key Differences | Aspect | Manual Config | Simple Config | | ------------------------- | -------------------------------- | ------------------------------ | | **Connection Definition** | Define each direction separately | Define once, get bidirectional | | **DVN Configuration** | Manual specification | Automated with metadata | | **Executor Setup** | Manual configuration | Automated with metadata | | **Library Selection** | Manual specification | Automated with metadata | | **VM-Specific Handling** | Separate configs per VM | Unified approach | | **Configuration Length** | 100+ lines per pathway | \~20 lines per pathway | | **Error Prone** | High (manual configuration) | Low (automated generation) | ## Configuration Parameters Explained **Custom Metadata**: For advanced use cases, you can provide custom metadata by passing a `fetchMetadata` function to `generateConnectionsConfig`. This allows you to extend the default metadata with custom DVNs and executors. ### Pathway Definition ```typescript wrap theme={null} [ contractA, // Source chain contract contractB, // Destination chain contract [['LayerZero Labs', ''], []], // DVN configuration [15, 20], // Confirmations for each direction [optionsA, optionsB], // Enforced options for each direction ]; ``` ### DVN Configuration ```typescript wrap theme={null} [['LayerZero Labs', ''], []]; // [ requiredDVN[], [ optionalDVN[], threshold ] ] ``` * **Required DVNs**: Must verify the message for it to be considered valid * **Optional DVNs**: Additional verifiers (with threshold) for enhanced security ### Confirmations ```typescript wrap theme={null} [15, 20]; // [A to B confirmations, B to A confirmations] ``` The number of block confirmations to wait before considering a message verified. ### Enforced Options ```typescript wrap theme={null} const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, // Message type (1 = OFT, 2 = OApp) optionType: ExecutorOptionType.LZ_RECEIVE, // Option type gas: 80000, // Gas limit for destination execution value: 0, // Value to send (usually 0) }, ]; ``` ## VM-Specific Considerations ### EVM Chains * Use `contractName` for contract identification * Gas values represent actual gas units * Value is typically 0 ### Solana * Use `address` for contract identification (required) * Gas values represent compute units * Value represents lamports (typically 2039280 for SPL token account rent) **Aptos Support**: Aptos is not yet supported by the Simple Config Generator. Use manual configuration for Aptos integrations. ## Migration Checklist * [ ] Install `@layerzerolabs/metadata-tools` * [ ] Update `layerzero.config.ts` to use Simple Config format * [ ] Define your contract objects with correct EIDs * [ ] Configure enforced options for your use case * [ ] Set appropriate DVN requirements * [ ] Handle VM-specific requirements (`address` for Solana contract objects) * [ ] Test by running `npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts` (if there were no config changes, there should be no transactions to be submitted) ## Troubleshooting ### Common Issues 1. **Missing Dependencies**: Ensure `@layerzerolabs/metadata-tools` is installed 2. **Incorrect EndpointId**: Verify you are using the correct Endpoint ID (if using Endpoint V2, the constant name should include `V2`) 3. **VM-Specific Requirements**: Remember to specify `address` for Solana contracts and `value` for enforced options `msgType` 1 when sending to Solana. 4. **Gas Estimation**: Profile your contracts to set appropriate gas limits ### Getting Help If you encounter issues during migration: 1. Check the [Simple Config documentation](/v2/tools/simple-config) 2. Review the [examples in devtools](https://github.com/LayerZero-Labs/devtools/tree/main/examples) 3. Consult VM-specific documentation. The Simple Config Generator significantly reduces the complexity of LayerZero configuration while maintaining the same functionality and security guarantees as manual configuration. # LayerZero Tools Overview Source: https://docs.layerzero.network/v2/tools/overview Step-by-step guide to layerzero tools overview using LayerZero V2. Build and deploy omnichain applications with crosschain messaging. Follow step-by-step de... This section of the documentation covers the key resources that help developers inspect and integrate with LayerZero’s crosschain infrastructure. Below is a summary of what each tool does and when you might want to use it. ## LayerZero Scan (UI) The [**LayerZero Scan** overview page](./layerzeroscan/overview) explains the **web-based crosschain explorer** that showcases: * **Crosschain transactions** (messages) in a unified interface * **Source/destination chain** details for any bridging operation * **Individual transaction status** (delivered, pending, or failed) * **Address search** to view bridging events associated with a particular user or contract **Use it if**: * You need a **visual** way to check crosschain TX status. * You want to see real-time bridging volume or debugging info for your messages. ## LayerZero Scan Swagger API The [**API** page](./layerzeroscan/api) documents the **Swagger-based** endpoints that expose the same crosschain transaction data as the web UI, but in a programmatic manner: * `GET /messages/tx/{tx}` to fetch message details * `GET /messages/wallet/{srcAddress}` to Retrieve messages initiated by a specific wallet address. * Query-based endpoints to **filter** or **search** messages by chain, status, or time **Use it if**: * You want to **automate** crosschain transaction queries (e.g., in a custom dashboard). * You need to **poll or monitor** message statuses at scale (like for bridging analytics or notifications). ## LayerZero Endpoint Metadata The [**Endpoint Metadata** page](./endpoint-metadata) details a **comprehensive JSON** file that maps: * **All known LayerZero chain deployments** (bridging contract addresses, RPC endpoints, etc.) * **Token metadata** on each chain (addresses, decimals, pegging info) * **DVNs** (Decentralized Verifier Network addresses), chain explorers, environment flags, and more It also explains how you can: * Programmatically configure bridging by reading the `deployments` or `tokens` fields. **Use it if**: * You want to ensure you have the latest official addresses rather than manually hardcoding them. ## Putting It All Together * **LayerZero Scan** (web UI) → Quick visual debugging, real-time transaction lookup. * **LayerZero Scan API** → Programmatic crosschain transaction data retrieval and stats. * **Endpoint Metadata** → Full listing of chain configs, bridging contracts, and tokens for advanced integrations or dynamic UIs. Consider each tool a different piece of the puzzle: * The **Scan** explorer helps confirm if a bridging transaction arrived safely. * The **Scan API** helps you build your own custom dashboards or monitoring scripts. * The **Endpoint Metadata** ensures your application always references the correct bridging addresses, token definitions, etc., across all LayerZero-supported networks. For more context on how bridging works under the hood, see the rest of our [LayerZero documentation](../home/intro). # Message Execution Options Source: https://docs.layerzero.network/v2/tools/sdks/options Use Message Execution Options with LayerZero V2. Developer tools for building and debugging omnichain applications. LayerZero enables crosschain messaging. When sending crosschain messages, the source chain has no knowledge of the destination chain's state or the resources required to execute a transaction on it. **Message Execution Options** provide a standardized way to specify the execution requirements for transactions on the destination chain. You can think of `options` as serialized requests in `bytes` that inform the off-chain infrastructure (`DVNs` and `Executors`) how to handle the execution of your message on the destination chain. See [Message Options](../../concepts/message-options) for more details on why Options exist in the LayerZero protocol. ## Options Builders LayerZero provides tools to build specific Message Execution Options for your application: ### EVM * `OptionsBuilder.sol`: Can be imported from [`@layerzerolabs/oapp-evm`](https://www.npmjs.com/package/@layerzerolabs/oapp-evm) * `options.ts`: Can be imported from [`@layerzerolabs/lz-v2-utilities`](https://www.npmjs.com/package/@layerzerolabs/lz-v2-utilities) ### Aptos & Solana * `options.ts`: Can be imported from [`@layerzerolabs/lz-v2-utilities`](https://www.npmjs.com/package/@layerzerolabs/lz-v2-utilities) ## Generating Options ### EVM (Solidity) ```solidity wrap theme={null} using OptionsBuilder for bytes; bytes memory options = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(50000, 0) .toBytes(); ``` ### All Chains (TypeScript) ```typescript wrap theme={null} import {Options} from '@layerzerolabs/lz-v2-utilities'; const options = Options.newOptions().addExecutorLzReceiveOption(gas_limit, msg_value).toBytes(); ``` ## Option Types ### `lzReceive` Option Specifies the gas values the Executor uses when calling `lzReceive` on the destination chain. ```typescript wrap theme={null} Options.newOptions().addExecutorLzReceiveOption(gas_limit, msg_value); ``` ### `lzRead` Option Specifies the gas values and response data size the Executor uses when delivering lzRead responses. Since the return data size is not known to the Executor ahead of time, you must estimate the expected response data size. This size is priced into the Executor's fee formula. Failure to correctly estimate the return data size will result in the Executor not delivering the response. ```typescript wrap theme={null} Options.newOptions().addExecutorLzReadOption(gas_limit, return_data_size, msg_value); ``` Parameters: * `gas_limit`: The amount of gas for delivering the lzRead response * `return_data_size`: The estimated size (in bytes) of the response data from the read operation * `msg_value`: The `msg.value` for the call ### `lzCompose` Option Allocates gas and value for **Composed Messages** on the destination chain. ```typescript wrap theme={null} Options.newOptions().addExecutorLzComposeOption(index, gas_limit, msg_value); ``` Parameters: * `_index`: The index of the `lzCompose()` function call * `_gas`: The gas amount for the lzCompose call * `_value`: The `msg.value` for the call ### `lzNativeDrop` Option Specifies how much native gas to drop to any address on the destination chain. ```typescript wrap theme={null} Options.newOptions().addExecutorNativeDropOption(amount, receiverAddressInBytes32); ``` Parameters: * `_amount`: The amount of gas in wei/lamports to drop * `_receiver`: The `bytes32` representation of the receiver address ### `OrderedExecution` Option Enables ordered message delivery, overriding the default unordered delivery. ```typescript wrap theme={null} Options.newOptions().addExecutorOrderedExecutionOption(''); ``` ## Chain-Specific Considerations ### EVM Chains * Gas values are specified in wei * Gas costs vary by chain and opcode pricing ### Aptos * Gas units are similar to EVM but may have different costs * Recommended starting gas limit: 1,500 units for `lzReceive` * Uses APT as native token ### Solana * Uses compute units instead of gas * For SPL token ATAs, rent-exempt minimum is 0.00203928 SOL (2,039,280 lamports); Token-2022 accounts may require more depending on enabled extensions * Native token drops are in lamports * Programs pull SOL from sender's account rather than pushing with transaction * Prefer per-tx `extraOptions` with `gas=0` and non-zero `msg.value` only if the recipient’s [Associated Token Account (ATA)](https://www.alchemy.com/overviews/associated-token-account) is missing; enforce gas via app-level `enforcedOptions` (options are combined). See [Solana OFT: Conditional msg.value for ATA creation](../../developers/solana/oft/overview#conditional-msgvalue-for-ata-creation). ## Determining Gas Costs ### Tenderly For supported chains, the [Tenderly Gas Profiler](https://dashboard.tenderly.co/explorer) can help determine optimal gas values: 1. Deploy and test your contract 2. Use Tenderly to profile actual gas usage 3. Set your options slightly above the profiled amount ### Testing Always test your gas settings thoroughly: 1. Start with conservative estimates 2. Profile actual usage 3. Adjust based on real-world performance 4. Consider chain-specific gas mechanisms ## Best Practices 1. **Gas Profiling**: Always profile your contract's gas usage on each target chain 2. **Conservative Estimates**: Start with higher gas limits and adjust down 3. **Chain-Specific Testing**: Test thoroughly on each target chain 4. **Native Caps**: Check Executor's native cap for each pathway 5. **Multiple Options**: Consider combining options for complex scenarios ## Further Reading * [EVM Gas Documentation](https://ethereum.org/en/developers/docs/gas/) * [Aptos Gas Fees](https://aptos.dev/en/network/blockchain/gas-txn-fee) * [Solana Compute Units](https://solana.com/docs/core/fees) * [LayerZero Executors](../../concepts/permissionless-execution/executors) # LayerZero Solana SDK Source: https://docs.layerzero.network/v2/tools/sdks/solana-sdk Step-by-step guide to layerzero solana sdk using LayerZero V2. Build and deploy omnichain applications with crosschain messaging. Follow step-by-step develo... ### Package Use the `@layerzerolabs/lz-solana-sdk-v2` package to interact with the LayerZero Endpoint program on Solana from TypeScript/JavaScript. ### Interacting with the Endpoint Note that the SDK makes use of [`Umi`](https://developers.metaplex.com/umi) in place of `@solana/web3.js` Create an `endpoint` instance: ```ts wrap theme={null} import {TransactionBuilder, publicKey as umiPublicKey} from '@metaplex-foundation/umi'; import {EndpointProgram} from '@layerzerolabs/lz-solana-sdk-v2/umi'; const endpoint = new EndpointProgram.Endpoint(EndpointProgram.ENDPOINT_PROGRAM_ID); ``` Note: If the payload account is missing in some flows, call `endpoint.initVerify(umiWalletSigner, { srcEid, sender, receiver, nonce })` before `skip` or `clear`. #### Skip a message `endpoint.skip(umiWalletSigner, { sender, receiver, srcEid, nonce })` * **When to use**: Bypass a stuck inbound message at a future nonce to unblock subsequent processing. * **Preconditions**: * `nonce > inboundNonce` * `nonce <= inboundNonce + 256` (sliding window) * If the payload account is missing, call `initVerify` first * Caller is the authorized delegate ```ts wrap theme={null} const skipIxn = endpoint.skip(umiWalletSigner, { sender: senderBytes32, // bytes32 normalized sender receiver: umiPublicKey(''), srcEid: , nonce: BigInt(), }) await new TransactionBuilder([skipIxn]).sendAndConfirm(umi) ``` Example usage: [https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/solana/endpoint/skip.ts](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/solana/endpoint/skip.ts) #### Nilify a nonce `endpoint.oAppNilify(umiWalletSigner, { nonce, receiver, sender, srcEid, payloadHash })` * **When to use**: Invalidate a verified payload by setting its payload hash to NIL without deleting the account. * **Preconditions**: * Provide the exact `payloadHash` (must match onchain) * Typically after verification; does not create the payload account * Caller is the authorized delegate ```ts wrap theme={null} const nilifyIxn = endpoint.oAppNilify(umiWalletSigner, { nonce: BigInt(), receiver: umiPublicKey(''), sender: senderBytes32, srcEid: , payloadHash: payloadHashBytes32, }) await new TransactionBuilder([nilifyIxn]).sendAndConfirm(umi) ``` Example usage: [https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/solana/endpoint/nilify.ts](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/solana/endpoint/nilify.ts) #### Burn a nonce `endpoint.oAppBurnNonce(umiWalletSigner, { nonce, receiver, sender, srcEid, payloadHash })` * **When to use**: Delete the payload hash account for an older nonce after inbound processing has advanced beyond it (state cleanup). * **Preconditions**: * `nonce < inboundNonce` * Provide the exact `payloadHash` (must match onchain) * Caller is the authorized delegate ```ts wrap theme={null} const burnIxn = endpoint.oAppBurnNonce(umiWalletSigner, { nonce: BigInt(), receiver: umiPublicKey(''), sender: senderBytes32, srcEid: , payloadHash: payloadHashBytes32, }) await new TransactionBuilder([burnIxn]).sendAndConfirm(umi) ``` Example usage: [https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/solana/endpoint/burn.ts](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/solana/endpoint/burn.ts) #### Clear a payload Note that `clear` does not make use of the `endpoint` class, but instead requires usage of `EndpointProgram.instruction`. `EndpointProgram.instructions.clear({ programs }, { accounts }, { args })` * **When to use**: Finalize/ack a payload for a nonce that has already been verified; clean up state for a known payload. * **Preconditions**: * `nonce <= inboundNonce` * Provide `payloadHash` OR `guid + message` (to derive the hash) * If payload account is missing, call `initVerify` first * Caller is the authorized delegate ```ts wrap theme={null} // Derive PDAs const [endpointPda] = endpoint.pda.setting() const [noncePda] = endpoint.pda.nonce(umiPublicKey(''), , senderBytes32) const [oappRegistryPda] = endpoint.pda.oappRegistry(umiPublicKey('')) const [payloadHashPda] = endpoint.pda.payloadHash(umiPublicKey(''), , senderBytes32, Number()) const clearIxn = EndpointProgram.instructions.clear( { programs: endpoint.programRepo }, { signer: umiWalletSigner, oappRegistry: oappRegistryPda, nonce: noncePda, payloadHash: payloadHashPda, endpoint: endpointPda, eventAuthority: endpoint.eventAuthority, program: endpoint.programId, }, { receiver: umiPublicKey(''), srcEid: , sender: senderBytes32, nonce: BigInt(), guid: guidBytes32, message: messageBytes, } ).items[0] await new TransactionBuilder([clearIxn]).sendAndConfirm(umi) ``` Example usage: [https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/solana/endpoint/clear.ts](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/solana/endpoint/clear.ts) # LayerZero Simple Config Generator Source: https://docs.layerzero.network/v2/tools/simple-config Use Simple Config Generator with LayerZero V2. Developer tools for building and debugging omnichain applications. LayerZero enables crosschain messaging. The LayerZero Simple Config Generator makes use of the `@layerzerolabs/metadata-tools` package to provide a streamlined approach to configuring your OApp connections. It allows for a more simplified LayerZero config file. **Current Support**: The Simple Config Generator currently supports EVM chains and Solana. Aptos support is not yet available. **Production deployments should use multiple required DVNs from independent operators.** The examples below use `` as a placeholder so that the config does not silently resolve to a single-DVN configuration. Replace it with a real DVN name from [DVN Addresses](/v2/deployments/dvn-addresses) before wiring. See the [Integration Checklist](/v2/tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway) for production DVN guidance. Here's how to use it: 1. Install metadata-tools: `pnpm add -D @layerzerolabs/metadata-tools` 2. Create a new [LZ config](/v2/concepts/glossary#lz-config) file named `layerzero.config.ts` (or edit your existing one) in the project root and use the examples below as a starting point: ```typescript wrap EVM Chains Only theme={null} import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {OAppEnforcedOption, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const avalancheContract: OmniPointHardhat = { eid: EndpointId.AVALANCHE_V2_TESTNET, contractName: 'MyOFT', }; const polygonContract: OmniPointHardhat = { eid: EndpointId.AMOY_V2_TESTNET, contractName: 'MyOFT', }; const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 80000, value: 0, }, ]; export default async function () { // note: pathways declared here are automatically bidirectional // if you declare A,B there's no need to declare B,A const connections = await generateConnectionsConfig([ [ avalancheContract, // Chain A contract polygonContract, // Chain B contract [['LayerZero Labs', ''], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [1, 1], // [A to B confirmations, B to A confirmations] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions ], ]); return { contracts: [{contract: avalancheContract}, {contract: polygonContract}], connections, }; } ``` ```typescript wrap EVM + Solana theme={null} import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {OAppEnforcedOption, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; export const avalancheContract: OmniPointHardhat = { eid: EndpointId.AVALANCHE_V2_TESTNET, contractName: 'MyOFT', }; export const solanaContract: OmniPointHardhat = { eid: EndpointId.SOLANA_V2_TESTNET, address: 'HBTWw2VKNLuDBjg9e5dArxo5axJRX8csCEBcCo3CFdAy', // your OFT Store address }; const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 80000, value: 0, }, ]; const SOLANA_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 200000, value: 2039280, // SPL token account rent value in lamports }, ]; export default async function () { // note: pathways declared here are automatically bidirectional // if you declare A,B there's no need to declare B,A const connections = await generateConnectionsConfig([ [ avalancheContract, // Chain A contract solanaContract, // Chain B contract [['LayerZero Labs', ''], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [1, 1], // [A to B confirmations, B to A confirmations] [SOLANA_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions ], ]); return { contracts: [{contract: avalancheContract}, {contract: solanaContract}], connections, }; } ``` * Note that only the Solana contract object requires `address` to be specified. Do not specify `address` for non-Solana contract objects. * The above examples contain a minimal mesh with only one pathway (two chains) for demonstration purposes. You are able to add as many pathways as you need into the `connections` param, via `generateConnectionsConfig`. 3. If your pathways include Solana, run the Solana init config command: ``` npx hardhat lz:oft:solana:init-config --oapp-config layerzero.config.ts ``` 4. Run the wire command: ```bash wrap theme={null} npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` The wire command processes all the transactions required to connect all pathways specified in the LZ Config file. You need to only run this once regardless of how many pathways there are. If you change anything in the LZ Config file, then it should be run again. ```bash wrap theme={null} npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` ## Key Features * **Automatic Bidirectional Connections**: Define one pathway, get both directions automatically * **Streamlined defaults**: Sensible structure for DVN and executor configuration that you must review and override per pathway (see Warning above) * **Cross-VM Compatibility**: Works seamlessly with EVM chains and Solana * **Reduced Complexity**: Fewer configuration parameters to manage * **Less Error-Prone**: Automated configuration generation reduces manual errors ## Configuration Parameters ### Pathway Definition ```typescript wrap theme={null} [ contractA, // Source chain contract contractB, // Destination chain contract [['LayerZero Labs', ''], []], // DVN configuration [1, 1], // Confirmations for each direction [optionsA, optionsB], // Enforced options for each direction ]; ``` ### DVN Configuration ```typescript wrap theme={null} [['LayerZero Labs', ''], []]; // [ requiredDVN[], [ optionalDVN[], threshold ] ] ``` * **Required DVNs**: Must verify the message for it to be considered valid * **Optional DVNs**: Additional verifiers (with threshold) for enhanced security ### Enforced Options ```typescript wrap theme={null} const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, // Message type (1 = OFT, 2 = OApp) optionType: ExecutorOptionType.LZ_RECEIVE, // Option type gas: 80000, // Gas limit for destination execution value: 0, // Value to send (usually 0) }, ]; ``` ## VM-Specific Considerations ### EVM Chains * Use `contractName` for contract identification * Gas values represent actual gas units * Value is typically 0 ### Solana * Use `address` for contract identification (required) * Gas values represent compute units * Value represents lamports (typically 2039280 for SPL token account rent) **Custom Metadata**: For advanced use cases, you can provide custom metadata by passing a `fetchMetadata` function to `generateConnectionsConfig`. This allows you to extend the default metadata with custom DVNs and executors. ## Next Steps * **Migrate from Manual Config**: See the [Migrate to Simple Config](/v2/tools/migrate-to-simple-config) guide * **Production Deployment**: Review and adjust settings for production environments * **Gas Optimization**: Profile your contracts to set optimal gas limits * **Custom DVNs**: Consider adding custom DVNs for enhanced security # Build Decentralized Verifier Networks (DVNs) Source: https://docs.layerzero.network/v2/workers/off-chain/build-dvns Technical guide for implementing and integrating a third-party DVN into the LayerZero V2 protocol, including fee quoting, event listening, and verification workflows. This document contains a high level overview of how to implement and integrate a basic third party DVN into the LayerZero V2 protocol. ## Fee Quoting, Collection, and Withdrawal DVN owners should implement and deploy a DVN contract on every chain they want to support. The contract must implement the `ILayerZeroDVN` interface, which specifies two core functions: * **`assignJob`** - Called by the Message Library when a packet is sent, paying the DVN for verification * **`getFee`** - Returns the fee for verifying a message to a specific destination For the complete interface specification, data structures, and method signatures, see the [DVN Technical Reference](/v2/workers/off-chain/dvn-technical-reference#ilayzerodvn). If your DVN is responsible for a packet, the LayerZero Endpoint will call your DVN contract's `assignJob` function. ## Building a DVN The DVN has one off-chain workflow: 1. The DVN first listens for the `PacketSent` event: ```solidity theme={null} PacketSent( bytes encodedPacket, bytes options, address sendLibrary) ``` The packet has the following structure: ```solidity theme={null} struct Packet { uint64 nonce; // the nonce of the message in the pathway uint32 srcEid; // the source endpoint ID address sender; // the sender address uint32 dstEid; // the destination endpoint ID bytes32 receiver; // the receiving address bytes32 guid; // a global unique identifier bytes message; // the message payload } ``` The encoded packet can be deserialized with the [`PacketSerializer`](https://github.com/LayerZero-Labs/monorepo/blob/a6c8758d436804f41db62d480f82cdb0690faaef/packages/layerzero-v2/utility/src/model/packet.ts#L29) and the option can be deserialized with the [`OptionSerializer`](https://github.com/LayerZero-Labs/monorepo/blob/a6c8758d436804f41db62d480f82cdb0690faaef/packages/layerzero-v2/utility/src/options/options.ts#L81). 2. After the `PacketSent` event, the `DVNFeePaid` event is how you know your DVN has been assigned to verify the packet's `payloadHash`. ```solidity theme={null} DVNFeePaid( address[] requiredDVNs, address[] optionalDVNs, uint256[] fees ); ``` The `DVNFeePaid` event returns a list of **all** of the OApp's configured DVNs, so your workflow should filter your specific DVN address from the array to make sure your DVN has been paid. 3. After receiving the fee, your DVN should query the address of the MessageLib on the destination chain: ```solidity theme={null} getReceiveLibrary( _receiver, _dstEid ); ``` 4. After your DVN has retrieved the receive MessageLib, you should read the MessageLib configuration from it. In the configuration is the required block `confirmations` to wait before calling `verify` on the destination chain. ```solidity theme={null} function getUlnConfig(address _oapp, uint32 _remoteEid) public view returns (UlnConfig memory rtnConfig); ``` This will return the `UlnConfig`, which you can use to read the number of `confirmations`: ```solidity theme={null} struct UlnConfig { uint64 confirmations; // ... ``` 5. Your DVN should next do an idempotency check: ```solidity theme={null} ULN._verified( _dvn, _headerHash, _payloadHash, _requiredConfirmation ); ``` This returns a boolean value: * If the state is `true`, then your idempotency check indicates that you already verified this packet. You can terminate your DVN workflow. * If the state is `false`, then you must call `ULN.verify`: ```solidity theme={null} ULN._verify( _packetHeader, _payloadHash, _confirmations ); ``` To know your workflow has successfully fulfilled its obligation, your DVN should perform an idempotency check at the end of the DVN workflow. # Build and Run Executors Source: https://docs.layerzero.network/v2/workers/off-chain/build-executors Deploy, configure, and operate a custom LayerZero V2 Executor on EVM testnets with setup, verification, replay, and production readiness guidance. Executors provide Execution as a Service for LayerZero messages. A custom Executor receives a fee on the source chain, waits until the destination chain can accept the verified packet, and then calls the destination Endpoint to deliver the message. This guide shows how to independently bring up an EVM Executor for a two-chain testnet path, then explains the checks and operational controls required before running one in production. The reference implementations linked in this guide are third-party examples, not LayerZero-owned production services. Use them to learn the protocol flow, then harden your own deployment with the production checklist below. ## What You Will Build By the end of this guide, you should have: * an Executor fee contract deployed on each source chain you want to support; * an OApp configured to pay your Executor instead of the default Executor; * an off-chain process watching source-chain `PacketSent` events; * fee validation that proves your Executor was paid for the exact packet; * destination-chain verification and execution logic; and * a repeatable smoke test that sends a message in both directions. The commands and addresses below use an EVM-to-EVM testnet path. For non-EVM chains, keep the same protocol lifecycle, but replace the EVM packet address codec, event provider, and execution client with chain-specific implementations. ## Executor Lifecycle A complete Executor has two off-chain workflows: commit and execute. 1. Watch the source chain for `PacketSent` on `EndpointV2`. 2. Validate that the same transaction paid your Executor through the trusted send library. 3. Decode the packet and route it by destination endpoint ID. 4. Wait for destination-chain DVN verification. 5. Call `ReceiveUln302.commitVerification(packetHeader, payloadHash)` when the packet is verifiable. 6. Poll `EndpointV2View.executable(origin, receiver)` until the packet is executable. 7. Call `EndpointV2.lzReceive(origin, receiver, guid, message, extraData)`. 8. Persist the result so restarts do not duplicate or lose work. The source-chain Executor contract handles fee quoting and fee collection. The off-chain worker handles observation, verification, commit, and execution. ## Prerequisites Install: * Node.js `>=18` * npm, pnpm, or Yarn * [Foundry](https://book.getfoundry.sh/getting-started/installation) * `git` * `jq` * RPC URLs for both chains * a funded signer on both chains Prepare environment variables: ```bash theme={null} export PRIVATE_KEY="0x..." export RPC_URL_A="https://..." export RPC_URL_B="https://..." ``` Use a dedicated Executor signer. Do not run an Executor with a production owner key, treasury key, or deployer key unless you have intentionally designed that operational model. ## Choose Your Chains Use the deployment pages or metadata API to collect the current Endpoint, SendUln302, ReceiveUln302, EndpointV2View, and endpoint ID values for each chain. ```bash theme={null} curl -s https://metadata.layerzero-api.com/v1/metadata \ | jq '.[] | select(.chainKey == "sepolia" or .chainKey == "hyperliquid-testnet")' ``` For example, this guide has been validated with: | Chain | Chain ID | Endpoint ID | Deployment page | | ------------------------ | ---------: | ----------: | ---------------------------------------------------------------------------------------- | | Ethereum Sepolia Testnet | `11155111` | `40161` | [/v2/deployments/chains/sepolia](/v2/deployments/chains/sepolia) | | HyperEVM Testnet | `998` | `40362` | [/v2/deployments/chains/hyperliquid-testnet](/v2/deployments/chains/hyperliquid-testnet) | Sepolia testnet pricing can be expensive because LayerZero testnet endpoints use real mainnet pricefeeds for crosschain transfers. For repeated tests, choose testnets with cheaper blockspace when possible. ## Bootstrap A Reference Implementation The Paladin reference implementation is a useful EVM starting point because it already separates the source-chain event provider, destination-chain executor, packet codec, and sample contracts. ```bash theme={null} git clone https://github.com/0xpaladinsecurity/zexecutor.git cd zexecutor npm install git submodule update --init --recursive npm test forge build --root ./contracts ``` If a fresh checkout fails while fetching `contracts/lib/forge-std`, add the missing submodule entry and retry: ```bash theme={null} git config -f .gitmodules submodule.contracts/lib/forge-std.path contracts/lib/forge-std git config -f .gitmodules submodule.contracts/lib/forge-std.url https://github.com/foundry-rs/forge-std git submodule sync --recursive git submodule update --init --recursive ``` The reference is intentionally minimal. Before operating it beyond a testnet smoke test, add durable state, replay checkpoints, rate limiting, metrics, and classified retry handling. ## Deploy The On-Chain Executor Deploy an Executor fee contract on every source chain that should pay your Executor. The contract must implement `ILayerZeroExecutor`: ```solidity theme={null} interface ILayerZeroExecutor { function assignJob( uint32 _dstEid, address _sender, uint256 _calldataSize, bytes calldata _options ) external payable returns (uint256 price); function getFee( uint32 _dstEid, address _sender, uint256 _calldataSize, bytes calldata _options ) external view returns (uint256 price); } ``` For a smoke test, a static-fee contract is enough. For production, the fee must be calculated from the destination gas limit, gas price, calldata size, native token price, margin, and any native drop or value instructions in `_options`. After deploying your Executor contracts, configure your OApp's send library so messages to the remote EID use your Executor: ```solidity theme={null} ExecutorConfig memory executorConfig = ExecutorConfig({ maxMessageSize: 1024, executor: address(customExecutor) }); SetConfigParam[] memory sendConfigs = new SetConfigParam[](1); sendConfigs[0] = SetConfigParam({ eid: remoteEid, configType: 1, // CONFIG_TYPE_EXECUTOR config: abi.encode(executorConfig) }); endpoint.setConfig(address(oapp), sendLib, sendConfigs); ``` Also configure the DVN set used by your test OApp. The Executor can only commit after the receive library has enough DVN verification for the packet. ## Configure OApp Peers Your test OApps must be deployed on both chains and configured as peers before `_lzSend` can route packets. For EVM-to-EVM tests, set each remote peer as a left-padded `bytes32` value: ```solidity theme={null} oappOnChainA.setPeer( chainBEid, bytes32(uint256(uint160(address(oappOnChainB)))) ); oappOnChainB.setPeer( chainAEid, bytes32(uint256(uint160(address(oappOnChainA)))) ); ``` Use the same owner account that controls the OApp configuration. If your test OApp configures DVNs and Executor settings inside `setPeer`, confirm that both the send library and receive library configs are written for each pathway before quoting a message. ## Create Executor Configuration Your off-chain Executor needs one config entry per chain. Use current metadata for LayerZero protocol addresses and your own deployed Executor contract for `executor`. ```json theme={null} { "chains": { "11155111": { "name": "Ethereum Sepolia Testnet", "rpc": "https://ethereum-sepolia-rpc.publicnode.com", "endpoint": "0x6EDCE65403992e310A62460808c4b910D972f10f", "endpointView": "0x982Ca8b3532236C5e77Ff215791dD454e07E21F7", "trustedSendLib": "0xcc1ae8Cf5D3904Cef3360A9532B477529b177cCE", "trustedReceiveLib": "0xdAf00F5eE2158dD58E0d3857851c432E34A3A851", "executor": "0xYourExecutorOnSepolia", "eid": 40161, "bootstrapLookbackBlocks": 0 }, "998": { "name": "HyperEVM Testnet", "rpc": "https://rpc.hyperliquid-testnet.xyz/evm", "endpoint": "0xf9e1815F151024bDE4B7C10BAC10e8Ba9F6b53E1", "endpointView": "0x386A3922470581155c42282801231762E7343802", "trustedSendLib": "0x43E505ba192aaC7BABdC1A796c87844171011684", "trustedReceiveLib": "0x012f6eaE2A0Bf5916f48b5F37C62Bcfb7C1ffdA1", "executor": "0xYourExecutorOnHyperEVM", "eid": 40362, "bootstrapLookbackBlocks": 0 } } } ``` Use `bootstrapLookbackBlocks` only if your worker persists checkpoints and can rate-limit historical replay. Endpoint-wide log scans can be noisy because the Endpoint emits traffic for all applications and executors. ## Validate Executor Payment Do not decide that your Executor was paid by checking whether a transaction contains any `ExecutorFeePaid` event. A single transaction can send multiple packets, and only one of them may have paid your Executor. Use this receipt-walking algorithm for each `PacketSent` log: 1. Fetch the transaction receipt. 2. Start from the specific `PacketSent.logIndex`. 3. Walk backward through logs in that transaction. 4. Stop if you hit another `PacketSent`; that earlier packet owns earlier fee events. 5. Ignore any event not emitted by the configured `trustedSendLib`. 6. Decode `ExecutorFeePaid(address executor, uint256 fee)`. 7. Accept the packet only if `executor == yourExecutorAddress`. Pseudocode: ```ts theme={null} for (const txLog of receipt.logsBefore(packetSentLog).reverse()) { if (txLog.topic0 === PACKET_SENT_TOPIC) return undefined; if (txLog.topic0 !== EXECUTOR_FEE_PAID_TOPIC) continue; if (txLog.address.toLowerCase() !== trustedSendLib.toLowerCase()) continue; const [executor, fee] = decodeExecutorFeePaid(txLog.data); if (executor.toLowerCase() !== configuredExecutor.toLowerCase()) continue; return { packet, fee }; } ``` Add tests for: * multiple `PacketSent` logs in one transaction; * `ExecutorFeePaid` emitted by an untrusted address; * `ExecutorFeePaid` for another Executor; * no `ExecutorFeePaid`; and * zero or insufficient fee. ## Commit Verification After accepting a packet, derive: * `origin`: `{ srcEid, sender, nonce }` * `receiver`: destination OApp address * `packetHeader` * `payloadHash` First check endpoint-level verifiability: ```solidity theme={null} EndpointV2View.verifiable( origin, receiver, receiveLib, payloadHash ); ``` Then check receive-library verifiability before committing: ```solidity theme={null} UlnConfig memory config = ReceiveUln302.getUlnConfig(receiver, origin.srcEid); ReceiveUln302.verifiable( config, keccak256(packetHeader), payloadHash ); ``` When the receive library reports the packet is verifiable, call: ```solidity theme={null} ReceiveUln302.commitVerification(packetHeader, payloadHash); ``` A packet can look close to ready at the Endpoint level while still not being committable at the receive library. Check both layers to avoid unnecessary commit reverts. ## Execute The Message After commit, query: ```solidity theme={null} EndpointV2View.executable(origin, receiver); ``` Handle execution state as: | State | Meaning | Action | | -------------------------- | -------------------------------------------------- | ----------------------------------- | | `NotExecutable` | Packet is not committed or is blocked by ordering. | Requeue with backoff. | | `VerifiedButNotExecutable` | Packet is verified, but not executable yet. | Requeue and inspect nonce ordering. | | `Executable` | Endpoint can deliver the message. | Call `lzReceive`. | | `Executed` | Message was already delivered. | Mark complete. | When executable: ```solidity theme={null} EndpointV2.lzReceive( origin, receiver, guid, message, extraData ); ``` Use the message options to set destination gas and value. A production Executor should parse options and enforce that the collected fee covers the requested execution resources. ## Run The Off-Chain Worker For the reference implementation, run: ```bash theme={null} PRIVATE_KEY="$PRIVATE_KEY" npm run dev -- -c ./executor.testnet.config.json ``` A clean startup should: * load each configured chain; * register one provider per source EID; * register one executor per destination EID; and * begin watching `PacketSent`. If you enable historical replay, start with a small block window and confirm your RPC provider supports the requested `eth_getLogs` range: ```json theme={null} { "bootstrapLookbackBlocks": 100, "bootstrapBlockRange": 50 } ``` For production, replay should resume from the last persisted checkpoint, not from an arbitrary fixed lookback on every restart. ## Smoke Test Both Directions Before treating the Executor as usable, run a two-way smoke test. 1. Quote a message from chain A to chain B. 2. Send a small payload from chain A to chain B. 3. Confirm the source transaction emitted `PacketSent` and `ExecutorFeePaid`. 4. Confirm your worker committed verification on chain B. 5. Confirm your worker executed `lzReceive` on chain B. 6. Query the destination OApp and confirm the payload was stored or handled. 7. Repeat from chain B to chain A. If you are using the Paladin reference scripts, the commands look like: ```bash theme={null} # Quote chain A -> chain B. forge script QuoteMessage \ --sig "run(address,uint32,bytes)" \ "$APP_ON_CHAIN_A" \ "$CHAIN_B_EID" \ 0x68656c6c6f2d6578656375746f72 \ --rpc-url "$RPC_URL_A" \ --root ./contracts # Send chain A -> chain B. Use a value above the quoted native fee. forge script SendMessage \ --sig "run(address,uint32,bytes,uint256)" \ "$APP_ON_CHAIN_A" \ "$CHAIN_B_EID" \ 0x68656c6c6f2d6578656375746f72 \ "$NATIVE_FEE_WITH_BUFFER" \ --rpc-url "$RPC_URL_A" \ --broadcast \ --root ./contracts # Query destination app state after execution. forge script GetExecutedMessages \ --sig "run(address)" \ "$APP_ON_CHAIN_B" \ --rpc-url "$RPC_URL_B" \ --root ./contracts ``` For a test OApp that stores received messages, the success condition is: ```bash theme={null} # Chain B after A -> B EXECUTION_LENGTH: 1 EXECUTION_0: # Chain A after B -> A EXECUTION_LENGTH: 1 EXECUTION_0: ``` Record the source send tx, destination commit tx, and destination execution tx for each direction. These three hashes are the fastest way to debug later failures. ## Production Runtime Checklist A production Executor needs more than the minimal reference loop. ### Persistent State Persist one row per packet: | Field | Purpose | | --------------- | -------------------------------------------------------------- | | `srcEid` | Source endpoint ID. | | `dstEid` | Destination endpoint ID. | | `guid` | Idempotency key for the packet. | | `srcTxHash` | Source transaction that emitted `PacketSent`. | | `srcLogIndex` | Exact source log index. | | `receiver` | Destination OApp. | | `payloadHash` | Payload hash used for verification. | | `status` | Observed, verifiable, committed, executable, executed, failed. | | `commitTxHash` | Destination commit transaction, if sent. | | `executeTxHash` | Destination execution transaction, if sent. | | `retryCount` | Retry accounting. | | `lastError` | Last classified error. | On restart, load pending packets from storage before scanning new blocks. ### Checkpointed Event Ingestion Do not rely on `watchEvent` alone. RPC subscriptions can miss logs during disconnects or process restarts. Use: * per-chain block checkpoints; * bounded `eth_getLogs` windows; * packet GUID and tx/log-index deduplication; * rate limits per RPC provider; * retries with exponential backoff; and * alerts when the scanner falls behind. ### Retry Classification Classify errors before retrying: | Error class | Retry? | Example response | | --------------------------- | ------ | ----------------------------------- | | DVN not ready | Yes | Requeue with normal backoff. | | Endpoint not executable | Yes | Requeue and inspect nonce ordering. | | Already committed | Yes | Move to execution check. | | Already executed | No | Mark complete. | | RPC rate limited | Yes | Back off and reduce scan range. | | Wrong config address | No | Alert operator. | | Insufficient signer balance | No | Alert and pause sends. | ### Signer Operations Monitor: * native token balance on every destination chain; * account nonce and stuck transactions; * RPC latency and error rate; * gas price spikes; * oldest pending packet age; and * queue depth by destination EID. ### Observability Expose health and metrics: * last source block scanned; * last packet observed; * packets committed; * packets executed; * retries by class; * failures by class; * signer balances; * pending queue depth; and * RPC provider status. ## Troubleshooting | Symptom | Likely cause | Fix | | -------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------- | | No packets accepted | OApp is still configured for another Executor. | Check `ExecutorConfig.executor` for the source pathway. | | Fee event found but packet rejected | Fee event belongs to another packet or another Executor. | Walk receipt logs backward from the specific `PacketSent`. | | `commitVerification` reverts | DVNs have not satisfied receive-library verification. | Check `ReceiveUln302.verifiable` before committing. | | `executable` stays `NotExecutable` | Packet is not committed or ordered execution is blocked. | Check commit tx and nonce ordering. | | Packet is delivered twice in logs | Worker is not deduplicating queue entries. | Deduplicate by `srcEid`, `dstEid`, and `guid`. | | Worker misses packets after restart | No durable checkpoint or catch-up scan. | Persist checkpoints and replay bounded block ranges. | | RPC returns block range or rate-limit errors | Historical scan is too broad. | Lower block range, throttle, or use an indexed provider. | | Signer transactions stop landing | Low balance or stuck nonce. | Monitor balances and nonce state; replace or cancel stuck txs. | ## Non-EVM Chains The EVM packet codec assumes sender and receiver values can be converted from `bytes32` to EVM addresses. For Solana, Aptos, Sui, or other non-EVM chains, implement chain-specific versions of: * event provider; * packet/address codec; * destination execution client; * transaction construction; * signer management; and * gas or fee accounting. The protocol lifecycle remains the same: observe, validate payment, wait for verification, commit when needed, and execute when the destination Endpoint allows delivery. ## Summary The minimal reference Executor is enough to prove the LayerZero V2 flow on testnet. To make it independently operable, you need: * current metadata-derived chain config; * an Executor contract deployed on each source chain; * OApp send config pointing to your Executor; * strict per-packet fee validation; * current receive-library and endpoint verifiability checks; * a two-way smoke test; and * durable runtime state, checkpointed replay, retries, and monitoring before production. ## Important Security And Operational Considerations ### Can a custom Executor forge or alter LayerZero messages? No. A custom Executor cannot make the destination Endpoint accept an unverified packet. Message authenticity is still enforced by the configured MessageLib and DVNs. The primary risk is not message forgery. The primary risk is that a poorly implemented Executor can fail to deliver valid messages, execute messages without being properly paid, overpay for gas, or expose its signer infrastructure. ### What happens if I validate `ExecutorFeePaid` incorrectly? You may execute packets that did not pay your Executor. This most commonly happens when an implementation checks whether the transaction contains any `ExecutorFeePaid` event instead of tying the payment to the exact `PacketSent` log. Batched sends can emit multiple `PacketSent` events in one transaction, and each packet must be matched to its own fee event. Mitigation: * walk receipt logs backward from the exact `PacketSent.logIndex`; * require the event emitter to equal the trusted send library; * require the decoded `executor` to equal your Executor contract; and * stop when a previous `PacketSent` is reached. ### What happens if I use the wrong Endpoint, SendUln, ReceiveUln, or DVN address? Messages can become stuck or fail in non-obvious ways. A packet may be observed on the source chain, but never become committable or executable on the destination chain because the worker is checking the wrong contract or the OApp pathway is configured differently than the worker expects. Mitigation: * source addresses from the LayerZero metadata endpoint or deployment pages; * store the exact metadata snapshot used for a deployment; * validate every configured address at startup; and * include a two-way smoke test after every config change. ### What happens if my worker misses events? Valid packets can remain undelivered. RPC subscriptions and HTTP polling can miss logs during restarts, disconnects, rate limits, or provider outages. Mitigation: * persist per-chain scan checkpoints; * replay bounded block ranges on startup; * deduplicate by packet GUID and tx/log index; * alert when the scanner falls behind; and * use an indexed or production-grade RPC provider for production traffic. ### What happens if I do not persist packet state? The worker can lose pending packets during a restart, retry the same packet indefinitely, or submit duplicate commit and execution transactions. Mitigation: * persist packet status transitions; * treat packet GUID as the primary idempotency key; * record commit and execution transaction hashes; and * resume pending work from storage before processing new logs. ### What are the signer risks? The Executor signer needs native gas on every destination chain it executes on. If the signer is overfunded, reused as an owner key, or stored on an insecure host, compromise of the Executor process can become a key-management incident. Mitigation: * use a dedicated low-privilege Executor signer; * keep owner, treasury, and Executor keys separate; * monitor signer balances and nonce state; * rotate keys through an operational runbook; and * fund only the amount required for expected execution volume plus buffer. ### Can a bad gas or fee model lose money? Yes. If your `getFee` and `assignJob` logic undercharges relative to destination gas costs, your Executor can pay more to execute packets than it collected on the source chain. If it over-trusts user-provided options, it can also accept work that is too expensive or impossible to execute. Mitigation: * parse message options before quoting; * cap supported gas and value options; * price destination native gas and token conversion conservatively; * include an operator margin; and * pause or reprice pathways when gas markets move sharply. ### Is a stalled Executor a security issue? It can be. A stalled Executor does not let invalid messages through, but it can create liveness failures for applications that rely on automatic delivery. If a chain is running its own Executor because another off-chain service was turned off, the Executor becomes part of that chain's application availability path. Mitigation: * alert on oldest pending packet age; * alert on failed commit or execution attempts; * maintain enough destination gas; * document manual execution fallback; and * run a tested recovery process for RPC outages and stuck nonces. # Decentralized Verifier Networks (DVNs) Overview Source: https://docs.layerzero.network/v2/workers/off-chain/dvn-overview Learn how DVNs work in LayerZero V2, including verification methods, implementation types, and the X-of-Y-of-N security model for cross-chain messaging. LayerZero is an **interoperability protocol** that enables secure and seamless communication between different blockchain networks. At the core of LayerZero's security model are **Decentralized Verifier Networks (DVNs)**. ## What are DVNs? DVNs are independent entities that **validate the authenticity and integrity of messages** sent across blockchains within the LayerZero ecosystem. They ensure that a message sent from a source chain arrives untampered at its destination. In the context of the LayerZero protocol, DVNs provide: * **Verification**: Verifying the hash (a unique digital fingerprint) of a LayerZero message emitted on a source chain. * **Customizable Security**: Applications built on LayerZero can select any number or type of DVNs to achieve their desired level of cross-chain security. This flexibility allows for tailored security postures. ## How DVNs Work A DVN is essentially a **smart contract** paired with off-chain infrastructure that provides its own inherent trust mechanism. When a message is sent via LayerZero: 1. The message is picked up by the chosen DVN(s) via the `PacketSent` event. 2. The DVN(s) independently verify the message hash using their unique security logic. 3. Upon successful verification, the DVN calls `verify` on the destination chain's Message Library. 4. Once all required DVNs have verified, the message can be committed and executed. ```mermaid wrap theme={null} sequenceDiagram participant OApp as Source OApp participant SendLib as Send Library participant DVN as DVN (Off-chain) participant ReceiveLib as Receive Library participant Executor OApp->>SendLib: _lzSend() SendLib-->>SendLib: Emit PacketSent SendLib-->>DVN: DVNFeePaid event DVN->>DVN: Wait for confirmations DVN->>DVN: Verify message hash DVN->>ReceiveLib: verify() Note over ReceiveLib: All required DVNs verified Executor->>ReceiveLib: commitVerification() ``` LayerZero is **agnostic to how a DVN is implemented**. This design allows for diverse verification approaches that can be tailored to specific security requirements. ## DVN Implementation Types DVNs can use various verification methods to confirm message authenticity including, but not limited to: | Implementation Type | Description | Example | | ------------------------- | ------------------------------------------------------- | ------------------------- | | **Multisignature** | Requires multiple parties to sign off on a message hash | Custom multisig contracts | | **Zero-Knowledge Proofs** | Uses cryptographic proofs to verify message validity | Polyhedra | | **Decentralized Oracles** | Leverages existing oracle networks for verification | Chainlink | | **Protocol Adapters** | Wraps existing interoperability protocols | Axelar, Wormhole | | **Light Clients** | Verifies using blockchain consensus proofs | Native bridge adapters | ## DVN Operator Responsibilities Regardless of implementation approach, DVN operators are responsible for: 1. **Chain Coverage**: Deploying DVN contracts on every chain they want to support 2. **Event Monitoring**: Listening for `PacketSent` and `DVNFeePaid` events on source chains 3. **Verification Logic**: Implementing secure verification of message hashes 4. **Transaction Submission**: Submitting verification proofs to destination chains 5. **Gas Management**: Maintaining sufficient gas tokens across all supported chains (unless using Gasolina) 6. **Fee Configuration**: Setting appropriate fees via `DstConfig` for each destination chain ## Implementation Paths When building or operating a DVN, you have two primary implementation paths: ### Traditional DVN Implementation The traditional approach gives you **full control** over your DVN but requires managing wallets, gas tokens, and transaction infrastructure across all supported chains. This path is ideal for organizations that need complete control over every aspect of their DVN operation. **Best for:** * Organizations with existing multi-chain infrastructure * Custom verification logic requirements * Full operational autonomy Learn more about building a traditional DVN implementation ### Gasolina DVN LayerZero Labs offers a simplified DVN implementation using **Gasolina**. This approach separates the security function (verification and signing) from the operational complexities (gas management and transaction submission), allowing DVN operators to focus purely on security while LayerZero's Essence service handles transaction delivery. **Best for:** * Security providers without multi-chain infrastructure * Rapid deployment scenarios * Teams wanting to focus on verification rather than operations Learn more about the Gasolina DVN approach ## Protocol Integration DVNs integrate with LayerZero through standardized interfaces that work across all supported virtual machines. ### EVM Interface All EVM DVNs must implement the `ILayerZeroDVN` interface: ```solidity wrap theme={null} interface ILayerZeroDVN { struct AssignJobParam { uint32 dstEid; bytes packetHeader; bytes32 payloadHash; uint64 confirmations; address sender; } function assignJob(AssignJobParam calldata _param, bytes calldata _options) external payable returns (uint256 fee); function getFee( uint32 _dstEid, uint64 _confirmations, address _sender, bytes calldata _options ) external view returns (uint256 fee); } ``` | Function | Type | Description | | ----------- | ------- | ------------------------------------------------------------------------------------ | | `assignJob` | Payable | Called by the Message Library when a packet is sent, paying the DVN for verification | | `getFee` | View | Returns the fee for verifying a message to a specific destination | ### Solana Interface Solana DVNs use a multi-step CPI (Cross-Program Invocation) based verification flow rather than a single interface. The verification process involves three key instructions: | Instruction | Program | Description | | ------------- | ---------- | ----------------------------------------------------------------------- | | `init_verify` | ReceiveULN | Initializes a confirmations account to store DVN verification state | | `invoke` | DVN | Executes DVN verification logic (signature validation, multisig checks) | | `verify` | ReceiveULN | Finalizes verification and emits `PayloadVerifiedEvent` | The core verification instruction signature: ```rust wrap theme={null} impl Verify<'_> { pub fn apply(ctx: &mut Context, params: &VerifyParams) -> Result<()> { ctx.accounts.confirmations.value = Some(params.confirmations); emit_cpi!(PayloadVerifiedEvent { dvn: ctx.accounts.dvn.key(), header: params.packet_header, confirmations: params.confirmations, proof_hash: params.payload_hash, }); Ok(()) } } ``` For complete Solana DVN implementation details, see the [Solana Protocol Overview](/v2/developers/solana/technical-overview#verification-workflow). ### X-of-Y-of-N Security Model OApps configure DVNs as part of their [Security Stack](/v2/concepts/modular-security/security-stack-dvns), using an X-of-Y-of-N model: ```solidity wrap theme={null} struct UlnConfig { uint64 confirmations; // Block confirmations required uint8 requiredDVNCount; // X - all of these must verify uint8 optionalDVNCount; // N - total optional DVNs uint8 optionalDVNThreshold; // Y - threshold of optional DVNs address[] requiredDVNs; // Addresses of required DVNs address[] optionalDVNs; // Addresses of optional DVNs } ``` This configuration allows applications to require: * **All** required DVNs to verify (X) * **At least Y** of the optional DVNs (N) to verify *** ## Summary DVNs are LayerZero's flexible and customizable security layer for cross-chain communication. They enable applications to choose their security parameters while maintaining decentralization and trust minimization. Whether you choose the traditional implementation path for full control or the Gasolina approach for operational simplicity, DVNs provide the critical verification layer that makes secure cross-chain messaging possible. ## Next Steps ### For DVN Operators * Review the [DVN Contract Reference](/v2/workers/off-chain/dvn-technical-reference) for methods, events, and errors * See [Build Decentralized Verifier Networks](/v2/workers/off-chain/build-dvns) for full implementation details * Review the [Gasolina Overview](/v2/workers/off-chain/gasolina-overview) for the simplified approach * Follow the [Implementation Guide](/v2/workers/off-chain/gasolina-implementation) to deploy Gasolina ### For Application Developers * Learn how to [configure DVNs](/v2/concepts/modular-security/security-stack-dvns) in your OApp * Understand [DVN pricing](/v2/concepts/protocol/transaction-pricing) and fee structures * Explore existing [DVN providers](/v2/deployments/dvn-addresses) available on each chain If you're new to operating DVNs, we recommend starting with the Gasolina approach. You can always migrate to a traditional implementation later as your needs evolve. # DVN Contract Reference Source: https://docs.layerzero.network/v2/workers/off-chain/dvn-technical-reference Comprehensive technical documentation for DVN contracts, including interfaces, methods, events, errors, and data structures for LayerZero V2. This page provides comprehensive technical documentation for DVN (Decentralized Verifier Network) contracts, including interfaces, methods, events, errors, and data structures. ## Core Interface All DVNs must implement the `ILayerZeroDVN` interface to integrate with LayerZero's Message Libraries. ### ILayerZeroDVN ```solidity wrap theme={null} interface ILayerZeroDVN { struct AssignJobParam { uint32 dstEid; // Destination endpoint ID bytes packetHeader; // Packet header containing routing info bytes32 payloadHash; // Hash of the message payload uint64 confirmations; // Required block confirmations address sender; // OApp sender address } /// @notice Assigns a verification job to the DVN /// @param _param Job parameters including destination and payload hash /// @param _options DVN-specific options /// @return fee The fee charged for this verification job function assignJob(AssignJobParam calldata _param, bytes calldata _options) external payable returns (uint256 fee); /// @notice Returns the fee for verifying a message /// @param _dstEid Destination endpoint ID /// @param _confirmations Required block confirmations /// @param _sender OApp sender address /// @param _options DVN-specific options /// @return fee The fee in native tokens function getFee( uint32 _dstEid, uint64 _confirmations, address _sender, bytes calldata _options ) external view returns (uint256 fee); } ``` *** ## DVN Contract Methods The LayerZero DVN contract extends the base Worker contract with multisig capabilities. Below are the key methods organized by access control. ### Public / View Methods #### `getFee` Returns the fee for verifying a message to a specific destination. ```solidity wrap theme={null} function getFee( uint32 _dstEid, uint64 _confirmations, address _sender, bytes calldata _options ) external view returns (uint256 fee) ``` | Parameter | Type | Description | | ---------------- | --------- | ------------------------------------ | | `_dstEid` | `uint32` | Destination endpoint ID | | `_confirmations` | `uint64` | Required block confirmations | | `_sender` | `address` | OApp sender address (for ACL checks) | | `_options` | `bytes` | DVN-specific options | **Returns:** Fee amount in native tokens. This function will revert if the sender is on the denylist or not on the allowlist (when allowlist is enabled). *** #### `hashCallData` Generates a hash of execution parameters for signature verification. ```solidity wrap theme={null} function hashCallData( uint32 _vid, address _target, bytes calldata _callData, uint256 _expiration ) public pure returns (bytes32) ``` | Parameter | Type | Description | | ------------- | --------- | -------------------------- | | `_vid` | `uint32` | DVN instance identifier | | `_target` | `address` | Target contract address | | `_callData` | `bytes` | Encoded function call data | | `_expiration` | `uint256` | Expiration timestamp | **Returns:** Keccak256 hash of the packed parameters. *** ### OnlyMessageLib Methods These methods can only be called by authorized Message Libraries. #### `assignJob` (ULN302) Assigns a verification job for ULN302 messages. ```solidity wrap theme={null} function assignJob( AssignJobParam calldata _param, bytes calldata _options ) external payable onlyRole(MESSAGE_LIB_ROLE) returns (uint256 totalFee) ``` | Parameter | Type | Description | | ---------- | ---------------- | --------------------- | | `_param` | `AssignJobParam` | Job parameters struct | | `_options` | `bytes` | DVN-specific options | **Emits:** None directly (fee calculation delegated to DVNFeeLib). *** #### `assignJob` (ULNv2 Legacy) Assigns a verification job for legacy ULNv2 messages. ```solidity wrap theme={null} function assignJob( uint16 _dstEid, uint16 _outboundProofType, uint64 _confirmations, address _sender ) external onlyRole(MESSAGE_LIB_ROLE) returns (uint256 totalFee) ``` **Emits:** `VerifierFeePaid(uint256 fee)` *** #### `assignJob` (Read/CmdLib) Assigns a verification job for lzRead commands. ```solidity wrap theme={null} function assignJob( address _sender, bytes calldata _packetHeader, bytes calldata _cmd, bytes calldata _options ) external payable onlyRole(MESSAGE_LIB_ROLE) returns (uint256 fee) ``` *** ### OnlyAdmin Methods These methods require the `ADMIN_ROLE`. #### `setDstConfig` Configures fee parameters for destination chains. ```solidity wrap theme={null} function setDstConfig(DstConfigParam[] calldata _params) external onlyRole(ADMIN_ROLE) ``` | Parameter | Type | Description | | --------- | ------------------ | ----------------------------------- | | `_params` | `DstConfigParam[]` | Array of destination configurations | **Emits:** `SetDstConfig(DstConfigParam[] params)` *** #### `execute` Executes a batch of signed instructions. This is the primary method for submitting verifications. ```solidity wrap theme={null} function execute(ExecuteParam[] calldata _params) external onlyRole(ADMIN_ROLE) ``` | Parameter | Type | Description | | --------- | ---------------- | ------------------------------------ | | `_params` | `ExecuteParam[]` | Array of signed execution parameters | **Behavior:** * Skips instructions with invalid VID * Skips expired instructions * Validates signatures against quorum * Prevents replay attacks via hash tracking * Emits events for failures but continues processing **Emits:** * `VerifySignaturesFailed(uint256 idx)` - if signature validation fails * `ExecuteFailed(uint256 index, bytes data)` - if execution fails * `HashAlreadyUsed(ExecuteParam param, bytes32 hash)` - if instruction was already executed *** #### `withdrawFeeFromUlnV2` Withdraws accumulated fees from ULNv2 Message Library. ```solidity wrap theme={null} function withdrawFeeFromUlnV2( address _lib, address payable _to, uint256 _amount ) external onlyRole(ADMIN_ROLE) ``` | Parameter | Type | Description | | --------- | ----------------- | ----------------------------- | | `_lib` | `address` | ULNv2 Message Library address | | `_to` | `address payable` | Recipient address | | `_amount` | `uint256` | Amount to withdraw | *** ### OnlySelf Methods These methods can only be called by the contract itself (via signed execute). #### `setSigner` Adds or removes a signer from the multisig. ```solidity wrap theme={null} function setSigner(address _signer, bool _active) external onlySelf ``` | Parameter | Type | Description | | --------- | --------- | -------------------------------- | | `_signer` | `address` | Signer address | | `_active` | `bool` | `true` to add, `false` to remove | **Function Signature:** `0x31cb6105` *** #### `setQuorum` Sets the required number of signatures for multisig operations. ```solidity wrap theme={null} function setQuorum(uint64 _quorum) external onlySelf ``` | Parameter | Type | Description | | --------- | -------- | -------------------- | | `_quorum` | `uint64` | New quorum threshold | **Function Signature:** `0x8585c945` *** ### Quorum Methods #### `quorumChangeAdmin` Allows the signer quorum to change the admin role without going through the standard execute flow. ```solidity wrap theme={null} function quorumChangeAdmin(ExecuteParam calldata _param) external ``` | Parameter | Type | Description | | --------- | -------------- | ------------------------------------------------------------- | | `_param` | `ExecuteParam` | Signed instruction with new admin address encoded in callData | **Usage:** The `callData` field should contain `abi.encode(newAdminAddress)`. This function ensures signers maintain ultimate control over the DVN. Even if the admin role is delegated to a service like Essence, signers can immediately reassign it. *** ## Events ### Core Events | Event | Signature | Description | | ------------------------ | --------------------------------------------------- | --------------------------------------------------------------------------- | | `VerifySignaturesFailed` | `VerifySignaturesFailed(uint256 idx)` | Signature verification failed at the specified index during batch execution | | `ExecuteFailed` | `ExecuteFailed(uint256 index, bytes data)` | Execution failed at the specified index with return data | | `HashAlreadyUsed` | `HashAlreadyUsed(ExecuteParam param, bytes32 hash)` | Attempted replay of an already-executed instruction | | `VerifierFeePaid` | `VerifierFeePaid(uint256 fee)` | Fee paid for ULNv2 verification job | | `SetDstConfig` | `SetDstConfig(DstConfigParam[] params)` | Destination configuration updated | ### Inherited Events (from Worker) | Event | Signature | Description | | ------------------------- | ----------------------------------------------- | ------------------------------ | | `SetWorkerFeeLib` | `SetWorkerFeeLib(address feeLib)` | Fee library address updated | | `SetPriceFeed` | `SetPriceFeed(address priceFeed)` | Price feed address updated | | `SetDefaultMultiplierBps` | `SetDefaultMultiplierBps(uint16 multiplierBps)` | Default fee multiplier updated | | `Withdraw` | `Withdraw(address to, uint256 amount)` | Fees withdrawn from worker | *** ## Errors ### DVN Errors | Error | Signature | Description | | ------------------------ | -------------------------------------------- | ------------------------------------------------------------------ | | `DVN_OnlySelf` | `DVN_OnlySelf()` | Action requires the contract to call itself (via signed execute) | | `DVN_InvalidRole` | `DVN_InvalidRole(bytes32 role)` | Specified role is not valid for the operation | | `DVN_InstructionExpired` | `DVN_InstructionExpired()` | The signed instruction has passed its expiration timestamp | | `DVN_InvalidTarget` | `DVN_InvalidTarget(address target)` | Target address is not valid for this operation | | `DVN_InvalidVid` | `DVN_InvalidVid(uint32 vid)` | VID in instruction does not match this DVN instance | | `DVN_InvalidSignatures` | `DVN_InvalidSignatures()` | Signature verification failed (invalid or insufficient signatures) | | `DVN_DuplicatedHash` | `DVN_DuplicatedHash(bytes32 executableHash)` | Instruction hash has already been executed (replay prevention) | ### DVNFeeLib Errors | Error | Signature | Description | | --------------------------- | ------------------------------------------------------- | ------------------------------------------------------------ | | `DVN_EidNotSupported` | `DVN_EidNotSupported(uint32 eid)` | Destination endpoint ID is not configured (gas = 0) | | `DVN_INVALID_INPUT_LENGTH` | `DVN_INVALID_INPUT_LENGTH()` | Array lengths do not match in configuration | | `DVN_TimestampOutOfRange` | `DVN_TimestampOutOfRange(uint32 eid, uint64 timestamp)` | Read request timestamp is outside the valid retention window | | `DVN_UnsupportedOptionType` | `DVN_UnsupportedOptionType(uint8 optionType)` | DVN option type is not supported | ### Inherited Errors (from Worker) | Error | Signature | Description | | ----------------------- | ------------------------- | ------------------------------------------- | | `Worker_OnlyMessageLib` | `Worker_OnlyMessageLib()` | Caller is not an authorized Message Library | | `Worker_NotAllowed` | `Worker_NotAllowed()` | Sender is on denylist or not on allowlist | *** ## Data Structures ### DstConfig Configuration for a destination chain's fee parameters. ```solidity wrap theme={null} struct DstConfig { uint64 gas; // Base gas cost for verification uint16 multiplierBps; // Fee multiplier in basis points (10000 = 100%) uint128 floorMarginUSD; // Minimum margin in USD (scaled) } ``` | Field | Type | Description | | ---------------- | --------- | ------------------------------------------------------------ | | `gas` | `uint64` | Base gas units required for verification on this destination | | `multiplierBps` | `uint16` | Fee multiplier (0 uses default, 10000 = 1x, 12000 = 1.2x) | | `floorMarginUSD` | `uint128` | Minimum fee floor in USD to ensure profitability | *** ### DstConfigParam Parameter struct for setting destination configuration. ```solidity wrap theme={null} struct DstConfigParam { uint32 dstEid; // Destination endpoint ID uint64 gas; // Base gas cost uint16 multiplierBps; // Fee multiplier uint128 floorMarginUSD; // Minimum margin } ``` *** ### ExecuteParam Parameters for executing a signed instruction. ```solidity wrap theme={null} struct ExecuteParam { uint32 vid; // DVN instance identifier address target; // Target contract address bytes callData; // Encoded function call uint256 expiration; // Expiration timestamp bytes signatures; // Concatenated signatures } ``` | Field | Type | Description | | ------------ | --------- | ------------------------------------------------- | | `vid` | `uint32` | Must match this DVN's VID | | `target` | `address` | Contract to call (often the receive MessageLib) | | `callData` | `bytes` | ABI-encoded function call (e.g., `verify(...)`) | | `expiration` | `uint256` | Unix timestamp after which instruction is invalid | | `signatures` | `bytes` | Concatenated 65-byte ECDSA signatures | *** ### AssignJobParam Parameters passed when a verification job is assigned. ```solidity wrap theme={null} struct AssignJobParam { uint32 dstEid; // Destination endpoint ID bytes packetHeader; // Full packet header bytes32 payloadHash; // Hash of message payload uint64 confirmations; // Required block confirmations address sender; // OApp that sent the message } ``` *** ### FeeParams (DVNFeeLib) Parameters used for fee calculation. ```solidity wrap theme={null} struct FeeParams { address priceFeed; // Price feed contract address uint32 dstEid; // Destination endpoint ID uint64 confirmations; // Required confirmations address sender; // OApp sender uint64 quorum; // Current quorum setting uint16 defaultMultiplierBps; // Default fee multiplier } ``` *** ## Access Control Roles The DVN contract uses role-based access control inherited from OpenZeppelin's AccessControl. | Role | Description | Controlled By | | ------------------ | ----------------------------------------------------------- | ----------------------------------------- | | `ADMIN_ROLE` | Can execute signed instructions, set configs, withdraw fees | Admin (or quorum via `quorumChangeAdmin`) | | `MESSAGE_LIB_ROLE` | Can call `assignJob` to request verifications | Self (via signed execute) | | `ALLOWLIST` | Addresses permitted to use the DVN | Self (via signed execute) | | `DENYLIST` | Addresses blocked from using the DVN | Self (via signed execute) | *** ## Admin Role Permissions in Practice The `ADMIN_ROLE` is an operational role, and its name overstates what it can do. It does not own the DVN or control verification on its own: the `execute` function validates a signer quorum on every message, so the admin alone cannot verify or forge messages. Signers keep ultimate control and can reassign the role at any time through [`quorumChangeAdmin`](#quorumchangeadmin). The role exists so that an operator can run day-to-day operations (fees, pricing, pathway configuration, and message submission) on a partner's behalf. In the Essence/Gasolina model, LZ holds the admin role and pays for cross-chain message execution for the partner. The sections below describe when each `ADMIN_ROLE`-gated function is used. ### Fee management | Function | Usage | | ------------------------- | --------------------------------------------------------- | | `setPriceFeed` | On deployment and whenever the price feed changes | | `setWorkerFeeLib` | On deployment and whenever the worker fee library changes | | `setDefaultMultiplierBps` | On deployment and whenever the fee calculation changes | | `withdrawFee` | Whenever fees are collected | | `withdrawToken` | Whenever fees are collected | | `withdrawFeeFromUlnV2` | Whenever fees are collected | ```solidity wrap theme={null} function setPriceFeed(address _priceFeed) external onlyRole(ADMIN_ROLE) function setWorkerFeeLib(address _workerFeeLib) external onlyRole(ADMIN_ROLE) function setDefaultMultiplierBps(uint16 _multiplierBps) external onlyRole(ADMIN_ROLE) function withdrawFee(address _lib, address _to, uint256 _amount) external onlyRole(ADMIN_ROLE) function withdrawToken(address _token, address _to, uint256 _amount) external onlyRole(ADMIN_ROLE) function withdrawFeeFromUlnV2(address _lib, address payable _to, uint256 _amount) external onlyRole(ADMIN_ROLE) ``` In the Essence/Gasolina model, Essence manages and collects fees and keeps pricing information onchain because LZ pays for cross-chain message execution. Apart from `withdrawFeeFromUlnV2`, these functions are inherited from the base `Worker` contract that the DVN extends. ### Chain expansion | Function | Usage | | ------------------------- | --------------------- | | `setSupportedOptionTypes` | Every chain expansion | | `setDstConfig` | Every chain expansion | ```solidity wrap theme={null} function setSupportedOptionTypes(uint32 _eid, uint8[] calldata _optionTypes) external onlyRole(ADMIN_ROLE) function setDstConfig(DstConfigParam[] calldata _params) external onlyRole(ADMIN_ROLE) ``` Both functions configure a new pathway when expanding to a new chain. Without them, the deploy-and-wire service cannot be operated on the partner's behalf. ### Execute | Function | Usage | | --------- | ------------------------------- | | `execute` | Every message this DVN verifies | ```solidity wrap theme={null} function execute(ExecuteParam[] calldata _params) external onlyRole(ADMIN_ROLE) ``` `execute` is the entry point the admin uses to submit signed verification instructions for the messages this DVN handles. Even though it is `ADMIN_ROLE`-gated, every instruction must carry a valid signer-quorum signature, so holding the admin role alone does not let the holder verify messages. The admin cannot lock signers out. Independently of `execute`, the signer quorum can reassign the `ADMIN_ROLE` at any time by calling [`quorumChangeAdmin`](#quorumchangeadmin), which only requires a valid quorum signature. Ultimate control stays with the signers. ### Role management | Function | Usage | | ------------ | -------------------------------------------- | | `grantRole` | Whenever an admin wallet is added or rotated | | `revokeRole` | Whenever an admin wallet is rotated out | ```solidity wrap theme={null} function grantRole(bytes32 _role, address _account) public override onlySelfOrAdmin(_role) function revokeRole(bytes32 _role, address _account) public override onlySelfOrAdmin(_role) ``` When called by an admin, `grantRole` and `revokeRole` operate only on the `ADMIN_ROLE`, letting the operator add or remove admin wallets. Additional admin wallets are used to parallelize message submission and scale delivery throughput to a chain, while revoking is used during wallet rotation. The `ALLOWLIST`, `DENYLIST`, and `MESSAGE_LIB_ROLE` roles cannot be changed by an admin directly; they are managed by the contract itself through signed `execute` calls, as enforced by the `onlySelfOrAdmin` modifier. *** ## Constructor Parameters ```solidity wrap theme={null} constructor( uint32 _localEidV2, // Local endpoint V2 ID uint32 _vid, // Unique DVN instance identifier address[] memory _messageLibs, // Initial Message Libraries address _priceFeed, // Price feed contract address[] memory _signers, // Initial multisig signers uint64 _quorum, // Initial quorum requirement address[] memory _admins // Initial admin addresses ) ``` | Parameter | Description | | -------------- | ------------------------------------------------------------------------------ | | `_localEidV2` | The endpoint ID for this chain (used for lzRead) | | `_vid` | Unique identifier for this DVN instance (typically `eidV1` or `eidV2 % 30000`) | | `_messageLibs` | Array of Message Library addresses granted `MESSAGE_LIB_ROLE` | | `_priceFeed` | Contract providing cross-chain gas price estimates | | `_signers` | Array of addresses that can sign verification instructions | | `_quorum` | Number of signatures required for execution | | `_admins` | Addresses granted `ADMIN_ROLE` for operational management | *** ## State Variables | Variable | Type | Visibility | Description | | ------------ | ------------------------------ | ------------------ | ---------------------------------- | | `vid` | `uint32` | `public immutable` | Unique DVN instance identifier | | `localEidV2` | `uint32` | `public immutable` | Local endpoint V2 ID | | `dstConfig` | `mapping(uint32 => DstConfig)` | `public` | Fee configuration per destination | | `usedHashes` | `mapping(bytes32 => bool)` | `public` | Tracks executed instruction hashes | *** ## Related Resources * [DVN Overview](/v2/workers/off-chain/dvn-overview) - Conceptual introduction to DVNs * [Build DVNs](/v2/workers/off-chain/build-dvns) - Traditional DVN implementation guide * [Gasolina Overview](/v2/workers/off-chain/gasolina-overview) - Simplified DVN with gas abstraction * [Security Stack DVNs](/v2/concepts/modular-security/security-stack-dvns) - OApp security configuration * [DVN Source Code](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/DVN.sol) - Reference implementation # DVN Troubleshooting Source: https://docs.layerzero.network/v2/workers/off-chain/dvn-troubleshooting Diagnose and resolve common issues with DVN contracts, Gasolina services, and infrastructure deployments for LayerZero V2. This guide helps DVN operators diagnose and resolve common issues with DVN contracts, Gasolina services, and infrastructure deployments. ## Contract Error Reference ### DVN Contract Errors These errors are emitted by the DVN smart contract when operations fail. | Error | Signature | Cause | Solution | | ------------------------ | ----------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | | `DVN_OnlySelf` | `DVN_OnlySelf()` | Calling a restricted function directly instead of through signed execute | Use the `execute` function with proper signatures to call `setSigner`, `setQuorum`, or role management functions | | `DVN_InvalidRole` | `DVN_InvalidRole(bytes32 role)` | Attempting to grant/revoke an invalid role | Use only valid roles: `ADMIN_ROLE`, `MESSAGE_LIB_ROLE`, `ALLOWLIST`, or `DENYLIST` | | `DVN_InstructionExpired` | `DVN_InstructionExpired()` | The signed instruction's expiration timestamp has passed | Generate new signatures with a future expiration timestamp | | `DVN_InvalidTarget` | `DVN_InvalidTarget(address target)` | Target address doesn't match the DVN contract | Verify the target address in `ExecuteParam` matches the DVN contract address | | `DVN_InvalidVid` | `DVN_InvalidVid(uint32 vid)` | VID in the instruction doesn't match this DVN instance | Ensure the VID matches the DVN's configured VID (check `vid()` on contract) | | `DVN_InvalidSignatures` | `DVN_InvalidSignatures()` | Signature verification failed | Verify signers match those registered in the contract and signatures are correctly formatted | | `DVN_DuplicatedHash` | `DVN_DuplicatedHash(bytes32 hash)` | Attempting to replay an already-executed instruction | Generate a new instruction with different parameters or expiration | ### DVNFeeLib Errors These errors relate to fee calculation and configuration. | Error | Signature | Cause | Solution | | --------------------------- | ------------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------- | | `DVN_EidNotSupported` | `DVN_EidNotSupported(uint32 eid)` | Destination endpoint ID is not configured | Add the destination to `DstConfig` using `setDstConfig` | | `DVN_INVALID_INPUT_LENGTH` | `DVN_INVALID_INPUT_LENGTH()` | Array lengths don't match in batch operations | Ensure all arrays have matching lengths | | `DVN_TimestampOutOfRange` | `DVN_TimestampOutOfRange(uint32 eid, uint64 timestamp)` | Read request timestamp outside valid retention window | Adjust the timestamp to fall within `maxPastRetention` and `maxFutureRetention` bounds | | `DVN_UnsupportedOptionType` | `DVN_UnsupportedOptionType(uint8 optionType)` | DVN option type not supported | Check option encoding matches expected format | ### Worker Errors (Inherited) | Error | Signature | Cause | Solution | | ----------------------- | ------------------------- | ------------------------------------------- | ------------------------------------------------------------------------ | | `Worker_OnlyMessageLib` | `Worker_OnlyMessageLib()` | Caller is not an authorized Message Library | Verify the caller has `MESSAGE_LIB_ROLE` | | `Worker_NotAllowed` | `Worker_NotAllowed()` | Sender is on denylist or not on allowlist | Check ACL configuration; add sender to allowlist or remove from denylist | *** ## DVN Contract Events Monitor these events to track DVN operations: | Event | When Emitted | Action Required | | ------------------------------------------ | -------------------------------------------------- | ----------------------------------------------- | | `VerifySignaturesFailed(uint256 idx)` | Signature verification failed during batch execute | Check signer configuration and signature format | | `ExecuteFailed(uint256 index, bytes data)` | Execution of instruction failed | Decode return data for specific error | | `HashAlreadyUsed(ExecuteParam, bytes32)` | Replay attempt detected | Instruction already executed; generate new one | | `VerifierFeePaid(uint256 fee)` | Fee paid for ULNv2 verification | Informational; fee collection successful | | `SetDstConfig(DstConfigParam[])` | Destination configuration updated | Informational; verify new config is correct | *** ## Gasolina Service Issues ### Health Check Failures **Symptom**: `GET /` doesn't return "HEALTHY" **Possible Causes:** 1. **Service not running** ```bash theme={null} # AWS: Check ECS task status aws ecs describe-tasks --cluster --tasks # GCP: Check Cloud Run status gcloud run services describe gasolina-api --region= ``` 2. **Container crash loop** ```bash theme={null} # AWS: Check logs aws logs tail /ecs/gasolina-api --follow # GCP: Check logs gcloud logging read "resource.type=cloud_run_revision" --limit 100 ``` 3. **Port misconfiguration** * Verify the container is listening on the correct port (default: 8999 for GCP) * Check load balancer health check configuration *** ### RPC Connection Failures **Symptom**: Signature requests fail with RPC-related errors **Diagnostic Steps:** 1. **Verify RPC endpoints are accessible:** ```bash theme={null} curl -X POST \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' ``` 2. **Check provider configuration:** * Verify `providers.json` contains valid endpoints * Ensure API keys are correct and not rate-limited * Confirm chain IDs match expected values 3. **Add backup providers:** ```json theme={null} { "1": { "chainName": "ethereum", "uris": ["https://primary-rpc.example.com", "https://backup-rpc.example.com"] } } ``` **Solutions:** * Add multiple RPC providers per chain for redundancy * Monitor RPC usage to avoid rate limits * Use dedicated RPC endpoints for production *** ### Signature Generation Failures **Symptom**: API returns 500 error when requesting signatures **Possible Causes:** 1. **KMS key access issues (AWS/GCP)** ```bash theme={null} # AWS: Check KMS key permissions aws kms describe-key --key-id # GCP: Check KMS permissions gcloud kms keys describe \ --keyring=gasolinaKeyRing --location=global ``` 2. **Mnemonic secret not found** ```bash theme={null} # AWS: Verify secret exists aws secretsmanager get-secret-value --secret-id ``` 3. **Event not found on chain** * Verify transaction hash is correct * Ensure RPC provider is synced * Check if transaction has been mined **Solutions:** * Verify IAM/service account permissions for KMS * Check secret manager configuration * Wait for transaction confirmation before requesting signatures *** ## Infrastructure Issues ### AWS Deployment Failures #### "Resource already exists" Error **Cause**: Previous deployment artifacts weren't cleaned up **Solution:** ```bash wrap theme={null} # Delete the CloudWatch log group aws logs delete-log-group --log-group-name GasolinaMetricLogGroup # Delete the S3 bucket (empty it first) aws s3 rm s3://providerconfigs---gasolina --recursive aws s3 rb s3://providerconfigs---gasolina # Retry deployment cd cdk/gasolina cdk deploy ``` #### CDK Bootstrap Required **Symptom**: Deployment fails with "This stack uses assets, so the toolkit stack must be deployed" **Solution:** ```bash wrap theme={null} cd cdk/gasolina cdk bootstrap cdk deploy ``` *** ### GCP Deployment Failures #### "API has not been used in project" Error **Cause**: GCP APIs need time to propagate after enabling **Solution:** 1. Wait 2-5 minutes after enabling APIs 2. Retry the Terraform apply: ```bash theme={null} terraform apply --var-file=lz-mainnet-verifier.tfvars ``` #### KeyRing Creation Error **Cause**: Cloud KMS API not fully enabled **Solution:** 1. Visit the Cloud KMS API page in GCP Console 2. Ensure the API is enabled 3. Wait a few minutes and retry *** ### Accessing Logs #### AWS CloudWatch ```bash wrap theme={null} # Tail logs in real-time aws logs tail /ecs/gasolina-api --follow # Search for errors aws logs filter-log-events \ --log-group-name /ecs/gasolina-api \ --filter-pattern "ERROR" # Get logs from specific time range aws logs filter-log-events \ --log-group-name /ecs/gasolina-api \ --start-time $(date -d '1 hour ago' +%s000) \ --end-time $(date +%s000) ``` #### GCP Cloud Logging ```bash wrap theme={null} # Recent logs gcloud logging read "resource.type=cloud_run_revision" --limit 50 # Filter by severity gcloud logging read "resource.type=cloud_run_revision AND severity>=ERROR" --limit 50 # Logs from specific time gcloud logging read "resource.type=cloud_run_revision AND timestamp>=\"2024-01-01T00:00:00Z\"" ``` *** ## Verification Failures ### Block Confirmation Issues **Symptom**: Signatures not generated; waiting for confirmations **Diagnostic:** ```bash wrap theme={null} # Check current block vs transaction block curl -X POST \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' ``` **Solutions:** * Wait for required confirmations to pass * Verify RPC provider is synced to chain head * Check if chain is experiencing delays *** ### Signer Mismatch **Symptom**: `DVN_InvalidSignatures` error when submitting to contract **Diagnostic:** 1. Query signers from Gasolina: ```bash theme={null} curl "https://your-gasolina.com/signer-info?chainName=ethereum" ``` 2. Query signers from contract: ```solidity theme={null} // Check if address is a signer dvn.isSigner(address) ``` **Solutions:** * Ensure Gasolina signers match those registered in the DVN contract * If signers changed, update the DVN contract using `setSigner` * Verify you're using the correct Gasolina instance *** ### Quorum Not Met **Symptom**: Verification transaction reverts despite having signatures **Diagnostic:** 1. Check required quorum on contract: ```solidity theme={null} dvn.quorum() ``` 2. Count signatures in your payload **Solutions:** * Ensure you have at least `quorum` signatures * Verify all signers are valid (not removed) * Check signature order (some chains require alphabetical ordering) *** ## Common Scenarios ### Scenario: New Chain Support **Issue**: DVN doesn't support a newly added chain **Steps:** 1. Add RPC providers for the new chain to `providers.json` 2. Update `availableChainNames` in config 3. Redeploy Gasolina 4. Request DVN contract deployment on the new chain from LayerZero 5. Update `setDstConfig` for fee configuration *** ### Scenario: Key Rotation **Issue**: Need to rotate signing keys **Steps:** 1. Generate new keys (KMS or mnemonic) 2. Add new signer to DVN contract: ```bash theme={null} ts-node scripts/configChangePayloads/createAddOrRemoveSignerSignatures.ts \ -e mainnet -c ethereum,bsc -q 2 \ --signerAddress 0xNewSigner --shouldRevoke 0 ``` 3. Update Gasolina configuration with new key 4. Redeploy Gasolina 5. Optionally remove old signer: ```bash theme={null} ts-node scripts/configChangePayloads/createAddOrRemoveSignerSignatures.ts \ -e mainnet -c ethereum,bsc -q 2 \ --signerAddress 0xOldSigner --shouldRevoke 1 ``` *** ### Scenario: Taking Over Admin Role **Issue**: Need to take direct control from Essence **Steps:** 1. Prepare a wallet to receive admin role 2. Generate `quorumChangeAdmin` signatures: ```javascript theme={null} const newAdmin = '0xYourAddress'; const callData = ethers.utils.defaultAbiCoder.encode(['address'], [newAdmin]); ``` 3. Sign with Gasolina signers 4. Call `quorumChangeAdmin` directly on DVN contract 5. Now you control transaction submission After taking admin control, you're responsible for: * Managing gas across all chains * Submitting verification transactions * Monitoring and operational tasks *** ## Debugging Commands Reference ### Contract Queries ```bash wrap theme={null} # Using cast (Foundry) # Check VID cast call "vid()(uint32)" --rpc-url # Check quorum cast call "quorum()(uint64)" --rpc-url # Check if address is signer cast call "isSigner(address)(bool)"
--rpc-url # Check destination config cast call "dstConfig(uint32)(uint64,uint16,uint128)" --rpc-url ``` ### Gasolina API Tests ```bash wrap theme={null} # Health check curl -s https://your-gasolina.com/ | grep HEALTHY # Signer info curl -s "https://your-gasolina.com/signer-info?chainName=ethereum" | jq # Test with sample message ts-node scripts/testDeployment.ts -u https://your-gasolina.com -e mainnet ``` ### Log Analysis ```bash wrap theme={null} # AWS: Find signature failures aws logs filter-log-events \ --log-group-name /ecs/gasolina-api \ --filter-pattern "signature" \ --limit 20 # GCP: Find errors gcloud logging read \ "resource.type=cloud_run_revision AND textPayload:error" \ --limit 20 ``` *** ## Getting Help If you've exhausted these troubleshooting steps: 1. **Check Documentation:** * [Gasolina Overview](/v2/workers/off-chain/gasolina-overview) * [Implementation Guide](/v2/workers/off-chain/gasolina-implementation) * [DVN Technical Reference](/v2/workers/off-chain/dvn-technical-reference) 2. **GitHub Issues:** * [gasolina-aws issues](https://github.com/LayerZero-Labs/gasolina-aws/issues) * [gasolina-gcp issues](https://github.com/LayerZero-Labs/gasolina-gcp/issues) 3. **Community Support:** * [LayerZero Discord](https://discord.com/invite/ktbvm8Nkcr) When reporting issues, include: * Error messages and logs * Configuration (redact secrets) * Chain and environment * Steps to reproduce # Gasolina API Reference Source: https://docs.layerzero.network/v2/workers/off-chain/gasolina-api-reference Complete REST API documentation for Gasolina, including endpoints, request/response formats, error handling, and integration patterns for DVN operators. This page documents the Gasolina REST API endpoints, request/response formats, and integration patterns. The Gasolina API is a lightweight service that verifies LayerZero messages and produces signatures for DVN contracts. ## Base URL After deploying Gasolina, your API will be available at: * **AWS**: `https://.execute-api..amazonaws.com` * **GCP**: `https://-.a.run.app` *** ## Endpoints ### Health Check Verify that the Gasolina service is running. ``` GET / ``` **Response:** ``` HEALTHY ``` **Example:** ```bash wrap theme={null} curl https://your-gasolina-instance.com/ ``` *** ### Get Signer Info Retrieve the signer addresses registered with this Gasolina instance for a specific chain. ``` GET /signer-info?chainName={chainName} ``` **Query Parameters:** | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ---------------------------------------------------- | | `chainName` | string | Yes | The chain name (e.g., `ethereum`, `bsc`, `arbitrum`) | **Response:** ```typescript wrap theme={null} { signers: string[] // Array of signer addresses } ``` **Example:** ```bash wrap theme={null} curl "https://your-gasolina-instance.com/signer-info?chainName=ethereum" ``` **Response:** ```json wrap theme={null} { "signers": [ "0x1234567890123456789012345678901234567890", "0x0987654321098765432109876543210987654321" ] } ``` The signer addresses returned here are what LayerZero uses when deploying the DVN contracts. These addresses must match the signers registered in the onchain DVN contract. *** ### Request Signatures Request signatures for a LayerZero message verification. This is the primary endpoint called by Essence to obtain verification signatures. ``` POST / ``` **Request Body:** ```typescript wrap theme={null} interface SignatureRequest { lzMessageId: { srcUAAddress: string; // Source OApp address dstUAAddress: string; // Destination OApp address srcChainId: string; // Source endpoint ID dstChainId: string; // Destination endpoint ID srcChainName: string; // Source chain name (e.g., "ethereum") dstChainName: string; // Destination chain name (e.g., "bsc") nonce: number; // Message nonce }; srcTxHash: string; // Source transaction hash expiration: number; // Signature expiration (Unix timestamp) blockConfirmation: number; // Required block confirmations ulnVersion: 'V2' | 'V302'; // ULN version self?: string; // Optional: self reference for callbacks skipVId?: boolean; // Optional: skip VID validation } ``` **Response:** ```typescript wrap theme={null} interface SignatureResponse { body: { signatures: Array<{ signature: string; // Hex-encoded ECDSA signature address: string; // Signer address (for verification) }>; }; } ``` **Example Request:** ```bash wrap theme={null} curl -X POST https://your-gasolina-instance.com/ \ -H "Content-Type: application/json" \ -d '{ "lzMessageId": { "srcUAAddress": "0x4fa745fccc04555f2afa8874cd23961636cdf982", "dstUAAddress": "0xe9f183fc656656f1f17af1f2b0df79b8ff9ad8ed", "srcChainId": "101", "dstChainId": "102", "srcChainName": "ethereum", "dstChainName": "bsc", "nonce": 190 }, "srcTxHash": "0x2c58710ed1a83e2fff10adb0eb2b70f9262df6937d45e65a6fca5f2a043e7332", "expiration": 1701410857, "blockConfirmation": 15, "ulnVersion": "V2", "skipVId": false }' ``` **Example Response:** ```json wrap theme={null} { "body": { "signatures": [ { "signature": "0x1234...abcd", "address": "0x1234567890123456789012345678901234567890" }, { "signature": "0x5678...efgh", "address": "0x0987654321098765432109876543210987654321" } ] } } ``` *** ## Request Parameters Reference ### lzMessageId Object | Field | Type | Description | | -------------- | ------ | ---------------------------------------------- | | `srcUAAddress` | string | The OApp address on the source chain | | `dstUAAddress` | string | The OApp address on the destination chain | | `srcChainId` | string | LayerZero endpoint ID of the source chain | | `dstChainId` | string | LayerZero endpoint ID of the destination chain | | `srcChainName` | string | Human-readable source chain name | | `dstChainName` | string | Human-readable destination chain name | | `nonce` | number | Sequential message nonce for this pathway | ### Top-Level Fields | Field | Type | Description | | ------------------- | -------- | ----------------------------------------------- | | `srcTxHash` | string | Transaction hash where `PacketSent` was emitted | | `expiration` | number | Unix timestamp after which signature is invalid | | `blockConfirmation` | number | Number of block confirmations required | | `ulnVersion` | string | `"V2"` for ULNv2, `"V302"` for ULN302 | | `self` | string? | Optional callback reference | | `skipVId` | boolean? | If true, skip VID validation (default: false) | ### ULN Version Selection | Version | Description | When to Use | | ------- | ------------------- | ----------------------------------------- | | `V2` | Legacy ULN version | For messages using ULNv2 Message Library | | `V302` | Current ULN version | For messages using ULN302 Message Library | *** ## Error Responses ### HTTP Status Codes | Status | Meaning | Common Causes | | ------ | -------------- | ---------------------------------------- | | `200` | Success | Signatures generated successfully | | `400` | Bad Request | Invalid request body or parameters | | `404` | Not Found | Chain not supported or message not found | | `500` | Internal Error | RPC failure or signing error | ### Common Error Scenarios #### Chain Not Supported ```json wrap theme={null} { "error": "Chain not available", "chainName": "unsupported-chain" } ``` **Solution**: Ensure the chain is listed in your `availableChainNames` configuration and has RPC providers configured. #### Message Not Found ```json wrap theme={null} { "error": "PacketSent event not found", "txHash": "0x..." } ``` **Solution**: Verify the transaction hash is correct and the transaction has been mined. Check that your RPC providers are synced. #### Block Confirmations Not Met ```json wrap theme={null} { "error": "Insufficient block confirmations", "required": 15, "current": 5 } ``` **Solution**: Wait for more blocks to be mined before retrying the request. #### RPC Provider Error ```json wrap theme={null} { "error": "Failed to connect to RPC provider" } ``` **Solution**: Check RPC provider configuration and ensure endpoints are accessible. Consider adding backup providers. *** ## Testing Your API ### Using the Test Script Both gasolina-aws and gasolina-gcp include a test script: ```bash wrap theme={null} # From repository root ts-node scripts/testDeployment.ts -u -e ``` **Options:** | Flag | Description | | ------------------- | ---------------------- | | `-u, --url` | Your Gasolina API URL | | `-e, --environment` | `mainnet` or `testnet` | **Successful Response:** ``` --- Sending request to https://your-api.com --- Sample request: { lzMessageId: {...}, srcTxHash: '0x...', ... } --- [200] Successful request --- Response: { signatures: [ { signature: '', address: '
' }, { signature: '', address: '
' } ] } ``` ### Manual Testing 1. **Test health check:** ```bash wrap theme={null} curl https://your-gasolina-instance.com/ # Expected: HEALTHY ``` 2. **Test signer info:** ```bash wrap theme={null} curl "https://your-gasolina-instance.com/signer-info?chainName=ethereum" # Expected: { "signers": ["0x...", "0x..."] } ``` 3. **Test with sample message:** ```bash wrap theme={null} # Create a test payload file cat > test-payload.json << 'EOF' { "lzMessageId": { "srcUAAddress": "0xc769361cce2a4a61572d59faf3b58065c6faac04", "dstUAAddress": "0xc769361cce2a4a61572d59faf3b58065c6faac04", "srcChainId": "40161", "dstChainId": "40102", "srcChainName": "sepolia", "dstChainName": "bsc", "nonce": 1 }, "srcTxHash": "0xc5171abb2c8601ff5062c27c12f32c609b89eb38876a2509a4cd6d5327c64564", "expiration": 1701493303, "blockConfirmation": 1, "ulnVersion": "V302", "skipVId": true } EOF # Send test request curl -X POST https://your-gasolina-instance.com/ \ -H "Content-Type: application/json" \ -d @test-payload.json ``` *** ## Extra Context Verification API If you've configured extra context verification, Gasolina will call your custom API for additional validation. ### Request Format (to your API) ```typescript wrap theme={null} interface ExtraContextRequest { sentEvent: { lzMessageId: { pathwayId: { srcEid: number; dstEid: number; sender: string; receiver: string; srcChainName: string; dstChainName: string; }; nonce: number; ulnSendVersion: string; }; guid: string; message: string; options: { lzReceive?: {gas: string; value: string}; nativeDrop?: Array<{amount: string; receiver: string}>; compose?: Array<{index: number; gas: string; value: string}>; ordered?: boolean; }; payload?: string; sendLibrary?: string; onChainEvent: { chainName: string; txHash: string; blockHash: string; blockNumber: number; }; }; from: string; } ``` ### Expected Response Your API should return a boolean indicating whether the message should be signed: ```json wrap theme={null} { "valid": true } ``` Or to reject: ```json wrap theme={null} { "valid": false } ``` *** ## Rate Limiting Considerations While Gasolina doesn't implement rate limiting by default, consider these factors: 1. **RPC Provider Limits**: Your RPC providers may have rate limits that affect signature generation speed. 2. **Cloud Provider Limits**: * AWS API Gateway has default throttling limits * GCP Cloud Run has concurrency limits 3. **Scaling**: * AWS ECS auto-scales based on CPU/memory * GCP Cloud Run auto-scales based on request volume *** ## Security Considerations 1. **No Authentication by Default**: The API is publicly accessible. Consider adding: * IP allowlisting at the load balancer level * API key authentication if needed * VPC/private endpoints for internal access 2. **Signer Key Security**: * Keys are never exposed via the API * Only signatures are returned, not private key material 3. **Request Validation**: * Gasolina independently verifies events via RPC * Block confirmation requirements are enforced * Invalid requests are rejected before signing *** ## Related Resources * [Gasolina Overview](/v2/workers/off-chain/gasolina-overview) - Architecture and security model * [Implementation Guide](/v2/workers/off-chain/gasolina-implementation) - Deployment instructions * [DVN Troubleshooting](/v2/workers/off-chain/dvn-troubleshooting) - Debugging common issues * [DVN Technical Reference](/v2/workers/off-chain/dvn-technical-reference) - Contract methods and events # Gasolina Implementation Guide Source: https://docs.layerzero.network/v2/workers/off-chain/gasolina-implementation Step-by-step instructions for deploying Gasolina on AWS or Google Cloud Platform, including configuration, signer setup, and integration with LayerZero. This guide provides step-by-step instructions for deploying your own Gasolina instance on AWS or Google Cloud Platform. Gasolina is a REST API service that verifies LayerZero messages and produces signatures for DVN contracts. ## Prerequisites Before deploying Gasolina, ensure you have: | Requirement | Details | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Cloud Provider Account** | AWS account with CDK permissions, or GCP account with billing enabled | | **Node.js** | Version 18 or higher | | **Package Manager** | npm, yarn, or pnpm | | **Cloud CLI** | AWS CLI + CDK CLI, or gcloud CLI + Terraform | | **RPC Providers** | Reliable endpoints for each supported chain. Production requires at least 3 to 4 independent provider entities per chain (multiple URLs from one vendor count as one); see the [production hardening checklist](#production-hardening-checklist). | *** ## AWS Implementation ### Step 1: Clone and Setup ```bash wrap theme={null} # Clone the repository git clone https://github.com/LayerZero-Labs/gasolina-aws.git cd gasolina-aws # Install dependencies pnpm install # or: yarn install ``` ### Step 2: Configure AWS Authentication ```bash wrap theme={null} # Configure AWS CLI with your credentials aws configure # Verify authentication aws sts get-caller-identity ``` ### Step 3: Choose Signer Type You have two options for managing signing keys: AWS KMS provides HSM-backed signing keys that are automatically created and managed: 1. Set `signerType: 'KMS'` in your configuration 2. Specify `kmsNumOfSigners` for the number of keys to create 3. Keys are automatically created during CDK deployment **Benefits:** * Hardware security module protection * Automatic key rotation support * No manual secret management Mnemonic-based signing is for local development and testing only. Mnemonics are extractable secrets and must never be used for production signers; production deployments must use AWS KMS (HSM-backed). Even for development, store mnemonics in AWS Secrets Manager or similar and never commit them to version control. See the [production hardening checklist](#production-hardening-checklist). For mnemonic-based signers, create secrets in AWS Secrets Manager: ```bash wrap theme={null} # Create a secret for each signer aws secretsmanager create-secret \ --name "gasolina-signer-1" \ --secret-string '{ "LAYERZERO_WALLET_MNEMONIC": "your twelve word mnemonic phrase here", "LAYERZERO_WALLET_PATH": "m/44'"'"'/60'"'"'/0'"'"'/0/0" }' ``` ### Step 4: Configure Infrastructure Edit `cdk/gasolina/config/index.ts`: ```typescript wrap theme={null} export const CONFIG = { '123456789012': { // Your AWS account number projectName: 'my-gasolina-mainnet', // Must be globally unique environment: 'mainnet', // or "testnet" availableChainNames: 'ethereum,bsc,avalanche,polygon,arbitrum,optimism', signerType: 'KMS', // production must use KMS; MNEMONIC is for local development/testing only kmsNumOfSigners: 3, // Only if using KMS // Optional: Extra context verification extraContextGasolinaUrl: 'https://your-verification-api.com/verify', }, }; ``` | Config Option | Description | | ------------------------- | ------------------------------------------------------------------------------------ | | `projectName` | Unique project identifier (used for S3 bucket naming) | | `environment` | `mainnet` or `testnet` | | `availableChainNames` | Comma-separated list of supported chains | | `signerType` | `KMS` (HSM-backed, required for production) or `MNEMONIC` (development/testing only) | | `kmsNumOfSigners` | Number of KMS signing keys to create | | `extraContextGasolinaUrl` | Optional custom verification endpoint | ### Step 5: Configure RPC Providers Edit `cdk/gasolina/config/providers/mainnet/providers.json`. Key each entry by chain name, list the independent provider entities under `uris`, and set `quorum` to how many must return matching responses: ```json wrap theme={null} { "ethereum": { "uris": [ "https://eth-mainnet.g.alchemy.com/v2/YOUR-API-KEY", "https://mainnet.infura.io/v3/YOUR-PROJECT-ID", "https://ethereum-rpc.publicnode.com" ], "quorum": 3 }, "bsc": { "uris": [ "https://bsc-dataseed1.binance.org", "https://bsc-rpc.publicnode.com", "https://bsc-mainnet.nodereal.io/v1/YOUR-API-KEY" ], "quorum": 3 } } ``` * Use at least 3 to 4 independent provider entities per chain for production quorum, not just failover; multiple URLs from a single vendor count as one fault domain * Prioritize reliable, independent providers (different vendors such as Alchemy, Infura, QuickNode, or an operator-run node) * Set `quorum` to the number of providers that must return matching responses (production: at least 3) ### Step 6: Configure Wallet Definitions (Mnemonic Only) This step applies only to the development and testing mnemonic flow. Production deployments use KMS (Step 3) and do not configure mnemonic wallet definitions. If using mnemonics, edit `cdk/gasolina/config/walletConfig/mainnet.json`: ```json wrap theme={null} { "definitions": [ { "address": "0x1234567890123456789012345678901234567890", "secretName": "gasolina-signer-1" }, { "address": "0x0987654321098765432109876543210987654321", "secretName": "gasolina-signer-2" } ] } ``` ### Step 7: Bootstrap CDK (First Time Only) ```bash wrap theme={null} cd cdk/gasolina cdk bootstrap ``` ### Step 8: Deploy Infrastructure ```bash wrap theme={null} # Review the deployment plan cdk diff # Deploy the infrastructure cdk deploy ``` After successful deployment, you'll see: ``` Outputs: Oracle.ApiGatewayUrl = https://xxxxxxxxxx.execute-api.region.amazonaws.com ``` ### Step 9: Test Deployment For production (`mainnet`), Gasolina must sit behind an authenticated gateway (IAM/SigV4, mTLS, or private networking) and status routes such as `/signer-info` and `/provider-health` must not be reachable by unauthenticated callers (see the [production hardening checklist](#production-hardening-checklist)). Run these probes through your authenticated control plane, not against a public URL. ```bash wrap theme={null} # Test the health endpoint curl https://xxxxxxxxxx.execute-api.region.amazonaws.com # Test signer info curl "https://xxxxxxxxxx.execute-api.region.amazonaws.com/signer-info?chainName=ethereum" # Run comprehensive test cd ../../ # Back to repository root ts-node scripts/testDeployment.ts -u https://xxxxxxxxxx.execute-api.region.amazonaws.com -e mainnet ``` A successful response looks like: ``` --- [200] Successful request --- Response: { signatures: [ { signature: '', address: '
' }, { signature: '', address: '
' } ] } ``` *** ## Google Cloud Platform Implementation ### Step 1: Clone and Setup ```bash wrap theme={null} # Clone the repository git clone https://github.com/LayerZero-Labs/gasolina-gcp.git cd gasolina-gcp # Install dependencies yarn install ``` ### Step 2: Configure GCP Project ```bash wrap theme={null} # Set your project gcloud config set project YOUR-PROJECT-ID # Authenticate gcloud auth application-default login # Enable required APIs gcloud services enable cloudkms.googleapis.com gcloud services enable run.googleapis.com gcloud services enable secretmanager.googleapis.com ``` ### Step 3: Create Terraform Backend Storage ```bash wrap theme={null} # Create a GCS bucket for Terraform state gsutil mb -p YOUR-PROJECT-ID -l US-EAST1 gs://your-project-gasolina-tfstate ``` ### Step 4: Configure Terraform Backend Edit `terraform/lz-mainnet-verifier.backend.conf`: ```hcl wrap theme={null} bucket = "your-project-gasolina-tfstate" prefix = "mainnet" ``` ### Step 5: Configure Infrastructure Variables Edit `terraform/lz-mainnet-verifier.tfvars`: ```hcl wrap theme={null} /* Project variables */ project = "your-gcp-project" project_id = "123456789012" region = "us-east1" zone = "us-east1-c" /* General variables */ env = "mainnet" /* KMS-HSM variables */ num_signers = 3 /* App variables */ app_name = "gasolina-api" available_chain_names = "ethereum,bsc,avalanche,polygon,arbitrum,optimism" ``` ### Step 6: Configure RPC Providers Edit `terraform/providers-mainnet.json`. Key each entry by chain name, list the independent provider entities under `uris`, and set `quorum` to how many must return matching responses: ```json wrap theme={null} { "ethereum": { "uris": [ "https://eth-mainnet.g.alchemy.com/v2/YOUR-API-KEY", "https://mainnet.infura.io/v3/YOUR-PROJECT-ID", "https://ethereum-rpc.publicnode.com" ], "quorum": 3 }, "bsc": { "uris": [ "https://bsc-dataseed1.binance.org", "https://bsc-rpc.publicnode.com", "https://bsc-mainnet.nodereal.io/v1/YOUR-API-KEY" ], "quorum": 3 } } ``` * Use at least 3 to 4 independent provider entities per chain for production quorum; multiple URLs from a single vendor count as one fault domain * Set `quorum` to the number of providers that must return matching responses (production: at least 3) ### Step 7: Deploy with Terraform ```bash wrap theme={null} cd terraform # Initialize Terraform terraform init -backend-config=lz-mainnet-verifier.backend.conf -reconfigure # Review the deployment plan terraform plan --var-file=lz-mainnet-verifier.tfvars # Apply the deployment terraform apply --var-file=lz-mainnet-verifier.tfvars ``` ### Step 8: Test Deployment For production (`mainnet`), the Cloud Run service must require authentication (deploy with `--no-allow-unauthenticated` and front it with an authenticated gateway or private ingress) so these probes run through the authenticated control plane rather than the public default Cloud Run URL, and status routes such as `/signer-info` and `/provider-health` are not anonymously reachable (see the [production hardening checklist](#production-hardening-checklist)). ```bash wrap theme={null} # Get the Cloud Run URL GASOLINA_URL=$(gcloud run services describe gasolina-api \ --region=us-east1 \ --format='value(status.url)') # Test health endpoint curl $GASOLINA_URL # Test signer info curl "$GASOLINA_URL/signer-info?chainName=ethereum" # Run comprehensive test cd .. # Back to repository root ts-node scripts/testDeployment.ts -u $GASOLINA_URL -e mainnet ``` *** ## Integration with LayerZero Once your Gasolina instance is deployed and tested: ### Step 1: Share Gasolina URL Provide your Gasolina API endpoint to LayerZero Labs. This must be the authenticated gateway endpoint (IAM/SigV4, mTLS, or private-network ingress), not a publicly open URL; coordinate the required caller credentials or role with LayerZero so requests are authenticated per the [production hardening checklist](#production-hardening-checklist): ``` https://your-gasolina-instance.com ``` ### Step 2: DVN Contract Deployment LayerZero will: 1. Query your `/signer-info` endpoint to retrieve signer addresses 2. Deploy DVN contracts on all supported chains with: * Your signer addresses registered * Agreed-upon signer threshold * Essence wallet as initial `ADMIN_ROLE` holder 3. Provide you with the DVN contract addresses for each chain ```mermaid wrap theme={null} sequenceDiagram participant LZ as LayerZero participant G as Your Gasolina participant C as Chains LZ->>G: GET /signer-info G->>LZ: Signer addresses LZ->>C: Deploy DVN contracts C->>LZ: Contract addresses LZ->>You: DVN addresses for OApp config ``` ### Step 3: OApp Configuration OApps configure your DVN using the contract addresses: ```solidity wrap theme={null} // Example UlnConfig for OApp UlnConfig({ confirmations: 15, requiredDVNCount: 2, optionalDVNCount: 0, optionalDVNThreshold: 0, requiredDVNs: [ 0xYourDVNContractAddress, // Your Gasolina DVN 0xOtherDVNAddress // Another DVN ], optionalDVNs: [] }) ``` *** ## Advanced Configuration ### Extra Context Verification Extra-context verification is optional and owned by the Gasolina operator, who runs the verification endpoint and decides what it accepts or rejects. It runs in addition to Gasolina's mandatory chain-derived checks, never instead of them — it can only make signing stricter, never looser. Enable it deliberately, and only with a policy whose intent the asset issuer / OApp has agreed to; a generic DVN should not impose app-specific signing decisions on its own (see [Additional recommendations for OApp-owned DVNs](#additional-recommendations-for-oapp-owned-dvns)). Add custom verification logic by implementing an API endpoint: ```typescript wrap theme={null} // Your custom verification API app.post('/verify', async (req, res) => { const {sentEvent, from} = req.body; // Implement your verification rules const isValid = await verifyCustomRules(sentEvent, from); res.json(isValid); }); ``` **API Input Schema:** ```typescript wrap theme={null} { sentEvent: { lzMessageId: { pathwayId: { srcEid: number, // Source endpoint ID dstEid: number, // Destination endpoint ID sender: string, // Sender OApp address receiver: string, // Receiver OApp address }, nonce: number, }, guid: string, message: string, options: { /* ... */ }, onChainEvent: { chainName: string, txHash: string, blockNumber: number, } }, from: string // Transaction initiator } ``` The endpoint must return a bare JSON boolean: `true` allows Gasolina to sign, and `false` refuses signing. Configure the URL in your infrastructure config: ```typescript wrap theme={null} extraContextGasolinaUrl: 'https://your-api.com/verify'; ``` Returning `false` refuses to sign. If Gasolina is configured as a required DVN for the pathway, the message cannot reach `VERIFIABLE` because the destination ULN waits for every required DVN stamp. It will not be committed or delivered until the message is signed, the OApp changes its ULN config, or the nonce is skipped or unblocked through the appropriate Endpoint action, such as skip, nilify, or burn, depending on state. A refused nonce keeps later messages on the same pathway from becoming executable, because delivery depends on preceding nonces being verified or skipped. Before enabling extra-context checks, define asset-issuer and ecosystem impact, who can change or disable the policy, and how refused messages are unblocked. Log and alert on policy-check failures. ### Multi-Signer Setup For enhanced security and availability, deploy multiple Gasolina instances with different signers: | Instance | Endpoint | Signers | | ---------- | ------------------------------- | ------------ | | Gasolina 1 | `https://gasolina1.example.com` | Signers A, B | | Gasolina 2 | `https://gasolina2.example.com` | Signers C, D | | Gasolina 3 | `https://gasolina3.example.com` | Signers E, F | Essence requests signatures from all instances in parallel, then combines them for submission. Configure the signer threshold to require signatures from multiple instances (e.g., 4 of 6 signers across 3 instances). *** ## Managing Configuration Changes Operator control over DVN configuration is split into two steps: producing a quorum-signed change payload, then submitting that payload on-chain. The scripts below generate the `ExecuteParam`/calldata and signer quorum signatures for signer-threshold and signer-set changes. They do not execute the change, submit a transaction, or pay gas. After generation, the payload must be submitted before its expiration. For normal signer and threshold changes, submission goes through the DVN `execute(ExecuteParam[])` path and must be sent by an account with the DVN admin role; today operators typically send the generated payload to LayerZero Labs for submission through Essence. The separate `quorumChangeAdmin` recovery path is different: any account can submit a valid quorum-signed admin-change payload, because the contract verifies the signatures rather than trusting the sender. The signer quorum retains ultimate control: via `quorumChangeAdmin`, it can reassign the admin role without the current admin's cooperation. See [Emergency Admin Takeover](#emergency-admin-takeover). ### Change Signer Threshold This procedure changes the on-chain DVN signer threshold, not the RPC provider quorum in `providers.json`. Never set the production signer threshold below 2. Lowering the threshold below 2 should happen only as a documented emergency risk exception, and all threshold changes should go through reviewed governance or operational procedures. ```bash wrap theme={null} ts-node scripts/configChangePayloads/createSetQuorumSignatures.ts \ -e mainnet \ -c ethereum,bsc,avalanche \ --oldQuorum 2 \ --newQuorum 3 ``` ### Add a Signer ```bash wrap theme={null} ts-node scripts/configChangePayloads/createAddOrRemoveSignerSignatures.ts \ -e mainnet \ -c ethereum,bsc,avalanche \ -q 2 \ --signerAddress 0xNewSignerAddress \ --shouldRevoke 0 ``` ### Remove a Signer ```bash wrap theme={null} ts-node scripts/configChangePayloads/createAddOrRemoveSignerSignatures.ts \ -e mainnet \ -c ethereum,bsc,avalanche \ -q 2 \ --signerAddress 0xOldSignerAddress \ --shouldRevoke 1 ``` ### Emergency Admin Takeover The DVN contract includes a `quorumChangeAdmin` function that allows the signer quorum to reassign the admin role **without requiring the current admin's permission**. This ensures signers maintain ultimate control over the DVN even if the admin role has been delegated to a service like Essence. #### ExecuteParam Structure The function accepts a single `ExecuteParam` struct: ```solidity wrap theme={null} struct ExecuteParam { uint32 vid; // DVN instance identifier (endpoint v1 eid or v2 eid % 30000) address target; // DVN contract address (must equal address(this)) bytes callData; // abi.encode(newAdminAddress) uint256 expiration; // Unix timestamp (must be in the future) bytes signatures; // Concatenated quorum signatures, sorted by signer address } ``` #### Step-by-Step Implementation **Step 1: Prepare the Parameters** ```javascript wrap theme={null} const ethers = require('ethers'); // Configuration const dvnAddress = '0xYourDVNContractAddress'; const newAdminAddress = '0xYourControlledAddress'; const vid = 101; // Your DVN's vid (check contract or use endpoint v1 eid) const expiration = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60; // 1 week from now // Encode the new admin address as callData const callData = ethers.utils.defaultAbiCoder.encode(['address'], [newAdminAddress]); ``` **Step 2: Generate the Hash for Signing** ```javascript wrap theme={null} // Hash follows the DVN contract's hashCallData format const hash = ethers.utils.keccak256( ethers.utils.solidityPack( ['uint32', 'address', 'uint256', 'bytes'], [vid, dvnAddress, expiration, callData], ), ); ``` **Step 3: Collect Quorum Signatures** Each signer must sign the hash. Signatures must be sorted by signer address (ascending) before concatenation: ```javascript wrap theme={null} // Each signer signs the hash const signers = [signer1, signer2, signer3]; // Your Wallet instances const signatures = []; for (const signer of signers) { const sig = await signer.signMessage(ethers.utils.arrayify(hash)); signatures.push({ address: await signer.getAddress(), signature: sig, }); } // Sort by address (required by contract) signatures.sort((a, b) => a.address.toLowerCase().localeCompare(b.address.toLowerCase())); // Concatenate signatures const concatenatedSignatures = ethers.utils.solidityPack( signatures.map(() => 'bytes'), signatures.map((s) => s.signature), ); ``` **Step 4: Submit the Transaction** ```javascript wrap theme={null} const dvnAbi = [ 'function quorumChangeAdmin((uint32 vid, address target, bytes callData, uint256 expiration, bytes signatures) _param) external', ]; const dvnContract = new ethers.Contract(dvnAddress, dvnAbi, provider); // Anyone can submit this transaction - no special permissions required const tx = await dvnContract.quorumChangeAdmin({ vid: vid, target: dvnAddress, callData: callData, expiration: expiration, signatures: concatenatedSignatures, }); await tx.wait(); console.log('Admin role transferred to:', newAdminAddress); ``` #### Validation Requirements The contract validates: | Check | Requirement | | ---------- | -------------------------------------------- | | Expiration | `expiration > block.timestamp` | | Target | `target == address(this)` (the DVN contract) | | VID | `vid == contract.vid` | | Signatures | Must meet quorum, sorted by signer address | | Replay | Hash must not have been used before | Before Taking Over Admin: * **Gas infrastructure**: Ensure you have gas management ready on all chains where you'll submit transactions * **Signer-threshold coordination**: Collect signatures from enough signers to meet the signer threshold * **Transaction systems**: Have reliable transaction submission infrastructure prepared * **Test first**: Test the process on testnet before executing on mainnet The `quorumChangeAdmin` function is defined in [DVN.sol](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/DVN.sol) lines 137-160. *** ## Operational Management ### Monitoring * **CloudWatch Logs**: Automatic log collection * **CloudWatch Alarms**: Set up error rate monitoring * **SNS Notifications**: Configure alerts ```bash wrap theme={null} # Tail logs aws logs tail /ecs/gasolina-api --follow ``` * **Cloud Logging**: Automatic log collection * **Alerting Policies**: Configure error notifications * **Notification Channels**: Set up email/Slack alerts ```bash wrap theme={null} # View recent logs gcloud logging read "resource.type=cloud_run_revision" --limit 50 ``` ### Scaling | Platform | Scaling Method | | -------- | ---------------------------------------------- | | AWS | ECS auto-scales based on CPU/memory thresholds | | GCP | Cloud Run auto-scales based on request volume | ### Key Rotation **KMS Keys (Recommended)** * AWS KMS supports automatic key rotation * Update DVN contract if key ID changes **Mnemonic Keys (development/testing only)** Production signers use KMS/HSM and must not be operated as mnemonics; migrate any mnemonic signer to KMS before production use. 1. Generate new mnemonic 2. Update Secrets Manager 3. Update wallet configuration 4. Redeploy application 5. Update DVN contract with new signer addresses *** ## Troubleshooting ### Common Issues | Issue | Solution | | ---------------------------------------- | ------------------------------------------------------------------ | | "API has not been used in project" (GCP) | Wait a few minutes for API enablement to propagate | | "Resource already exists" (AWS) | Delete `GasolinaMetricLogGroup` and S3 bucket, retry | | RPC connection failures | Verify endpoints, check API keys, add independent quorum providers | | Signature verification failures | Ensure signer addresses match DVN contract config | ### Debug Commands ```bash wrap theme={null} # AWS: Check ECS logs aws logs tail /ecs/gasolina-api --follow # GCP: Check Cloud Run logs gcloud logging read "resource.type=cloud_run_revision" --limit 50 # Test signing manually (issue through your authenticated gateway, e.g. SigV4-signed # or mTLS; the signing route must not be reachable by unauthenticated callers) curl -X POST https://your-instance.com \ -H "Content-Type: application/json" \ -d @test-payload.json ``` *** ## Production hardening checklist Gasolina holds DVN signing authority, so its job is simple: produce a signature only after the operator has independently verified that the LayerZero message is real, the source transaction has enough confirmations for the pathway's finality and reorg-risk requirements, the request is inside its validity window, and the signed payload is derived from trustworthy chain data. A hardened DVN should satisfy the controls in each checklist below before it signs traffic. The short version: diversify your clients, use non-extractable keys, require at least 2 independent signatures, isolate signers, authenticate callers, use independent RPC quorum, validate caller-supplied security context, instrument audit logs, alert on anomalies, and keep an incident runbook. The items below describe the production hardening posture every Gasolina-derived DVN deployment should meet. The reference deployment templates in [gasolina-aws](https://github.com/LayerZero-Labs/gasolina-aws) and [gasolina-gcp](https://github.com/LayerZero-Labs/gasolina-gcp) may not enable all of these by default; treat this checklist as the *required* posture for any DVN attesting production messages, regardless of the template defaults. Items marked `` require security-team confirmation of the current default state in the reference templates. ### Signers and threshold Gasolina can return signatures from the signer keys configured for the destination chain. The on-chain DVN signer threshold determines how many signatures are required before verification succeeds. * **Multiple independent signer identities.** Configure more than one signer identity, and each signer must use a distinct key. * **Threshold `>= 2` in production.** Require at least 2 signatures for production. Do not lower the production threshold below 2 except as a documented emergency risk exception. A threshold of 2 or more reduces the impact of any single signer key compromise. * **Signer isolation.** Run signers in separate compute or trust domains rather than hosting multiple signer containers on a single machine. * **Signer inventory.** Maintain an inventory of the signer addresses and public keys the operator owns and expects `GET /signer-info` to return for each production pathway. * **Reviewed signer-key changes.** Keep Gasolina signer-key changes behind reviewed deployment and key-management procedures. * **Reviewed threshold changes.** Keep on-chain DVN signer threshold changes behind reviewed governance or operational procedures. * **Documented rotation policy.** Keys rotate on a defined cadence and on suspicion of compromise. ### Managed keys vs mnemonics Never use mnemonics. They are extractable secrets: anyone who can read the secret, dump process memory, or capture a deployment artifact can reuse the key outside Gasolina. Prefer managed signing services. The mnemonic-based signer option shown earlier in this guide (Step 3: Choose Signer Type) is intended for local development and testing only. Production deployments must use cloud KMS or HSM-backed keys. * **Use cloud KMS or HSM-backed keys** for production signers. No long-lived keys in plaintext on disk. * **Grant routine signing permission only to the expected Gasolina runtime identity.** * **Define any human, CI, or admin signing access as time-bound break-glass access** with documented approval, immutable logs, and appropriate alerts. * **Keep KMS/HSM policy changes behind reviewed change control.** Gasolina (TypeScript) supports AWS KMS and GCP Cloud KMS for HSM-backed signing. The KMS signer is selected through environment variables on the running service: * `SIGNER_TYPE=KMS` * `KMS_CLOUD_TYPE=AWS` or `KMS_CLOUD_TYPE=GCP` * `LAYERZERO_KMS_IDS`: comma-separated list of KMS key IDs, one per signer key * For GCP Cloud KMS: `GCP_PROJECT_ID` and `GCP_KEY_RING_ID` In the AWS CDK deployment these are derived from the `signerType: 'KMS'` and `kmsNumOfSigners` settings (see Step 3: Choose Signer Type above); the GCP Terraform deployment sets them on the Cloud Run service. Operators using Azure Key Vault, another HSM, or a custom signer should provide an equivalent non-extractable signer adapter and apply the same permission, monitoring, and alerting requirements. ### Access control and audit trail Gasolina should be reachable only through an authenticated control plane. It does not sign arbitrary payloads: it reconstructs the message from chain data and runs validation before signing. The risk of public exposure is that attackers can directly exercise the signing workflow, abuse expensive RPC-backed validation paths, probe validation edge cases, and turn any caller-context or validation bug into a signing or availability incident. * **Authenticated gateway.** Put Gasolina behind IAM/SigV4, mTLS, VPN, private networking, or an equivalent authenticated gateway. No anonymous endpoints in production. `` * **No public exposure.** Do not expose the Gasolina task, container, or internal load balancer directly to the public internet. The signer process must sit in a private subnet, reachable only via the gateway or load balancer. `` * **Restrict signing-route access** to the expected caller role or service, not a broad account or network range. `` * **Rate-limit signing and status routes** (such as `GET /signer-info` and `GET /provider-health`) per caller at the gateway or equivalent network control where supported. Attach payload-size limits and common-injection rule sets where a WAF is available. `` * **TLS 1.2+ enforced** end-to-end, including internal hops between the gateway and worker. * **No introspection or health endpoints** that disclose the configured RPC providers, signer counts, or service identifiers to unauthenticated callers. `` * **Instrument audit logs.** Add application, gateway, or sidecar instrumentation to log caller identity, source transaction hash, LayerZero message ID, source/destination chain, `dvnAddress`, signer addresses, outcome, and rejection reason. * **Retain audit logs** long enough to investigate delayed reports of fraudulent or anomalous signatures. ### Client and deployment diversity Client diversity reduces common-mode risk from implementation bugs, compromised dependencies, or compromised deployment infrastructure. * **Run more than one independent Gasolina deployment** when possible. * **Make deployments independent.** Make deployments independent across release pipelines, deploy credentials, runtime credentials, hosts, and registries or image digests. * **Add independent implementations** when available, such as Rust or partner-written clients. * **Expose resolved payloads, signer addresses, and signature results** from each deployment so downstream policy can compare them for high-value pathways. * **Pin production images by immutable digest** where possible and require reviewed deploys. Supply-chain and binary integrity controls support deployment diversity. Where supported, pin package manifests to specific versions or hashes and fail CI on drift, use reproducible builds with a publicly documented build pipeline, publish release artifacts with cryptographic signatures from the build pipeline, verify a SHA256 attestation before each signer binary runs, and use file-integrity monitoring on the running node to detect post-deployment binary swaps. ### RPC quorum and provider configuration A compromised or faulty RPC provider can lie about receipts, blocks, timestamps, contract state, or transaction contents. Quorum only helps if the providers and quorum strategy are independent and protected from tampering. This RPC provider quorum (how many independent providers must agree on source-chain data) is a separate control from the on-chain signer threshold in [Signers and threshold](#signers-and-threshold): the quorum governs the data Gasolina reads, while the threshold governs how many signatures the DVN requires. Security-critical data includes: * source transaction receipts and emitted packet events, * message hash inputs reconstructed from source-chain state, * source block confirmations, * destination block timestamps used for expiration checks, * destination ULN/DVN config and verification state, * read-message time markers and resolved read payload inputs. * **Use RPC quorum, not a single provider,** for security-critical reads. * **Require matching responses from at least 3 or 4 independent provider entities** for every chain in production. Multiple URLs from one vendor count as one fault domain. A single compromised or faulty RPC can otherwise feed forged source-chain data and the DVN will sign attestations of events that did not occur, so a single-provider (`quorum: 1`) configuration is never acceptable. `` * **Treat provider entities as separate fault domains,** for example operator-run nodes, dedicated external providers, and shared third-party providers. * **Treat all configured RPCs as primary,** not as failover. Backup-only RPCs do not contribute to consensus and reduce the effective quorum. * **Prefer including an operator-run node in quorum,** especially for high-value pathways. * **Fail closed when quorum (at least 3 independent providers) cannot be reached** or providers disagree on security-critical data. * **Use reorg-aware confirmation depth.** Your DVN's `confirmations` value must exceed the source chain's typical reorg depth by a margin appropriate to the attestation value at stake. * **Export RPC health, block lag, latency, disagreement rate, and quorum-failure metrics.** * **Document RPC selection rationale.** Record why each RPC was chosen, when it was last reviewed, and the conditions under which it would be replaced. * **Restrict writes to provider lists and quorum strategy files** to the deploy pipeline or another reviewed change path. Configuration changes should require a multi-party process so no single operator can rotate provider endpoints unilaterally. * **Enable versioning or audit trails for provider configuration.** Where supported, store configuration on immutable object storage (for example, Object Lock) and keep versioned access logs on every read and write. `` * **Do not put RPC API keys or credentials in logs or public config.** ### Required checks before every signature Gasolina reconstructs the source message from chain state and should not trust caller-provided data by itself. If caller-provided identifiers, hashes, or security context do not match the chain-derived data and configured pathway requirements, Gasolina should reject the request instead of signing a payload built from different verified data. The checks below are what every Gasolina client is expected to perform before signing. Operators should verify that each deployed client performs these checks and that operator-controlled settings, such as supported chains, RPC/quorum configuration, and enabled extra policy checks, are configured as intended. Every signature must pass all of the following checks: * **Supported chains:** source and destination chains are supported. * **Valid protocol type:** protocol type is valid for the request (`MESSAGE` or `READ`) and matches the requested ULN/version. * **Source transaction exists** on the source chain. * **Packet event matches:** packet event exists and matches the requested message ID, pathway, nonce, and version. * **Message hash matches** packet data reconstructed from source-chain state. * **Block confirmations (message verification):** for message verification, the source transaction has at least the caller-supplied `blockConfirmation`. * **Time markers (Read verification):** for Read verification, resolved time markers are valid and their referenced blocks have the required confirmations. * **Expiration window valid:** request expiration is still valid and is not more than the configured maximum window in the future. * **Destination config read via RPC stack:** destination ULN/DVN config and current verification state are read through the configured RPC stack. Production deployments should configure that stack for quorum as described above. * **Payload built only from verified state:** final hash call data and resolved payload are built only from verified source-chain and destination-chain state. Only after all required client checks and enabled extra policy checks pass should signer keys produce signatures. * **Client consistency.** Verify every Gasolina client, deployment, or implementation whose signatures may be accepted implements these checks and uses the intended supported-chain, RPC/quorum, and extra-policy configuration. ### Additional recommendations for OApp-owned DVNs OApp-owned or app-specific DVNs may have enough application context to apply business, risk, or compliance checks beyond the base protocol checks. Generic DVNs are usually blind to application semantics and should not make app-specific signing decisions unless the OApp owner has explicitly provided the required payload context, policy, and authority. Before an OApp-owned or app-specific DVN enables these checks, define: * **Implementation:** where the check runs, for example a Gasolina extra-context endpoint, sidecar, upstream verifier/orchestrator, or independent client-comparison service. * **OApp-owner policy:** which apps, pathways, payloads, or message types the DVN is expected to sign or refuse. * **Asset-issuer impact:** whether refusing to sign can affect minting, burning, unlocking, withdrawals, or other asset movement. * **Ecosystem impact:** whether refusals can affect pathway liveness, ordered nonce progress, composability, or downstream user experience. * **Business and ecosystem alignment:** whether the policy changes the relationship between the DVN, the OApp owner, asset issuers, or other ecosystem participants. * **Operational process:** who can change or disable the policy, how emergency exceptions work, how refused messages can resume or be unblocked, and how refusals are communicated. Generic DVNs should treat the examples below as application-owner responsibilities unless they have a specific integration that gives them reliable decoding, expected payload semantics, and an agreed policy. Examples for OApp-owned or app-specific DVNs include: * **Inflow/outflow controls** for applications where the OApp owner understands the message format and asset semantics well enough to make the check reliable. * **Emergency controls** that allow the operator to intentionally fail closed for a compromised pathway, chain, RPC provider, or app. Consider allowlists for approved pathways, apps, message types, token routes, or specific payloads, only when this restriction is defined by the OApp owner, and its ecosystem impact is understood. Policy-check failures should be logged and alerted. OApp owners and DVN operators should understand that refusing to sign a nonce can block later ordered messages on the same pathway until the OApp or protocol operators take corrective action. ### Monitoring and alerts Gasolina clients do not emit structured success-path signing audit logs for all of the fields below. Operators should add application, gateway, or sidecar instrumentation before treating these as covered controls. * Alert on unexpected signer address changes or public-key changes. * Alert on KMS/HSM key-policy, grant, or permission changes on signing keys. * Alert on KMS/HSM signing attempts from unexpected principals or infrastructure. * Alert on abnormal signing volume by caller, pathway, destination chain, or signer key. * Track per-pathway attestation latency, alerting on attestations that are unusually fast (possible pre-positioning), unusually slow (possible degradation), or in non-canonical block ranges. * Alert on duplicate-sign attempts and repeated requests missing `dvnAddress`. * Alert on RPC quorum failures, provider disagreement, provider lag, and provider-config changes. * Alert when extra policy checks reject unusually often or stop responding. * Alert when health-check or status routes (such as `GET /signer-info` and `GET /provider-health`) are called at unusual volume. * Alert on authentication-failure and authorization-failure rates. Probe activity is an attack precursor. * Run continuous source-chain to destination-chain reconciliation, alerting on attestations that do not correspond to source-chain emit events. * Ensure alerts route to a staffed on-call channel or paging system. A monitoring dashboard nobody is paged on is not a control. * Keep a runbook for key compromise, signer rotation, bad RPC/provider data, provider-config tampering, deploy rollback, and emergency pathway disablement. Include a kill-switch to stop signing within minutes, tested in a non-production environment at least quarterly; an isolation playbook to remove a compromised RPC, signer, or node without taking the whole DVN offline; and a current communication path to integrators using your DVN. *** ## Repository Links | Resource | Link | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | AWS Infrastructure | [gasolina-aws](https://github.com/LayerZero-Labs/gasolina-aws) | | GCP Infrastructure | [gasolina-gcp](https://github.com/LayerZero-Labs/gasolina-gcp) | | DVN Contract | [DVN.sol](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/DVN.sol) | *** ## Next Steps * [Gasolina Overview](/v2/workers/off-chain/gasolina-overview) - Architecture and security model * [DVN Technical Reference](/v2/workers/off-chain/dvn-technical-reference) - Contract methods and events * [DVN Overview](/v2/workers/off-chain/dvn-overview) - General DVN concepts For implementation support, reach out through [LayerZero Discord](https://discord.com/invite/ktbvm8Nkcr) or open an issue in the respective GitHub repository. # Gasolina Overview Source: https://docs.layerzero.network/v2/workers/off-chain/gasolina-overview Learn about Gasolina, a DVN implementation that separates security functions from operational complexities, allowing DVN operators to focus on verification. Gasolina is a DVN implementation approach that separates the security function (verification and signing) from operational complexities (gas management and transaction submission). This allows DVN operators to focus purely on security while LayerZero's infrastructure handles transaction delivery. ## The Challenge Traditional DVN operation requires significant infrastructure: * Maintaining funded wallets on every supported chain * Acquiring and managing native tokens across diverse blockchains * Building and maintaining reliable transaction submission systems * 24/7 monitoring of gas balances, transaction failures, and chain-specific issues For organizations focused on providing verification services, this operational burden can be a significant barrier to entry. ## The Gasolina Solution Gasolina introduces a separation of concerns: | Responsibility | Traditional DVN | Gasolina DVN | | ---------------------- | --------------- | ------------------- | | Message verification | DVN Operator | DVN Operator | | Signature generation | DVN Operator | DVN Operator | | Gas management | DVN Operator | LayerZero (Essence) | | Transaction submission | DVN Operator | LayerZero (Essence) | | Signer key security | DVN Operator | DVN Operator | | Contract control | DVN Operator | DVN Operator | With Gasolina, operators maintain full control over the security-critical aspects while delegating gas logistics to LayerZero's Essence service. *** ## Architecture The Gasolina system consists of three components working together: ### 1. DVN Contract (Onchain) The smart contract deployed on each supported blockchain: * Maintains the list of authorized signer addresses * Enforces multisig quorum requirements * Validates signatures before accepting verifications * Grants ultimate control to the signer quorum **Reference source code:** [DVN.sol](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/DVN.sol) ### 2. Gasolina Service (Offchain) A lightweight REST API that operators deploy: * Connects to RPC providers for each supported chain * Verifies source chain events independently * Waits for required block confirmations * Signs verification payloads with secure keys * Returns signatures to requesting parties **Infrastructure as Code:** * AWS: [gasolina-aws](https://github.com/LayerZero-Labs/gasolina-aws) * Google Cloud: [gasolina-gcp](https://github.com/LayerZero-Labs/gasolina-gcp) ### 3. Essence Service (Gas Abstraction) LayerZero's infrastructure that: * Monitors for cross-chain messages requiring verification * Requests signatures from Gasolina nodes * Aggregates signatures to meet quorum requirements * Submits verified transactions onchain * Handles all gas payments across chains *** ## Message Flow When a message is sent through LayerZero with a Gasolina DVN configured: ```mermaid wrap theme={null} sequenceDiagram participant OApp as Source OApp participant Essence participant Gasolina participant DVN as DVN Contract participant Dest as Destination OApp->>OApp: Emit PacketSent Note over Essence: Detect message Essence->>Essence: Wait for confirmations Essence->>Gasolina: Request signatures Gasolina->>Gasolina: Verify event via RPC Gasolina->>Gasolina: Confirm block finality Gasolina->>Gasolina: Sign payload Gasolina->>Essence: Return signatures Essence->>DVN: Submit verification tx DVN->>DVN: Validate signatures DVN->>DVN: Check quorum met DVN->>Dest: Mark as verified ``` **Key steps:** 1. OApp emits `PacketSent` event on source chain 2. Essence monitors for messages requiring Gasolina DVN verification 3. Essence waits for required block confirmations 4. Essence calls Gasolina API with message details 5. Gasolina verifies the event via its own RPC providers 6. Gasolina signs the payload after confirming finality 7. Essence submits signatures to DVN contract on destination 8. DVN contract validates signatures against registered signers *** ## Signer Sovereignty A key design principle of Gasolina is that **signers maintain ultimate control** over the DVN. This is enforced through the contract architecture: | Entity | Control Level | Capabilities | | ----------------- | ------------- | -------------------------------------------- | | **Signer Quorum** | Ultimate | Change admins, modify signers, adjust quorum | | **DVN Contract** | Enforcement | Validates signatures, executes instructions | | **Gasolina** | Verification | Produces signatures (requires valid keys) | | **Essence** | Operational | Submits transactions (delegated privilege) | ### Key Guarantees * **Signer set is sovereign**: The signer quorum has complete control over the DVN, including the ability to change admins, modify the signer set, and adjust quorum thresholds. * **Admin role is delegated authority**: While Essence typically holds the admin role for gas-efficient operations, this is a revocable privilege. Signers can use `quorumChangeAdmin` to reassign this role at any time. * **No external dependencies for control**: The DVN cannot be held hostage by any party, including LayerZero. If Essence becomes unavailable, signers can immediately take over transaction submission. * **Quorum prevents single-key compromise**: An attacker would need to compromise more than the configured threshold of signer keys to control the DVN. ### Emergency Takeover If signers need to take direct control: 1. Generate a signed `quorumChangeAdmin` instruction 2. Submit directly to the DVN contract (no Essence required) 3. Assign admin role to an operator-controlled address 4. Resume operations with full control *** ## Security Model The Gasolina architecture ensures security through separation of concerns: ### Gasolina Operator Responsibilities | Area | Controls | Does NOT Control | | ------------------- | ------------------------------------------------- | -------------------------- | | Signer keys | Private keys that produce verification signatures | | | RPC providers | Which providers verify source chain events | | | Confirmations | How many blocks to wait before signing | | | Verification logic | Optional additional verification rules | | | Gas costs | | Handled by Essence | | Transaction timing | | Determined by Essence | | Contract deployment | | Coordinated with LayerZero | ### Essence Responsibilities | Area | Controls | Does NOT Control | | ---------------------- | ------------------------------------ | ------------------------- | | Transaction submission | When and how to submit verifications | | | Gas optimization | Efficient batching and gas pricing | | | Retry logic | Handling failed transactions | | | Signature generation | | Cannot forge signatures | | Signer modifications | | Cannot add/remove signers | | Quorum changes | | Cannot adjust thresholds | | Admin role | | Delegated and revocable | *** ## Benefits ### For Security Providers | Benefit | Description | | -------------------------- | ------------------------------------------- | | **Simplified operations** | No need to manage gas wallets across chains | | **Lower barrier to entry** | Start with just RPC access and signing keys | | **Enhanced key security** | Signing keys never need to hold value | | **Predictable costs** | No variable gas expenses to manage | | **Focus on security** | Concentrate on verification, not operations | ### For OApp Developers | Benefit | Description | | ------------------------- | --------------------------------------------------- | | **More DVN options** | Lower barriers mean more security providers | | **Reliable verification** | Professional gas management ensures timely delivery | | **Diverse security** | Combine multiple Gasolina DVNs easily | *** ## When to Choose Gasolina ### Ideal Use Cases * Professional security providers entering the DVN space * Organizations without existing multi-chain infrastructure * Teams wanting to focus on verification quality over operations * Multi-signature security models requiring distributed trust * Rapid deployment and proof-of-concept scenarios ### Consider Traditional DVN When * You need complete control over every operational aspect * Custom gas optimization strategies are required * Regulatory requirements mandate full infrastructure control *** ## Getting Started ### Prerequisites 1. **Cloud provider account**: AWS or Google Cloud 2. **RPC providers**: Reliable endpoints for each chain you'll support (2+ per chain recommended) 3. **Development tools**: Node.js, Git, cloud CLI tools ### Quick Setup Overview 1. Clone the infrastructure repository ([AWS](https://github.com/LayerZero-Labs/gasolina-aws) or [GCP](https://github.com/LayerZero-Labs/gasolina-gcp)) 2. Configure your signer type (mnemonic or HSM-backed) 3. Add RPC providers for supported chains 4. Deploy using CDK (AWS) or Terraform (GCP) 5. Share your Gasolina URL with LayerZero for contract deployment 6. Receive DVN contract addresses for OApp configuration For detailed instructions, see the [Implementation Guide](/v2/workers/off-chain/gasolina-implementation). *** ## Repository Links | Resource | Description | Link | | ---------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | **gasolina-aws** | AWS CDK infrastructure | [GitHub](https://github.com/LayerZero-Labs/gasolina-aws) | | **gasolina-gcp** | GCP Terraform infrastructure | [GitHub](https://github.com/LayerZero-Labs/gasolina-gcp) | | **DVN Contract** | Reference implementation | [GitHub](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/DVN.sol) | *** ## Next Steps * [Implementation Guide](/v2/workers/off-chain/gasolina-implementation) - Step-by-step deployment instructions * [DVN Technical Reference](/v2/workers/off-chain/dvn-technical-reference) - Contract methods, events, and errors * [DVN Overview](/v2/workers/off-chain/dvn-overview) - General DVN concepts * [Security Stack DVNs](/v2/concepts/modular-security/security-stack-dvns) - How OApps configure DVNs The [Implementation Guide](/v2/workers/off-chain/gasolina-implementation) walks you through deploying Gasolina on AWS or Google Cloud Platform. # Workers in LayerZero V2 Source: https://docs.layerzero.network/v2/workers/overview Learn about Workers in LayerZero V2, including Decentralized Verifier Networks (DVNs) and Executors that power cross-chain messaging and execution. In the LayerZero V2 protocol, **Workers** serve as the umbrella term for two key types of service providers: **Decentralized Verifier Networks (DVNs)** and **Executors**. Both play crucial roles in facilitating crosschain messaging and execution by providing verification and execution services. By abstracting these roles under the common interface known as a `worker`, LayerZero ensures a consistent and secure method to interact with both service types. ## What Are Workers? **Workers** are specialized entities that interact with the protocol to perform essential functions: * **Verification as a Service:** Decentralized Verifier Networks (DVNs) verify the authenticity and correctness of messages across chains. * **Execution as a Service:** Executors carry out transactions on behalf of applications once verification is complete. These roles are unified under the Worker interface, meaning that whether a service provider is a DVN or an Executor, it interacts with the protocol using a standardized set of methods. *** ## For DVN Operators If you're looking to operate a DVN, these resources will help you understand the architecture and implementation options. Understand how DVNs work and your options for operating one. Run a DVN with simplified gas management via Gasolina. *** ## Build Custom Workers For developers building their own DVN or Executor from scratch, these guides cover the technical implementation details. Technical guide for implementing a custom DVN from scratch. Technical guide for implementing a custom Executor. *** ## Resources ### Technical Reference * [DVN Technical Reference](/v2/workers/off-chain/dvn-technical-reference) - Contract methods, events, and error signatures * [Gasolina API Reference](/v2/workers/off-chain/gasolina-api-reference) - REST API documentation for Gasolina ### Guides * [Gasolina Implementation Guide](/v2/workers/off-chain/gasolina-implementation) - Deploy Gasolina on AWS or GCP * [Troubleshooting](/v2/workers/off-chain/dvn-troubleshooting) - Diagnose and fix common DVN issues ### External Links * [gasolina-aws](https://github.com/LayerZero-Labs/gasolina-aws) - AWS CDK infrastructure * [gasolina-gcp](https://github.com/LayerZero-Labs/gasolina-gcp) - GCP Terraform infrastructure * [DVN Contract Source](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/DVN.sol) - Reference implementation *** This architecture allows LayerZero V2 to provide robust, decentralized crosschain communication while giving application developers the tools needed to fine-tune their security and operational parameters.