--- slug: / sidebar_label: Welcome title: LayerZero Documentation --- **LayerZero** is an **omnichain interoperability protocol** that enables smart contracts to read from and write state to different blockchains. Developers can build omnichain applications (OApps) that can send state transitions, value transfers, and call smart contracts on other networks as if they were on a single blockchain. These operations can be defined simply as **messages** between blockchains, often referred to as **generic message passing (GMP)**, which bridges the state information available from blockchain to blockchain. LayerZero's design ensures that the **core protocol contracts** are **immutable** and **non-upgradeable**, ensuring your application continues to operate as expected indefinitely, while **your contracts** stay easily **configurable** and **flexible** to define each part of the protocol's message passing rails. This enables you to optimize the security, cost, and execution of messages according to your specific needs. ![LayerZero OApp](/img/layerzero-oapp.png) ## Why LayerZero? Before LayerZero, users of blockchains were isolated per network, limiting the ability for decentralized applications (dApps) to share information, value, and composability. Message and value based bridges evolved from the necessity to reduce friction when moving state information between chains. Many early cross-chain solutions, however, relied on centralized services, a collection of fixed validators, or both to witness state transitions on a source blockchain before writing the corresponding updates on the destination network. When hundreds of dApps rely on the same small validator set, one compromise puts every contract at risk. Attacks on fixed validator set bridges have been responsible for over $2 billion dollars of user funds lost. LayerZero solves these problems by providing a **secure, efficient, and user-friendly** smart contract framework for building omnichain applications. With LayerZero, you have access to: - **Immutable protocol / configurable edge**: LayerZero’s transport layer is immutable, audited, and battle-tested; securing over $50 billion in transfer volume. Your own smart contracts stay configurable on top of this framework so you can ship features without directly modifying the transport layer. - **Dial-in security per message**: pick which decentralized verifier networks secure each message between blockchains. You control the trust assumptions, not someone else. - **Expand instantly**: ship to over 120+ different EVM, Solana, Move, TON compatible blockchains, with the same contract business logic. - **Unify liquidity**: Omnichain Fungible Token (OFT) & Omnichain NFT (ONFT) standards: have one canonical supply, transport wherever it’s needed. No wrapped “IOU” tokens, so TVL stays in one global mesh. - **Simplify development**: One framework, one mental model, every chain: `lzSend(_destination, _message, _options)` and `lzReceive(_origin, _guid, _message)` using Solidity, Vyper, Rust, Move, or funC. - **Invisible UX**: users sign one tx; your contract handles cross-chain gas, swapping, and final settlement under the hood. OApps have moved > $50 billion without forcing users to understand the complexities of "the bridge.” Ready to try sending your first message? Jump to the [Quick Start](#quick-start-guide) below and send your first cross-chain message in under 2 minutes. ### Use Cases LayerZero unlocks a wide range of possibilities for decentralized applications, including: - **Omnichain tokens**: Create and manage tokens that are available on multiple chains with a unified supply. - **Cross-chain lending and borrowing**: Lend on one chain and borrow on another, all within a single transaction flow. - **Unified liquidity DEXes**: Aggregate liquidity from multiple chains to provide deeper liquidity and better prices. - **Omnichain NFTs**: Mint, transfer, and manage NFTs across different chains while preserving ownership records. - **Cross-chain governance**: Allow governance protocols to operate across multiple chains for broader participation. - **Cross-chain gaming**: Build games that leverage assets and functionalities from different chains, enriching gameplay experiences. - **Cross-chain data queries**: Securely read real-time state from multiple networks, such as price feeds or contract balances, to trigger onchain operations like rebalancing liquidity pools, adjusting lending parameters or executing conditional logic across chains. ## Quick Start Guide Ready to start building with LayerZero? Here’s your roadmap: ### 1. Choose Your Environment ### 2. Choose a Starting Contract Standard ### 3. Understand Core Concepts & Architecture - [**What is LayerZero?**](../concepts/v2-overview.md): Understand what LayerZero fundamentally solves and why it exists. - [**Protocol Overview**](../concepts/protocol/protocol-overview.md): Understand how messages flow between chains. - [**Security Stack (DVNs)**](../concepts/modular-security/security-stack-dvns.md): Learn about decentralized verifier networks that validate cross-chain messages. - [**Executors**](../concepts/permissionless-execution/executors.md): Discover how messages are delivered and executed on destination chains. ### 4. Dive Deeper - **[Omnichain Application Standard](../concepts/applications/oapp-standard.md)** for generic message passing. - **[Omnichain Tokens](../concepts/applications/oft-standard.md#omnichain-tokens)** for moving fungible and non-fungible asset value transfers between blockchains. - **[Omnichain Composability](../concepts/applications/composer-standard.md)** for how LayerZero connects smart contracts via cross-chain calls. - **[Omnichain Queries](../concepts/applications/read-standard.md)**: Learn how to query external on-chain data from another blockchain for use in another blockchain's smart contract. - **[Security & Executor Config](../concepts/modular-security/security-stack-dvns.md)** to lock in your DVNs and message libraries. - **[Gas Fees & Options](../concepts/message-options.md)** for cross-chain transaction execution settings. ## Key Concepts Below are common terms you’ll encounter in the docs. A more detailed listing can be found in the [Glossary](/v2/concepts/glossary). - **OApp (Omnichain Application)**: A smart contract that uses LayerZero to send and receive messages across different blockchains. - **Endpoint**: Immutable smart contracts deployed on each supported chain that serve as the entry and exit points for LayerZero messages. - **Endpoint ID (EID)**: A unique identifier for each LayerZero Endpoint contract, used for routing messages between chains. - **Decentralized Verifier Networks (DVNs)**: Independent entities that validate messages between chains for security and integrity. - **Executor**: An off-chain service that executes messages on the destination chain after verification. - **Message Library (MessageLib)**: Smart contracts for packing message payloads on the source chain and verifying them on the destination. - **Security Stack**: The combination of DVNs and other parameters that an application configures for message authenticity. - **Composed Message**: A cross-chain message that triggers another cross-chain message (nested calls). ## Developer Tooling LayerZero offers a suite of tools to streamline your development experience: - **[LayerZero Scan](https://layerzeroscan.com)**: Comprehensive block explorer for tracking and debugging cross-chain messages. - **[TestHelper (Foundry)](../developers/evm/tooling/test-helper.md)**: Simulate cross-chain transactions in Foundry tests. - **[Hardhat Toolbox](https://github.com/LayerZero-Labs/devtools/tree/main/packages/toolbox-hardhat)**: A framework for building, configuring, and deploying OApps locally. ## Community and Support Join our community to connect with other developers, ask questions, and share your projects: - [Discord](https://layerzero.network/community) - [Telegram](https://t.me/joinchat/VcqxYkStIDsyN2Rh) - [Twitter](https://x.com/layerzero_core) - [GitHub](https://github.com/LayerZero-Labs/) ## Start Building You are now ready to build the next generation of omnichain applications with LayerZero. Dive into the documentation, explore the examples, and join our community to unleash the full potential of cross-chain development! ### Your Feedback Shapes This Documentation If anything is unclear or missing, we invite you to [open an issue on GitHub](https://github.com/LayerZero-Labs/), ask in [Discord](https://layerzero.network/community), or get in touch with the team. Happy building! --- --- title: What is LayerZero? --- 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 cross-chain interaction, providing a predictable and adaptable foundation for decentralized applications living on multiple networks. ## Before LayerZero ![Attack Vector Dark](/img/learn/attack-vector.svg#gh-dark-mode-only) Before LayerZero, cross-chain communication was a patchwork of monolithic bridges and isolated solutions. Achieving true cross-chain 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 cross-chain 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 cross-chain 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. ## Key Primitives Built into LayerZero LayerZero’s architecture provides a robust set of core primitives that redefine cross-chain interaction. Each primitive has its own dedicated deep-dive section in our documentation to help you fully leverage its capabilities: - **Omnichain Message Passing (Generic Messaging):** 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. _Learn more in our [Omnichain Applications (OApp)](../applications/oapp-standard.md) overview._ - **Omnichain Tokens (OFT & ONFT):** Unified token standards that empower the cross-chain 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. _For additional details, refer to our [Omnichain Tokens (OFT & ONFT)](../applications/oft-standard.md) section._ - **Omnichain State Queries (lzRead):** Go beyond simple messaging—this primitive allows smart contracts to request and retrieve on-chain state from other blockchains securely. It empowers your applications to “pull” data across chains efficiently. _Dive deeper into this capability in our [Omnichain Queries (lzRead)](../applications/read-standard.md) section._ - **Omnichain Composability:** By decoupling security from execution, this design enables developers to build complex, multi-step workflows across chains. It breaks down cross-chain operations into discrete, manageable messages that achieve instant finality, facilitating advanced use cases and improved user experiences. _For detailed insights, refer to our [Omnichain Composability](../applications/composer-standard.md) documentation._ These primitives provide the building blocks for predictable, secure, and scalable cross-chain interactions within the LayerZero mesh network. ## Further Reading To dive deeper into LayerZero and its omnichain capabilities, explore our detailed documentation across three core sections: - [Protocol Overview](../protocol/protocol-overview.md): Understand the technical architecture behind LayerZero—from immutable smart contracts and configurable message libraries to the secure transmission of cross-chain messages. - [Workers Overview](../workers.md): Learn about the off-chain service providers—Decentralized Verifier Networks (DVNs) and Executors—that play a critical role in verifying and executing cross-chain messages. - [Omnichain Applications (OApp) Standard](../applications/oapp-standard.md): Discover how to build applications that leverage LayerZero’s omnichain messaging interface, allowing for generic message passing, dynamic fee estimation, and secure, composable cross-chain interactions. These sections offer comprehensive guides, best practices, and technical references to help you build secure, scalable, and truly omnichain solutions. --- --- title: LayerZero Glossary sidebar_label: Glossary toc_min_heading: 2 toc_max_heading: 5 --- # LayerZero V2 Glossary 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), 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 cross-chain operations into a single transaction. Composition allows for complex cross-chain interactions while maintaining transaction integrity across multiple chains. **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 cross-chain 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 cross-chain 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 cross-chain operations, as applications can handle partial successes and continue execution even if some components fail. For example, a cross-chain 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. ### 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. ### 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. ### 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) to understand what composition is._ A function that enables horizontal composition by allowing a received message to trigger additional cross-chain messages. These composed messages are processed sequentially, creating chains of cross-chain 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 cross-chain 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 cross-chain 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. ### 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 cross-chain 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: ![Vanilla OFT Diagram](/img/learn/oft_mechanism_light.jpg#gh-light-mode-only) ![Vanilla OFT Diagram](/img/learn/oft_mechanism.jpg#gh-dark-mode-only) OFT Adapters will utilize lock and mint: ![OFT Adapter](/img/learn/oft-adapter-light.svg#gh-light-mode-only) ![OFT Adapter](/img/learn/oft-adapter-dark.svg#gh-dark-mode-only) ### OMP (Omnichain Messaging Protocol) The core protocol that enables secure cross-chain 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 cross-chain LayerZero message. This could be token transfer information, function calls, or any other data the application needs to transmit between chains. ### Security Stack The combination of MessageLib, DVNs, and other security parameters that an application configures for its cross-chain messages. Each application can (and should) customize its security stack to balance security, cost, and performance. ### Source Chain The blockchain from which a cross-chain 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 cross-chain 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`. 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. ```solidity /// @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 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 // 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 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. :::tip 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. --- --- title: Protocol Overview --- To send a cross-chain 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 cross-chain 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 ![Protocol V2 Light](/img/learn/protocolv2light.svg#gh-light-mode-only) ![Protocol V2 Dark](/img/learn/protocolv2dark.svg#gh-dark-mode-only) 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.md#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 cross-chain 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/developer-overview.md) - **Solana Technical Overview:** Discover the adaptations made for Solana’s runtime, including cross-chain messaging through the LayerZero Endpoint and integrations with Solana’s unique architecture. [Read the Solana Protocol Overview](../../developers/solana/technical-overview.md) - **Aptos Technical Overview:** Explore how LayerZero leverages the Aptos Move language and framework to implement secure and efficient cross-chain messaging on Aptos-based networks. [Read the Aptos Protocol Overview](../../developers/aptos-move/overview.md) --- --- title: Omnichain Mesh Network --- # Omnichain Mesh Network 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.md) (the contract interface on each chain) to interact directly with any other Endpoint across all supported blockchains. ## What Is the LayerZero Mesh? ![Omnichain Light](/img/learn/omnichain-light.svg#gh-light-mode-only) ![Omnichain Dark](/img/learn/omnichain-dark.svg#gh-dark-mode-only) - **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 cross-chain 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.md#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.md) validate messages according to application–specific requirements. - [Configurable Block Confirmations](../../developers/evm/configuration/dvn-executor-config.md#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.md) 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 cross-chain capabilities—ensuring seamless, secure, and reliable messaging regardless of the underlying blockchain. --- --- sidebar_label: LayerZero Endpoint --- # LayerZero Endpoint 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.md#generic-message-passing) versus [read](../applications/read-standard.md#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.md#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 on-chain, 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. --- --- title: Message, Packet, and Payload --- Because cross-chain 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 // 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 cross-chain communication. The standard Packet structure is defined as follows in the LayerZero Endpoint: ```solidity // 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 // 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 // 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 // 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 cross-chain 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. --- --- title: Message Channel Security --- 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.md#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.md#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. --- --- title: Message Options sidebar_label: 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. 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.md) and [Executors](./permissionless-execution/executors.md)) 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 cross-chain 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. ## 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. :::info 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 **on-chain** at the destination, or use a worker you trust. See the [**Integration Checklist**](../developers/evm/technical-reference/integration-checklist.md#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 cross-chain 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 cross-chain 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. --- --- title: Message Library Overview --- 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 cross-chain 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 cross-chain 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 cross-chain communication. ## Further Reading - For details on how messages are processed on the sending side, see the [Message Send Library](./message-send-library.md) page. - For details on how inbound messages are decoded and verified on the receiving side, see the [Message Receive Library](./message-receive-library.md) page. --- --- title: Message Send Library --- 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 cross-chain 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 cross-chain 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.md). ## 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 cross-chain communication while ensuring high efficiency and security. --- --- title: 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 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. :::info 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.md#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.md). 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 cross-chain communication is both secure and efficient, while also remaining flexible enough to integrate future enhancements. --- --- title: Message Read Library --- 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.md) and [Message Receive Library](./message-receive-library.md), 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 cross-chain 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). --- --- title: Workers Overview sidebar_label: Workers Overview --- # Workers in LayerZero V2 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 cross-chain 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 cross-chain communication while giving application developers the tools needed to fine-tune their security and operational parameters. --- --- sidebar_label: Decentralized Verifier Networks (DVNs) --- # Security Stack (DVNs) As mentioned in previous sections, every application built on top of the LayerZero protocol can configure a unique [messaging channel](../protocol/message-security.md). 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.md#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. ![DVN Light](/img/dvn_light.svg#gh-light-mode-only) ![DVN Dark](/img/dvn_dark.svg#gh-dark-mode-only) 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.md)) can commit the message nonce into the destination [Endpoint’s](../protocol/layerzero-endpoint.md) messaging channel for execution. The following image and table describe how messages can be inserted into the Endpoint's messaging channel post-verification: ![DVN Light](/img/learn/dvn-light.svg#gh-light-mode-only) ![DVN Dark](/img/learn/dvn-dark.svg#gh-dark-mode-only) | 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.md). ### 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`. ![DVN Hook Light](/img/learn/dvnhook-light.svg#gh-light-mode-only) ![DVN Hook Dark](/img/learn/dvnhook-dark.svg#gh-dark-mode-only) Since “DVN” broadly describes any verification mechanism that securely delivers a message’s `payloadHash` to the destination [Message Library](../protocol/message-send-library.md), 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.md#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, LayerZero Labs ] | 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).

:::info What is a **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. ::: :::warning Even if the default configuration presets match the settings you want to use for your application, you should always **set your configuration**, so that it cannot change. The LayerZero default is a placeholder configuration, and subject to change. ::: ## 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.md) - [Solana DVN and Executor Configuration](../../developers/solana/configuration/dvn-executor-config.md) - [Aptos DVN and Executor Configuration](../../developers/aptos-move/configuration/dvn-executor-config.md) --- --- sidebar_label: Executors --- # Executors 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 cross-chain experience. ## What “Execution” Means In the LayerZero protocol, **execution** refers to the invocation of the [LayerZero Endpoint](../protocol/layerzero-endpoint.md) 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.md#lzreceive-option): Specify `gas` and `msg.value` when calling `lzReceive(...)`. - [`lzComposeOption`](../../developers/evm/configuration/options.md#lzcompose-option): Specify `gas` and `msg.value` when calling `lzCompose(...)`. - [`lzNativeDropOption`](../../developers/evm/configuration/options.md#lzairdrop-option): Drop a specified `amount` of native tokens to a `receiver` on the destination. - [`lzOrderedExecutionOption`](../../developers/evm/configuration/options.md#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.md). ## 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.md) 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.md) or a block explorer. :::info See [Executor Configuration](../../developers/evm/configuration/dvn-executor-config.md#custom-configuration) for details on wiring up a non-default Executor in your OApp. ::: --- --- sidebar_label: Omnichain Applications (OApp) title: Omnichain Applications (OApp) --- # Core Concepts for Omnichain Applications LayerZero’s Omnichain Application (OApp) standard defines a **generic cross-chain 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 ![OApp Example](/img/learn/ABLight.svg#gh-light-mode-only) ![OApp Example](/img/learn/ABDark.svg#gh-dark-mode-only) - **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 cross-chain 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.md) handling the transaction on the destination chain (e.g., Decentralized Verifier Networks and Executors). This unified fee model simplifies cross-chain 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 cross-chain 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. :::info The peer’s address is stored in a format (such as `bytes32`) that is interoperable between VMs. ::: - **Endpoint Integration:** All cross-chain messages are sent via a [standardized protocol endpoint](../protocol/layerzero-endpoint.md), 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 cross-chain 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 cross-chain 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.md) - [Solana OApp Reference](../../developers/solana/oapp/overview.md) - [Aptos Move OApp Overview](../../developers/aptos-move/contract-modules/oapp.md) This section highlights that, despite differences in language and runtime, the core concepts across LayerZero’s applications remain consistent—ensuring a unified cross-chain experience regardless of the underlying blockchain. --- --- sidebar_label: Omnichain Tokens (OFT & ONFT) title: Omnichain Tokens (OFT & ONFT) --- # Omnichain Tokens LayerZero’s omnichain token standards provide a unified framework to **transfer both fungible and non-fungible tokens across different blockchain networks**. Regardless of whether the tokens are built on EVM, Solana, or Aptos (or other environments), the underlying design follows these core principles: ## Unified Cross-Chain Transfer Mechanism - **Generic message passing:** Both fungible (OFT) and non-fungible (ONFT) tokens rely on a common cross-chain messaging interface defined in the [OApp Standard](./oapp-standard.md). This interface handles the sending and receiving of token transfer data between chains, abstracting away the underlying chain differences. - **Endpoint as a bridge:** All cross-chain 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. ![OFT Example](/img/learn/oft_mechanism_light.jpg#gh-light-mode-only) ![OFT Example](/img/learn/oft_mechanism.jpg#gh-dark-mode-only) - **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 cross-chain 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 cross-chain 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 cross-chain use cases. ## Robust Fee and Security Configuration - **Fee estimation and payment:** A built-in fee quoting mechanism estimates the cost of cross-chain 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 cross-chain 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 cross-chain 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.md) - [EVM ONFT Overview](../../developers/evm/onft/quickstart.md) - [Solana OFT Overview](../../developers/solana/oft/program.md) - [Aptos Move OFT Overview](../../developers/aptos-move/contract-modules/oft.md) 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. --- --- sidebar_label: Omnichain Composability title: Omnichain Composability --- **Composability** is a core requirement for building advanced, interconnected cross-chain applications. LayerZero’s framework for composability breaks complex cross-chain 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 Cross-Chain 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. Cross-chain 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 ![Composed Light](/img/learn/Composed-Light.svg#gh-light-mode-only) ![Composed Dark](/img/learn/Composed-Dark.svg#gh-dark-mode-only) - **Mitigating atomicity limitations:** In cross-chain 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 cross-chain 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 cross-chain 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 cross-chain operation reaches finality as soon as it is processed. This instant, irrevocable finality is invaluable in cross-chain scenarios, as it prevents inconsistencies between chains and instills user trust, making cross-chain interactions as reliable as single-chain transactions. ## How Composability Works 1. **Initial message dispatch:** The source application initiates a cross-chain 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.md) 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.md) 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 cross-chain 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 cross-chain call is optimized for its specific environment. ## Further Reading For VM-specific guides, developers can refer to: - [EVM Composer Overview](../../developers/evm/composer/overview.md) By leveraging dedicated composer contracts and a structured messaging system, LayerZero’s horizontal composability framework allows developers to build resilient and complex cross-chain applications. --- --- sidebar_label: Omnichain Queries (lzRead) title: Omnichain Queries (lzRead) --- **Omnichain Queries** extend LayerZero’s cross-chain messaging protocol to enable smart contracts to **request** and **retrieve** on-chain 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 cross-chain 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 cross-chain 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 on-chain 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, cross-chain 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 ![Read Example](/img/lzRead_diagram_light.svg#gh-light-mode-only) ![Read Example](/img/lzRead_diagram_dark.svg#gh-dark-mode-only) 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 cross-chain 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 cross-chain 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.md) --- --- sidebar_label: Start Here --- # LayerZero V2 Solidity Contract Standards LayerZero enable seamless cross-chain messaging, configurations for security, and other quality of life improvements to simplify cross-chain development. #### LayerZero Solidity Contract Standards #### LayerZero Solidity Protocol Configurations

:::info To find all of LayerZero's contracts visit the [**LayerZero V2 Protocol Repo**](https://github.com/LayerZero-Labs/LayerZero-v2). ::: ### Installation To start sending cross-chain messages with LayerZero, you can find install instructions for each contract package in [OApp Quickstart](./oapp/overview.md), [OFT Quickstart](./oft/quickstart.md), or [ONFT Quickstart](./onft/quickstart.md). LayerZero also provides [create-lz-oapp](./create-lz-oapp/start.md), an all-in-one npx package that allows developers to create a project from any of the available omnichain standards in <4 minutes! :::tip Get started by running the following from your command line: ```bash npx create-lz-oapp@latest ``` ::: ### Usage Once installed, you can use the contracts in the library by importing them: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { OApp } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; contract MyOApp is OApp { constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) Ownable(_owner) {} // ... rest of OApp interface functions } ``` To keep your system secure, you should **always** use the installed code as-is, and neither copy-paste it from online sources, nor modify it directly. Most of the LayerZero contracts are expected to be used via inheritance: you will inherit from them when writing your own contracts. ## Tooling LayerZero also provides developer tooling to simplify the contract creation, testing, and deployment process: - [LayerZero Scan](./tooling/layerzeroscan.md): a comprehensive block explorer, search, API, and analytics platform for tracking and debugging your omnichain transactions. - [TestHelper (Foundry)](./foundry.md): a suite of functions to simulate cross-chain transactions and validate the behavior of OApps locally in your Foundry unit tests. You can also ask for help or follow development in the [Discord](https://layerzero.network/community). --- --- title: Getting Started with Contract Standards sidebar_label: Getting Started toc_min_heading_level: 2 toc_max_heading_level: 5 --- Use LayerZero's **Contract Standards** to easily start sending arbitrary data, tokens, and external calls using the protocol: - [Omnichain Application (OApp)](./oapp/overview.md): the base contract standard for omnichain messaging and configuration. - [Omnichain Fungible Token (OFT)](./oft/quickstart.md): an extension of `OApp` built for handling and supporting omnichain `ERC20` transfers. - [Omnichain Non-Fungible Token (ONFT)](./onft/quickstart.md): an extension built for handling and supporting omnichain `ERC721` transfers. Each of these contract standards implement common functions for **sending**, **receiving**, and **configuring** omnichain messages via the protocol interface: the [LayerZero Endpoint](../../concepts/protocol/layerzero-endpoint.md) contract. - `OAppSender._lzSend`: internal function that calls `EndpointV2.send` to send a message as `bytes`. - `OAppReceiver._lzReceive`: internal function that delivers the encoded message as `bytes` after the `Executor` calls `EndpointV2.lzReceive`. This method of **encoding** send parameters and **decoding** them on the destination chain is the basis for how all OApps work. ## Example Omnichain Application The `OApp` Standard contains both a **send** and **receive** interface. :::info This code snippet is already implemented in the Remix example below. Simply review this code to understand how it works internally. ::: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; // @dev Import the 'MessagingFee' and 'MessagingReceipt' so it's exposed to OApp implementers import { OAppSender, MessagingFee, MessagingReceipt } from "./OAppSender.sol"; // @dev Import the 'Origin' so it's exposed to OApp implementers import { OAppReceiver, Origin } from "./OAppReceiver.sol"; import { OAppCore } from "./OAppCore.sol"; /** * @title OApp * @dev Abstract contract serving as the base for OApp implementation, combining OAppSender and OAppReceiver functionality. */ // highlight-next-line abstract contract OApp is OAppSender, OAppReceiver {} ``` You can use the **Remix IDE** to see how `OAppSender` and `OAppReceiver` work together for sending and receiving any arbitrary data to supported destination chains. #### OAppSender.sol #### OAppReceiver.sol ### Prerequisites 1. You should first be familiar with writing and deploying contracts to your desired blockchains. This involves understanding the specific smart contract language and the deployment process for those chains. 2. A wallet set up and funded for the chains you'll be working with. ### Deploying Your Contracts We'll deploy the **Source Contract** on `Sepolia`, and the **Destination Contract** on `Optimism Sepolia`: :::info This example can be used with any EVM-compatible blockchain that LayerZero supports. :::

1. Open MetaMask and select the `Ethereum Sepolia` network. Make sure you have native gas in the wallet connected. 2. In Remix under the **Deploy & Run Transactions** tab, select `Injected Provider - MetaMask` in the Environment list. 3. Under the Deploy section, fill in the [Endpoint Address](../../deployments/deployed-contracts.md) for your current chain. #### Sepolia Endpoint Address ``` 0x6edce65403992e310a62460808c4b910d972f10f ``` #### Optimism Sepolia Endpoint Address ``` 0x6edce65403992e310a62460808c4b910d972f10f ``` 4. Click deploy, follow the MetaMask prompt to confirm the transaction, and wait for the contract address to appear under **Deployed Contracts**. 5. Repeat the above steps for any other chains you plan to deploy to and connect. ### Connecting Your Contracts To connect your OApp deployments together, you will need to call `setPeer` on both the Ethereum Sepolia and Optimism Sepolia OApp. 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 // LayerZero/V2/oapp/contracts/oapp/OAppReceiver.sol // @dev must-have configurations for standard OApps function setPeer(uint32 _eid, bytes32 _peer) public virtual onlyOwner { peers[_eid] = _peer; // Array of peer addresses by destination. emit PeerSet(_eid, _peer); // Event emitted each time a peer is set. } ``` To `setPeer` on `SourceOApp`, take the `DestinationOApp` address and call `OApp.addressToBytes32`. Use the returned output as the `_peer`. Your `_peer` should look something like this: `0x0000000000000000000000000a3ecc421699e2eb7f53584d07165d95721a4ca7`. By default, the `OApp` standard inherits `OAppReceiver` which uses this peer inside `lzReceive` to enforce that the sender is the expected origin address. ```solidity // LayerZero/V2/oapp/contracts/oapp/OAppCore.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); } ``` :::tip Remember, an EVM `address` is a `bytes20` value, so you must convert your address to `bytes32` when calling `setPeer`. This can also be easily be done by [**Zero Padding**](https://ethereum.stackexchange.com/questions/103901/can-you-convert-my-address-bytes20-type-to-a-bytes32-string) the address until it is 32 bytes in length. LayerZero uses `bytes32` for broad compatibility with non-EVM chains. :::

Pass the address of your destination contract as a `bytes32` value, as well as the destination endpoint ID. - To send to Ethereum Sepolia, the Endpoint ID is: `40161`. - To send to Optimism Sepolia, the Endpoint ID is: `40232`. :::caution You'll need to repeat this wiring on both contracts in order to send and receive messages. That means calling `setPeer` on both your `Ethereum Sepolia` and `Optimism Sepolia` contracts. **Remember to switch networks in MetaMask.** ::: If successful, you now should be setup to start sending cross-chain messages! ### Estimating Fees The LayerZero Protocol gas fees can vary based on your source chain, `DVNs`, `Executor`, and amount of native gas token you request in `_options`, so you should estimate fees before sending your first transaction. The `OApp.quote` function invokes an internal `OAppSender._quote` to estimate the fees associated with a particular LayerZero transaction using four inputs: - `_dstEid`: This is the identifier of the destination chain's endpoint where the transaction is intended to go. - `_message`: This is the arbitrary message you intend to send to your destination chain and contract. - `_options`: A bytes array that contains serialized execution options that tell the protocol the amount of gas to for the [Executor](../../concepts/permissionless-execution/executors.md) to send when calling `lzReceive`, as well as other function call related settings. - `_payInLzToken`: A boolean which determines whether to return the fee estimate in the native gas token or in ZRO token. :::info In this tutorial, you will deliver `50000` wei for the `lzReceive` call by passing `0x0003010011010000000000000000000000000000c350` as your `_options`. You will be quoted `50000` wei on the source chain, which the Executor will convert to the destination gas token and use in the call. See [**Message Execution Options**](../evm/configuration/options.md) for all possible execution settings. ::: ### Sending Your Message To use the `send` function, simply input a string into the `message` field that you wish to send to your destination chain. #### Contract A Remember to pass the `quote` in Remix under `VALUE` to pay the gas fees on the source and destination, as well as for the [Security Stack](../../concepts/modular-security/security-stack-dvns.md) and Executor who verify and execute the messages. Then call `SourceOApp.send`! #### Contract B Your message may take a few minutes to appear in the destination block explorer, depending on which chains you deploy to. ### Tracking Your Message Finally, let's see what's happening in our transaction. Take your transaction hash and paste it into: https://testnet.layerzeroscan.com/ You should see `Status: Delivered`, confirming your message has been delivered to its destination using LayerZero. **Congrats, you just sent your first omnichain message!** 🥳 ### Further Reading Now that you understand the basics for how OApps work, you should explore setting up your development environment and diving deeper into the omnichain contract standards! - [Create LZ OApp Quickstart](../../developers/evm/create-lz-oapp/start) - [OApp Quickstart](../../developers/evm/oapp/overview) - [OFT Quickstart](../../developers/evm/oft/quickstart) --- --- title: Sending Tokenized Assets sidebar_label: Sending Tokens --- To transfer tokens to different blockchain networks using LayerZero, you have 3 options: - [Build your own Omnichain Token](#building-your-own-omnichain-token) using LayerZero contract standards. - [Send native gas tokens](#sending-small-amounts-of-native-gas) as part of your message's execution options. - [Utilize a native bridge](#option-1-protocols-or-native-bridges-built-on-layerzero) 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.md) and the [ONFT Quickstart](../onft/quickstart.md) 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.md) enable you to send small amounts of native gas as part of your cross-chain 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](../technical-reference/tx-pricing.md). ## 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](https://stargateprotocol.gitbook.io/stargate/developers/evm/how-to-swap)) 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/) for how to transfer and swap cross-chain 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.md) 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. :::caution 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. ::: --- --- title: LayerZero V2 EVM Technical Overview sidebar_label: Technical Overview toc_min_heading_level: 2 toc_max_heading_level: 5 --- 1. A user calls a smart contract `OApp` on the source chain and pays a fee to send a cross-chain message to the `Endpoint`. 2. The `Endpoint` check the validity of the cross-chain message and assigns each job to the `OApp` configured `DVNs` (Decentralized Verifier Networks) and `Executor` to execute the cross-chain 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 cross-chain 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 cross-chain message.

:::tip You can find all of the above contracts by visiting [**Supported Chains**](../../deployments/deployed-contracts.md) and [**Supported DVNs**](../../deployments/dvn-addresses.md). ::: ### Send Overview The `OApp` calls `EndpointV2.send` to send the cross-chain 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 cross-chain 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 // 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; // cross-chain 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 // 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 // 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 cross-chain message. ```solidity // 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 // 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 // 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 cross-chain message. - `payloadHash` is a digest including information about the version and path of the cross-chain message; - `payload` includes information of the `guid` and the body of the cross-chain message. - get the sender `OApp` config about which `DVNs` to use. - assign job for each `DVN`, including both required and optional. ```solidity // 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 // 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 cross-chain message on the destination chain. - update each `DVN`'s fee. - return the `totalFee` used by all `DVNs`. ```solidity // 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 // 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 // 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 // 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 cross-chain 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 cross-chain message using the `_payloadHash`. ```solidity // 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 // 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 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 cross-chain 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 // 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 cross-chain 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 // 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 cross-chain 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 // 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 // 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 // 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 // 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 cross-chain 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 // 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 // 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 // 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 // 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; } ``` :::info 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). ::: :::tip 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). ::: --- --- title: Quickstart - Create Your First Omnichain App sidebar_label: CLI Setup Guide --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; This guide will walk you through the process of sending a simple cross-chain 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. ![OApp Example](/img/learn/ABLight.svg#gh-light-mode-only) ![OApp Example](/img/learn/ABDark.svg#gh-dark-mode-only) 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 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, cross-chain 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 // 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 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 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 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 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 // 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 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 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 cross-chain 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 // 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 // hardhat.config.ts // (...) import {EndpointId} from '@layerzerolabs/lz-definitions'; import './tasks/sendMessage'; // Import the task ``` Now you can send a cross-chain message, for example from Avalanche to Amoy, using: ```bash 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 cross-chain 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 // 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 Status](/img/layerzero-scan-transaction-status.png) 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 cross-chain message are included here, including the message itself, encoded as bytes. 3. **Transaction Fee**: This is how much we paid to send the message cross-chain. 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. ![OmnichainMessage Successful](/img/omnichain-transaction-successful.png) 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 cross-chain messaging. From here, you can explore more advanced features and build more complex omnichain applications. ### Explore Contract Standards - [**Omnichain Token**](../oft/quickstart.md): Create an Omichain Fungible Token that works across chains. - [**Omnichain NFT**](../onft/quickstart.md): Build an Omnichain Non-Fungible Token (ONFT) collection that works across chains. - [**Omnichain Read**](../lzread/overview.md): Read external state from other chains and perform calculations, using LayerZero Read. ### Understand the Protocol and Core Concepts - [**Technical Overview**](../developer-overview.md): Learn about the LayerZero Protocol and how it works under the hood. - [**Configuring Your OApp**](../configuration/dvn-executor-config.md): Learn how to configure your application's Security Stack and Executor settings. --- --- title: Project Configuration sidebar_label: Adding Networks --- 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. ### Modifying Hardhat Config After initializing the repo, you will need to modify your `hardhat.config.ts` with the expected networks you will be working with: ```typescript // hardhat.config.ts networks: { sepolia: { // the LayerZero Endpoint ID // highlight-next-line eid: EndpointId.SEPOLIA_V2_TESTNET, url: 'https://rpc.sepolia.org/', accounts, }, ethereum: { // the LayerZero Endpoint ID // highlight-next-line eid: EndpointId.ETHEREUM_V2_MAINNET, url: 'https://eth.llamarpc.com', accounts, }, bsc_testnet: { // the LayerZero Endpoint ID // highlight-next-line eid: EndpointId.BSC_V2_TESTNET, url: 'https://bsc-testnet.publicnode.com', accounts, }, }, ``` :::info The only notable change from a standard `hardhat.config.ts` setup is the inclusion of a [**LayerZero Endpoint ID**](../../../deployments/deployed-contracts.md). For hardhat specific questions, refer to the [**Hardhat Configuration**](https://hardhat.org/hardhat-runner/docs/config) documentation. ::: :::tip 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`). ::: ### Modifying LayerZero Config The `layerzero.config.ts` first defines what contracts you expect to deploy on each network, using the `@layerzerolabs/lz-definitions` package as a mapping for each network: ```typescript // layerzero.config.ts import {EndpointId} from '@layerzerolabs/lz-definitions'; // Define the Ethereum contract // eid specifies the network (LZ V2 Ethereum Sepolia Testnet) // contractName is the name of the contract. const sepoliaContract = { eid: EndpointId.SEPOLIA_V2_TESTNET, contractName: 'MyOFT', }; // Define the Binance contract // eid specifies the network (LZ V2 BNB Chain Testnet) // contractName is the name of the contract. const bscContract = { eid: EndpointId.BSC_V2_TESTNET, contractName: 'MyOFT', }; // Define the Amoy (Polygon) contract // eid specifies the network (LZ V2 Polygon Amoy Testnet) // contractName is the name of the contract. const amoyContract = { eid: EndpointId.AMOY_V2_TESTNET, contractName: 'MyOFT', }; ``` After defining what contracts to use on each network, you can specify which contracts should be connected on a per pathway basis: ```typescript // layerzero.config.ts module.exports = { // Define the contracts to be deployed on each network // Each contract is associated with a specific blockchain. contracts: [ { contract: sepoliaContract, }, { contract: bscContract, }, { contract: amoyContract, }, ], // Define the pathway between each contract. // This allows for cross-chain communication using LayerZero. connections: [ { from: bscContract, to: sepoliaContract, }, { from: bscContract, to: amoyContract, }, { from: sepoliaContract, to: bscContract, }, { from: sepoliaContract, to: amoyContract, }, { from: amoyContract, to: sepoliaContract, }, { from: amoyContract, to: bscContract, }, ], }; ``` ### Checking Pathway Configurations To check your OApp's current configuration, you can run: ```bash 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 ┌────────────────────┬─────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────────────────────────┐ │ │ 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 │ │ │ │ │ └──────────────────────┴────────────────────────────────────────────────────┘ │ └──────────────────────┴────────────────────────────────────────────────────┘ │ └────────────────────┴─────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────┘ ``` ### Adding Pathway Configurations To add specific configurations on a per pathway basis, review the [Configuring Pathways](./configuring-pathways) section. --- --- title: Testing Contracts --- The LayerZero sample project supports unit testing using both the hardhat and foundry forge development framework, with specific test helpers for each: - `EndpointV2Mock.sol`: a mock LayerZero V2 Endpoint contract, meant for local testing of LayerZero message passing. - `TestHelper.sol`: an extensive LayerZero V2 testing framework, designed for simulating LayerZero state changes and contract interactions. To run your unit tests for both Hardhat and Foundry, run the `test` command using your package manager: ```bash pnpm test ``` ### Example Hardhat Test ```typescript import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; import {expect} from 'chai'; import {Contract, ContractFactory} from 'ethers'; import {deployments, ethers} from 'hardhat'; import {Options} from '@layerzerolabs/lz-v2-utilities'; describe('MyOFT Test', function () { // Constant representing a mock Endpoint ID for testing purposes const eidA = 1; const eidB = 2; // Declaration of variables to be used in the test suite let MyOFT: ContractFactory; let EndpointV2Mock: ContractFactory; let ownerA: SignerWithAddress; let ownerB: SignerWithAddress; let endpointOwner: SignerWithAddress; let myOFTA: Contract; let myOFTB: Contract; let mockEndpointA: Contract; let mockEndpointB: Contract; // Before hook for setup that runs once before all tests in the block before(async function () { // Contract factory for our tested contract MyOFT = await ethers.getContractFactory('MyOFT'); // Fetching the first three signers (accounts) from Hardhat's local Ethereum network const signers = await ethers.getSigners(); ownerA = signers.at(0)!; ownerB = signers.at(1)!; endpointOwner = signers.at(2)!; // The EndpointV2Mock contract comes from @layerzerolabs/test-devtools-evm-hardhat package // and its artifacts are connected as external artifacts to this project // // Unfortunately, hardhat itself does not yet provide a way of connecting external artifacts // so we rely on hardhat-deploy to create a ContractFactory for EndpointV2Mock // // See https://github.com/NomicFoundation/hardhat/issues/1040 const EndpointV2MockArtifact = await deployments.getArtifact('EndpointV2Mock'); EndpointV2Mock = new ContractFactory( EndpointV2MockArtifact.abi, EndpointV2MockArtifact.bytecode, endpointOwner, ); }); // beforeEach hook for setup that runs before each test in the block beforeEach(async function () { // Deploying a mock LZEndpoint with the given Endpoint ID mockEndpointA = await EndpointV2Mock.deploy(eidA); mockEndpointB = await EndpointV2Mock.deploy(eidB); // Deploying two instances of MyOFT contract with different identifiers and linking them to the mock LZEndpoint myOFTA = await MyOFT.deploy('aOFT', 'aOFT', mockEndpointA.address, ownerA.address); myOFTB = await MyOFT.deploy('bOFT', 'bOFT', mockEndpointB.address, ownerB.address); // Setting destination endpoints in the LZEndpoint mock for each MyOFT instance await mockEndpointA.setDestLzEndpoint(myOFTB.address, mockEndpointB.address); await mockEndpointB.setDestLzEndpoint(myOFTA.address, mockEndpointA.address); // Setting each MyOFT instance as a peer of the other in the mock LZEndpoint await myOFTA.connect(ownerA).setPeer(eidB, ethers.utils.zeroPad(myOFTB.address, 32)); await myOFTB.connect(ownerB).setPeer(eidA, ethers.utils.zeroPad(myOFTA.address, 32)); }); // A test case to verify token transfer functionality it('should send a token from A address to B address via each OFT', async function () { // Minting an initial amount of tokens to ownerA's address in the myOFTA contract const initialAmount = ethers.utils.parseEther('100'); await myOFTA.mint(ownerA.address, initialAmount); // Defining the amount of tokens to send and constructing the parameters for the send operation const tokensToSend = ethers.utils.parseEther('1'); const sendParam = [eidB, ethers.utils.zeroPad(ownerB.address, 32), tokensToSend, tokensToSend]; // Defining extra message execution options for the send operation const options = Options.newOptions().addExecutorLzReceiveOption(200000, 0).toHex().toString(); // Fetching the native fee for the token send operation const [nativeFee] = await myOFTA.quoteSend(sendParam, options, false, `0x`, `0x`); // Executing the send operation from myOFTA contract await myOFTA.send(sendParam, options, [nativeFee, 0], ownerA.address, '0x', '0x', { value: nativeFee, }); // Fetching the final token balances of ownerA and ownerB const finalBalanceA = await myOFTA.balanceOf(ownerA.address); const finalBalanceB = await myOFTB.balanceOf(ownerB.address); // Asserting that the final balances are as expected after the send operation expect(finalBalanceA.eq(initialAmount.sub(tokensToSend))).to.be.true; expect(finalBalanceB.eq(tokensToSend)).to.be.true; }); }); ``` ### Example Foundry Test ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.20; // Mock imports import { OFTMock } from "../mocks/OFTMock.sol"; import { ERC20Mock } from "../mocks/ERC20Mock.sol"; import { OFTComposerMock } from "../mocks/OFTComposerMock.sol"; // OApp imports import { IOAppOptionsType3, EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; // OFT imports import { IOFT, SendParam, OFTReceipt } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; import { MessagingFee, MessagingReceipt } from "@layerzerolabs/oft-evm/contracts/OFTCore.sol"; import { OFTMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTMsgCodec.sol"; import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol"; // OZ imports import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; // Forge imports import "forge-std/console.sol"; // DevTools imports import { TestHelperOz5 } from "@layerzerolabs/test-devtools-evm-foundry/contracts/TestHelperOz5.sol"; contract MyOFTTest is TestHelperOz5 { using OptionsBuilder for bytes; uint32 private aEid = 1; uint32 private bEid = 2; OFTMock private aOFT; OFTMock private bOFT; address private userA = address(0x1); address private userB = address(0x2); uint256 private initialBalance = 100 ether; function setUp() public virtual override { // Provide initial Ether balances to users for testing purposes vm.deal(userA, 1000 ether); vm.deal(userB, 1000 ether); // Call the base setup function from the TestHelperOz5 contract super.setUp(); // Initialize 2 endpoints, using UltraLightNode as the library type setUpEndpoints(2, LibraryType.UltraLightNode); // Deploy two instances of OFTMock for testing, associating them with respective endpoints aOFT = OFTMock( _deployOApp(type(OFTMock).creationCode, abi.encode("aOFT", "aOFT", address(endpoints[aEid]), address(this))) ); bOFT = OFTMock( _deployOApp(type(OFTMock).creationCode, abi.encode("bOFT", "bOFT", address(endpoints[bEid]), address(this))) ); // Configure and wire the OFTs together address[] memory ofts = new address[](2); ofts[0] = address(aOFT); ofts[1] = address(bOFT); this.wireOApps(ofts); // Mint initial tokens for userA and userB aOFT.mint(userA, initialBalance); bOFT.mint(userB, initialBalance); } // Test the constructor to ensure initial setup and state are correct function test_constructor() public { // Check that the contract owner is correctly set assertEq(aOFT.owner(), address(this)); assertEq(bOFT.owner(), address(this)); // Verify initial token balances for userA and userB assertEq(aOFT.balanceOf(userA), initialBalance); assertEq(bOFT.balanceOf(userB), initialBalance); // Verify that the token address is correctly set to the respective OFT instances assertEq(aOFT.token(), address(aOFT)); assertEq(bOFT.token(), address(bOFT)); } // Test sending OFT tokens from one user to another function test_send_oft() public { uint256 tokensToSend = 1 ether; // Build options for the send operation bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); // Set up parameters for the send operation SendParam memory sendParam = SendParam( bEid, addressToBytes32(userB), tokensToSend, tokensToSend, options, "", "" ); // Quote the fee for sending tokens MessagingFee memory fee = aOFT.quoteSend(sendParam, false); // Verify initial balances before the send operation assertEq(aOFT.balanceOf(userA), initialBalance); assertEq(bOFT.balanceOf(userB), initialBalance); // Perform the send operation vm.prank(userA); aOFT.send{ value: fee.nativeFee }(sendParam, fee, payable(address(this))); // Verify that the packets were correctly sent to the destination chain. // @param _dstEid The endpoint ID of the destination chain. // @param _dstAddress The OApp address on the destination chain. verifyPackets(bEid, addressToBytes32(address(bOFT))); // Check balances after the send operation assertEq(aOFT.balanceOf(userA), initialBalance - tokensToSend); assertEq(bOFT.balanceOf(userB), initialBalance + tokensToSend); } // Test sending OFT tokens with a composed message function test_send_oft_compose_msg() public { uint256 tokensToSend = 1 ether; // Create an instance of the OFTComposerMock contract OFTComposerMock composer = new OFTComposerMock(); // Build options for the send operation with a composed message bytes memory options = OptionsBuilder .newOptions() .addExecutorLzReceiveOption(200000, 0) .addExecutorLzComposeOption(0, 500000, 0); bytes memory composeMsg = hex"1234"; // Set up parameters for the send operation SendParam memory sendParam = SendParam( bEid, addressToBytes32(address(composer)), tokensToSend, tokensToSend, options, composeMsg, "" ); // Quote the fee for sending tokens MessagingFee memory fee = aOFT.quoteSend(sendParam, false); // Verify initial balances before the send operation assertEq(aOFT.balanceOf(userA), initialBalance); assertEq(bOFT.balanceOf(address(composer)), 0); // Perform the send operation vm.prank(userA); (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) = aOFT.send{ value: fee.nativeFee }( sendParam, fee, payable(address(this)) ); // Verify that the packets were correctly sent to the destination chain. // @param _dstEid The endpoint ID of the destination chain. // @param _dstAddress The OApp address on the destination chain. verifyPackets(bEid, addressToBytes32(address(bOFT))); // Set up parameters for the composed message uint32 dstEid_ = bEid; address from_ = address(bOFT); bytes memory options_ = options; bytes32 guid_ = msgReceipt.guid; address to_ = address(composer); bytes memory composerMsg_ = OFTComposeMsgCodec.encode( msgReceipt.nonce, aEid, oftReceipt.amountReceivedLD, abi.encodePacked(addressToBytes32(userA), composeMsg) ); // Execute the composed message this.lzCompose(dstEid_, from_, options_, guid_, to_, composerMsg_); // Check balances after the send operation assertEq(aOFT.balanceOf(userA), initialBalance - tokensToSend); assertEq(bOFT.balanceOf(address(composer)), tokensToSend); // Verify the state of the composer contract assertEq(composer.from(), from_); assertEq(composer.guid(), guid_); assertEq(composer.message(), composerMsg_); assertEq(composer.executor(), address(this)); assertEq(composer.extraData(), composerMsg_); // default to setting the extraData to the message as well to test } } ``` --- --- title: Deploying Contracts --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; 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 npx hardhat lz:deploy ``` ### Selecting Chains You will be prompted to select which chains to deploy to: ```bash 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 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 deploy.tags = [contractName]; ``` The generic message passing standard for creating [Omnichain Applications (OApps)](../oapp/overview.md): ```bash 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)](../oft/quickstart.md): ```bash 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](../oft/quickstart.md): ```bash 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)](../oft/quickstart.md): ```bash 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 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 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`! --- --- title: Configuring Contracts --- For each contract in your config file, you can configure the following: ```solidity 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. ### Initializing `Config` You can initialize your OApp configurations by running: ``` npx hardhat lz:oapp:config:init --contract-name DEPLOYMENT_NAME --oapp-config CONFIG_FILE_NAME ``` This will auto-populate the provided config file, or create the file if the path does not exist, with the current LayerZero default configurations as a placeholder. For example, running: ``` npx hardhat lz:oapp:config:init --contract-name MyOApp --oapp-config testnet.layerzero.config.ts ``` will create a new file in my directory with the correct type interface: ``` contracts/ deploy/ test/ foundry.toml hardhat.config.ts layerzero.config.ts testnet.layerzero.config.ts <----- ``` :::caution Make sure the `DEPLOYMENT_NAME` exists in your `./deployments` folder, otherwise the task will fail. :::

Head to the `layerzero.config.ts` and scroll down to `module.exports`. As explained previously, the CLI Toolkit organizes your configurations on a per-pathway basis: ```typescript module.exports = { // Define the contracts to be deployed on each network // Each contract is associated with a specific blockchain. contracts: [ { contract: sepoliaContract, }, { contract: bscContract, }, ], // Define the pathway between each contract. // This allows for cross-chain communication using LayerZero. connections: [ // highlight-start { from: bscContract, to: sepoliaContract, }, // highlight-end { from: sepoliaContract, to: bscContract, }, ], }; ``` To add a specific pathway configuration, add a `config: {}` to your connection: ```typescript module.exports = { // Define the contracts to be deployed on each network // Each contract is associated with a specific blockchain. contracts: [ { contract: sepoliaContract, }, { contract: bscContract, }, ], // Define the pathway between each contract. // This allows for cross-chain communication using LayerZero. connections: [ { from: bscContract, to: sepoliaContract, // highlight-next-line config: {}, }, { from: sepoliaContract, to: bscContract, }, ], }; ``` 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. | :::tip 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 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 // LayerZero/V2/protocol/contracts/interfaces/IMessageLibManager.sol function setSendLibrary(address _oapp, uint32 _eid, address _newLib) external; ``` Each [MessageLib](/concepts/protocol/message-send-library.md) contains the available configuration options for the protocol, and so must be set by the application owner to prevent unintended updates. :::info You should use the `sendLibrary` address for the chain you're sending `from` (i.e., `SendUln302.sol` on BSC). ::: :::info 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 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 // 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. :::info Each DVN and Executor contains both on-chain 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. ::: :::tip 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 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 // 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 // LayerZero/V2/messagelib/contracts/uln/uln302/SendUln302.sol uint32 internal constant CONFIG_TYPE_EXECUTOR = 1; uint32 internal constant CONFIG_TYPE_ULN = 2; ``` ```solidity // LayerZero/V2/messagelib/contracts/uln/SendLibBase.sol struct ExecutorConfig { uint32 maxMessageSize; address executor; } ``` ```solidity // 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. :::tip 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 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 // 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 Option Message Type](../oapp/overview#optional-enforced-options). :::info The **Omnichain Fungible Token (OFT) Standard** by default already has **Enforced Options** added to the contract, with two message types available: ```solidity // @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 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 // 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; ``` Review the [Transaction Pricing](../technical-reference/tx-pricing) section and the [Execution Options](../configuration/options) to better understand how you should add your execution gas settings. ### Adding `delegate` ```typescript // layerzero.config.ts contracts: [ { contract: sepolia, config: { delegate: '0x0000000000000000000000000000000000000000', }, }, { contract: bsc, config: { delegate: '0x0000000000000000000000000000000000000000', }, }, ]; ``` ### Adding `owner` ```typescript // 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 npx hardhat lz:ownable:transfer-ownership --oapp-config layerzero.config.ts ``` :::caution 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. ::: ### Final Config Your final config may have different settings, but should define the following parameters: ```typescript 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: 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, }, }, // 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(0), // 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, }, }, // 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, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 60000, value: 0, }, { msgType: 1, optionType: ExecutorOptionType.NATIVE_DROP, amount: 0, receiver: "0x0000000000000000000000000000000000000000", }, { msgType: 2, optionType: ExecutorOptionType.LZ_RECEIVE, index: 0, gas: 60000, value: 1, }, { msgType: 2, optionType: ExecutorOptionType.COMPOSE, index: 0, gas: 50000, value: 0, }, ], }, }, ], ``` ## 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`. If unfamiliar with this concept, review the [OApp Quickstart](../oapp/overview.md#setting-peer). ### 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 $ 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`. If unfamiliar with this concept, review the [OApp Quickstart](../oapp/overview.md#setting-peer). The CLI Tool makes this one step easier by enabling you to wire and configure your contract pathways with a single command: ```bash $ 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 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 $ npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` ### Checking `executor` To see your OApp's configured executor, you can run: ```bash 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 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 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 // 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 $ 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. :::note Ensure your development tools are up to date to utilize this feature, as it relies on the latest versions of the required dependencies. ::: --- --- title: Debugging LayerZero Errors --- 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 npx hardhat lz:errors:list ``` To decode custom error data based on the error selector, run: ```bash npx hardhat lz:errors:decode ``` The output will provide information about the custom error name, which you can compare against the error list. --- --- title: LayerZero V2 OApp Quickstart sidebar_label: Omnichain Application (OApp) --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; 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. ![OApp Example](/img/learn/ABLight.svg#gh-light-mode-only) ![OApp Example](/img/learn/ABDark.svg#gh-dark-mode-only) 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. LayerZero provides `OApp.sol` for implementing generic message passing in your contracts: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import { OAppSender } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppSender.sol"; // @dev import the origin so its exposed to OApp implementers import { OAppReceiver, Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppReceiver.sol"; import { OAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppCore.sol"; abstract contract OApp is OAppSender, OAppReceiver { constructor(address _endpoint, address _owner) OAppCore(_endpoint, _owner) {} function oAppVersion() public pure virtual returns (uint64 senderVersion, uint64 receiverVersion) { senderVersion = SENDER_VERSION; receiverVersion = RECEIVER_VERSION; } } ``` `OApp.sol` provides the core interface logic for interacting with the LayerZero `EndpointV2.sol` contract interface, utilities for managing cross-chain applications, and an extendable send / receive interface for application business logic: ![OApp Inheritance](/img/oapp-inheritance-light.svg#gh-light-mode-only) ![OApp Inheritance](/img/oapp-inheritance-dark.svg#gh-dark-mode-only) :::info If you prefer reading the contract code, see the OApp package in the LayerZero Devtools [**OApp Package**](https://github.com/LayerZero-Labs/devtools/blob/main/packages/oapp-evm/contracts/oapp/OApp.sol). ::: :::tip For developers interested in sending and receiving omnichain tokens, we recommend inheriting the [**OFT Standard**](../oft/quickstart.md) directly instead of OApp. ::: ## Installation To start using LayerZero contracts, you can install the [OApp package](https://github.com/LayerZero-Labs/devtools/tree/main/packages/oapp-evm) to an existing project: ```bash npm install @layerzerolabs/oapp-evm ``` ```bash yarn add @layerzerolabs/oapp-evm ``` ```bash pnpm add @layerzerolabs/oapp-evm ``` ```bash forge init ``` ```bash forge install https://github.com/LayerZero-Labs/devtools ``` ```bash forge install https://github.com/LayerZero-Labs/layerzero-v2 ``` ```bash forge install OpenZeppelin/openzeppelin-contracts@v5.1.0 ``` Then add to your `foundry.toml` under `[profile.default]`: ```toml [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', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', ] ``` :::info 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 "resolutions": { "@openzeppelin/contracts": "^5.0.1", } ``` ::: :::tip LayerZero also provides [**create-lz-oapp**](../create-lz-oapp/start.md), 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 npx create-lz-oapp@latest ``` ::: ## Creating an OApp Contract Every OApp will need to set two arguments in the constructor: 1. **Endpoint Address:** The source chain's [Endpoint Address](../../../deployments/deployed-contracts.md) for communicating with the protocol. 2. **Owner Address:** The address that will own the OApp contract. And define the send and receive function: - `_lzSend`: the internal function your application must call to send an omnichain message. - `_lzReceive`: the function to receive an omnichain message. This internal method is called whenever the `EndpointV2.lzReceive()` is executed at the receiving OApp. :::info The OApp Contract Standard inherits directly from both `OAppSender.sol` and `OAppReceiver.sol`, so that your child contract has handling for both sending and receiving messages. You can inherit directly from either the [**Sender**](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppSender.sol) or [**Receiver**](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppReceiver.sol) contract if your child contract only needs one type of handling, as shown in [**Getting Started**](../../evm/getting-started.md). ::: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { OApp, Origin, MessagingFee } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; contract MyOApp is OApp { constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) Ownable(_owner) {} // Some arbitrary data you want to deliver to the destination chain! string public data; /** * @notice Sends a message from the source to destination chain. * @param _dstEid Destination chain's endpoint ID. * @param _message The message to send. * @param _options Message execution options (e.g., for sending gas to destination). */ function send( uint32 _dstEid, string memory _message, bytes calldata _options ) external payable { // Encodes the message before invoking _lzSend. // Replace with whatever data you want to send! bytes memory _payload = abi.encode(_message); _lzSend( _dstEid, _payload, _options, // Fee in native gas and ZRO token. MessagingFee(msg.value, 0), // Refund address in case of failed source message. payable(msg.sender) ); } /** * @dev Called when data is received from the protocol. It overrides the equivalent function in the parent contract. * Protocol messages are defined as packets, comprised of the following parameters. * @param _origin A struct containing information about where the packet came from. * @param _guid A global unique identifier for tracking the packet. * @param payload Encoded message. */ function _lzReceive( Origin calldata _origin, bytes32 _guid, bytes calldata payload, address, // Executor address as specified by the OApp. bytes calldata // Any extra data or options to trigger on receipt. ) internal override { // Decode the payload to get the message // In this case, type is string, but depends on your encoding! data = abi.decode(payload, (string)); } } ``` ## Deployment Workflow 1. Deploy the `OApp` to all the chains you want to connect. 2. Call `MyOApp.setPeer` to whitelist each destination contract on every destination chain. ```solidity // 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; function addressToBytes32(address _addr) public pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } // Call on both sides per pathway aOApp.setPeer(bEid, addressToBytes32(address(bOApp))); bOApp.setPeer(aEid, addressToBytes32(address(aOApp))); ``` 3. Set the DVN configuration, including optional settings such as block confirmations, security threshold, the Executor, max message size, and send/receive libraries. ```solidity EndpointV2.setSendLibrary(aOApp, bEid, newLib) EndpointV2.setReceiveLibrary(aOApp, bEid, newLib, gracePeriod) EndpointV2.setReceiveLibraryTimeout(aOApp, bEid, lib, gracePeriod) EndpointV2.setConfig(aOApp, sendLibrary, sendConfig) EndpointV2.setConfig(aOApp, receiveLibrary, receiveConfig) EndpointV2.setDelegate(delegate) ``` These custom configurations will be stored on-chain as part of EndpointV2 and your respective `SendLibrary` and `ReceiveLibrary`: ```solidity // LayerZero V2 MessageLibManager.sol (part of EndpointV2.sol) mapping(address sender => mapping(uint32 dstEid => address lib)) internal sendLibrary; mapping(address receiver => mapping(uint32 srcEid => address lib)) internal receiveLibrary; mapping(address receiver => mapping(uint32 srcEid => Timeout)) public receiveLibraryTimeout; // LayerZero V2 SendLibBase.sol (part of SendUln302.sol) mapping(address oapp => mapping(uint32 eid => ExecutorConfig)) public executorConfigs; // LayerZero V2 UlnBase.sol (both in SendUln302.sol and ReceiveUln302.sol) mapping(address oapp => mapping(uint32 eid => UlnConfig)) internal ulnConfigs; // LayerZero V2 EndpointV2.sol mapping(address oapp => address delegate) public delegates; ``` You can find example scripts to make these calls under [Security and Executor Configuration](../configuration/dvn-executor-config.md). :::warning These configurations control the verification mechanisms of messages sent between your OApps. You should review the above settings carefully. If no configuration is set, the configuration will fallback to the default configurations set by LayerZero Labs. For example: ```solidity /// @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(); } } ``` :::

4. (**Recommended**) Optionally, if you inherit `OAppOptionsType3`, you can enforce specific gas settings when users call `aOApp.send`. ```solidity // 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 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 details about each setting below. ### Implementing `_lzSend` To start sending messages from your OApp, you'll need to call `_lzSend` with your own contract logic. Depending on your application, this might initiate token transfers, burn and mint NFTs, or just pass a simple string between chains. #### Example: Sending a String Consider the scenario where you want to send a simple string `_message` to store on a destination chain. ```solidity // Sends a message from the source to destination chain. function send(uint32 _dstEid, string memory _message, bytes calldata _options) external payable { bytes memory _payload = abi.encode(_message); // Encodes message as bytes. _lzSend( _dstEid, // Destination chain's endpoint ID. _payload, // Encoded message payload being sent. _options, // Message execution options (e.g., gas to use on destination). MessagingFee(msg.value, 0), // Fee struct containing native gas and ZRO token. payable(msg.sender) // The refund address in case the send call reverts. ); } ``` You start by first encoding the `_message` as a bytes array and passing five arguments to `_lzSend`: 1. `_dstEid`: The destination Endpoint ID. 2. `_message`: The message to be sent. 3. `_options`: Message execution options for protocol handling _(see below)_. 4. `MessagingFee`: what token will be used to pay for the transaction? ```solidity struct MessagingFee { uint256 nativeFee; // Fee amount in native gas token. uint256 lzTokenFee; // Fee amount in ZRO token. } ``` 5. `_refundAddress`: specifies the address to which any excess fees should be refunded. ```solidity payable(msg.sender) // The address of the user or contract that initiated the transaction. ``` :::info If your refund address is a smart contract you will need to implement a fallback function in order for it to receive the refund. ::: ### Message Execution Options You might be wondering, what are message execution `_options`? `_options` are a generated bytes array with specific instructions for the [Security Stack](../../../concepts/modular-security/security-stack-dvns.md) and [Executor](../../../concepts/permissionless-execution/executors) to use when handling the authentication and execution of received messages. You can find how to generate all the available `_options` in [Message Execution Options](../configuration/options.md), but for this tutorial you'll focus on providing the Executor with a gas amount to use when executing our message: - `ExecutorLzReceiveOption`: instructions for how much gas should be used when calling `lzReceive` on the destination Endpoint. When generated correctly, the `_options` parameter will be used in the Endpoint `quote` to ensure enough `msg.value` is paid based to match the Executor amount. For example, to send a vanilla OFT, you usually need `60000` wei in destination native gas during message execution: ```solidity _options = 0x0003010011010000000000000000000000000000ea60; ``` :::tip `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. ::: #### Optional: Enforced Options Once you determine ideal message `_options`, you will want to make sure users adhere to it. In the case of OApp, you mostly want to make sure the gas amount you have included in `_options` for the `lzReceive` call can be enforced for all callers of `_lzSend`, to prevent reverts. To require a caller to use a specific `_options`, your OApp can inherit the enforced options interface `IOAppOptionsType3.sol`: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import { OApp, Origin, MessagingFee } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import { IOAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppOptionsType3.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; contract MyOApp is OApp, IOAppOptionsType3 { constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) Ownable(_owner) {} } ``` The `setEnforcedOptions` function allows the contract owner to specify mandatory execution options, making sure that the application behaves as expected when users interact with it. Here is code snippet from `oapp/libs/OAppOptionsType3.sol`: ```solidity /** * @dev Sets the enforced options for specific endpoint and message type combinations. * @param _enforcedOptions An array of EnforcedOptionParam structures specifying enforced options. * * @dev Only the owner/admin of the OApp can call this function. * @dev Provides a way for the OApp to enforce things like paying for PreCrime, AND/OR minimum dst lzReceive gas amounts etc. * @dev These enforced options can vary as the potential options/execution on the remote may differ as per the msgType. * eg. Amount of lzReceive() gas necessary to deliver a lzCompose() message adds overhead you dont want to pay * if you are only making a standard LayerZero message ie. lzReceive() WITHOUT sendCompose(). */ function setEnforcedOptions(EnforcedOptionParam[] calldata _enforcedOptions) public virtual onlyOwner { _setEnforcedOptions(_enforcedOptions); } function _setEnforcedOptions(EnforcedOptionParam[] memory _enforcedOptions) internal virtual { for (uint256 i = 0; i < _enforcedOptions.length; i++) { // @dev Enforced options are only available for optionType 3, as type 1 and 2 dont support combining. _assertOptionsType3(_enforcedOptions[i].options); enforcedOptions[_enforcedOptions[i].eid][_enforcedOptions[i].msgType] = _enforcedOptions[i].options; } emit EnforcedOptionSet(_enforcedOptions); } ``` To use `setEnforcedOptions`, we only need to pass one parameter: - `EnforcedOptionParam[]`: a struct specifying the execution options per message type and destination chain. ```solidity struct EnforcedOptionParam { uint32 eid; // destination endpoint id uint16 msgType; // the message type bytes options; // the execution option bytes array } ``` You will need to define your OApp's `msgType` and what those messaging types look like. For example, OFT Standard only has handling for 2 message types: ```solidity // @dev execution types to handle different enforcedOptions uint16 internal constant SEND = 1; // a standard token transfer via send() uint16 internal constant SEND_AND_CALL = 2; // a composed token transfer via send() ``` You will pass these values in when specifying the `msgType` for your `_options`. If you're looking for complete example how to set enforced options in Solidity this Foundry [test case](https://github.com/LayerZero-Labs/LayerZero-v2/blob/7aebbd7c79b2dc818f7bb054aed2405ca076b9d6/packages/layerzero-v2/evm/oapp/test/OFT.t.sol#L441) might be helpful: ```solidity function test_combine_options() public { uint32 eid = 1; uint16 msgType = 1; bytes memory enforcedOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); EnforcedOptionParam[] memory enforcedOptionsArray = new EnforcedOptionParam[](1); enforcedOptionsArray[0] = EnforcedOptionParam(eid, msgType, enforcedOptions); aOFT.setEnforcedOptions(enforcedOptionsArray); bytes memory extraOptions = OptionsBuilder.newOptions().addExecutorNativeDropOption( 1.2345 ether, addressToBytes32(userA) ); bytes memory expectedOptions = OptionsBuilder .newOptions() .addExecutorLzReceiveOption(200000, 0) .addExecutorNativeDropOption(1.2345 ether, addressToBytes32(userA)); bytes memory combinedOptions = aOFT.combineOptions(eid, msgType, extraOptions); assertEq(combinedOptions, expectedOptions); } ``` ### Estimating Gas Fees Often with the LayerZero protocol you'll want to know an estimate of how much gas a message will cost to be sent and received. To do this you can implement a `quote()` function within the OApp contract to return an estimate from the Endpoint contract to use as a recommended `msg.value`. ```solidity /* @dev Quotes the gas needed to pay for the full omnichain transaction. * @return nativeFee Estimated gas fee in native gas. * @return lzTokenFee Estimated gas fee in ZRO token. */ function quote( uint32 _dstEid, // Destination chain's endpoint ID. string memory _message, // The message to send. bytes calldata _options, // Message execution options bool _payInLzToken // boolean for which token to return fee in ) public view returns (uint256 nativeFee, uint256 lzTokenFee) { bytes memory _payload = abi.encode(_message); MessagingFee memory fee = _quote(_dstEid, _payload, _options, _payInLzToken); return (fee.nativeFee, fee.lzTokenFee); } ``` The `_quote` can be returned in either the native gas token or in ZRO token, supporting both payment methods. Because cross-chain gas fees are dynamic, this quote should be generated right before calling `_lzSend` to ensure accurate pricing. :::tip Make sure that the arguments passed into the `quote()` function identically match the parameters used in the `lzSend()` function. If parameters mismatch, you may run into errors as your `msg.value` will not match the actual gas quote. :::

:::info Remember that when sending a message through LayerZero, the `msg.sender` will be paying for gas on the source chain, fees to the selected DVNs to validate the message, and for gas on the destination chain to execute the transaction. This results in a single bundled fee on the source chain, abstracting gas away on every other chain, leading to better composability. ::: ### Implementing `_lzReceive` To start receiving messages on a destination, your OApp needs to override the `_lzReceive` function. ```solidity function _lzReceive( Origin calldata _origin, // struct containing info about the message sender bytes32 _guid, // global packet identifier bytes calldata payload, // encoded message payload being received address _executor, // the Executor address. bytes calldata _extraData // arbitrary data appended by the Executor ) internal override { data = abi.decode(payload, (string)); // your logic here } ``` `_lzReceive` takes a few main inputs for message handling: 1. `_origin`: a struct generated by the protocol containing information about where the message came from. ```solidity struct Origin { uint32 srcEid; // The source chain's Endpoint ID. bytes32 sender; // The sending OApp address. uint64 nonce; // The message nonce for the pathway. } ``` 2. `_guid`: a unique identifier for tracking the message. 3. `payload`: the message in encoded bytes format. 4. `_executor`: the address of the Executor calling the Endpoint's `lzReceive` function. 5. `_extraData`: Designed to carry arbitrary data appended by the Executor and passed along with the message payload. Cannot be modified by the OApp. :::note Even if your receiving OApp contract doesn't use every interface parameter, they must be included to match `_lzReceive`'s function signature. :::

What's great about an OApp is that you can define any arbitrary contract logic to trigger within `_lzReceive`. That means that this function could store data, trigger other functions, or even invoke a nested `_lzSend` again to trigger an action back on the source chain. For advanced usage, LayerZero provides a full list of [Message Design Patterns](../oapp/message-design-patterns.md) to experiment with. ### Setting Delegates In a given OApp, a delegate is able to apply configurations on behalf of the OApp. This delegate gains the ability to handle various critical tasks such as setting configurations and MessageLibs, and skipping or clearing payloads. By default, the contract owner is set as the delegate. The `setDelegate` function allows for changing this, but we recommend you always keep contract owner as delegate. ```solidity function setDelegate(address _delegate) public onlyOwner { endpoint.setDelegate(_delegate); } ``` For instructions on how to implement custom configurations after setting your delegate, refer to the [OApp Configuration](../configuration/dvn-executor-config.md). ### Security and Governance Given the impact associated with deployment, configuration, and debugging functions, OApp owners may want to add additional security measures in place to call core contract functions beyond just the `onlyOwner` requirement, such as: - **Governance Controls**: Implementing a governance mechanism where decisions to clear messages are voted upon by stakeholders. - **Multisig Deployment**: Deploying with a multisig wallet, preventing arbitrary actions by any one team member. - **Timelocks**: Using a timelock to delay the execution of certain function, giving stakeholders time to react if the function is called inappropriately. ## Usage That's it. Once deployed, you just need to complete a few post-deployment requirements. ### Setting Peer Once you've finished your [OApp Configuration](../configuration/dvn-executor-config.md), 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 // @dev must-have configurations for standard OApps function setPeer(uint32 _eid, bytes32 _peer) public virtual onlyOwner { peers[_eid] = _peer; // Array of peer addresses by destination. emit PeerSet(_eid, _peer); // Event emitted each time a peer is set. } ``` :::caution 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`. :::

:::warning 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. :::

The [LayerZero Endpoint](../../../concepts/protocol/layerzero-endpoint.md) will use this peer as the destination address for the cross-chain message: ```solidity // @dev the endpoint send method called by _lzSend endpoint.send{ value: messageValue }( MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0), _refundAddress ); ``` To see if an address is the trusted peer you expect for a destination, you can read the `peers` mapping directly. ### Calling `send` Once your source and destination chain contracts have successfully been deployed and peers set, you're ready to begin passing messages between them. Remember to generate a fee estimate using `quote` first, and then pass the returned native gas amount as your `msg.value`. ```solidity > MyOApp.send{value: msg.value}(101, "My first omnichain message!", 0x0003010011010000000000000000000000000000c350) ``` ### 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.md) for any unexpected complications when sending a message. You can also ask for help or follow development in the [Discord](https://layerzero.network/community). --- --- title: LayerZero V2 OFT Quickstart sidebar_label: Omnichain Fungible Token (OFT) --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; 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. #### OFT.sol `_burn` the spender's amount on the source chain (Chain A), triggering a new token to `_mint` on the target chain (Chain B), via the paired OFT contract. ![OFT Example](/img/learn/oft_mechanism_light.jpg#gh-light-mode-only) ![OFT Example](/img/learn/oft_mechanism.jpg#gh-dark-mode-only) `OFT.sol` extends the base `OApp.sol`'s bridging logic and inherits `ERC20`, meaning your OFT contract address supports `IERC20` directly: ![OFT Inheritance](/img/oft-inheritance-light.svg#gh-light-mode-only) ![OFT Inheritance](/img/oft-inheritance-dark.svg#gh-dark-mode-only) #### OFTAdapter.sol `ERC20.safeTransferFrom` the spender to the OFT Adapter contract, triggering a `_mint` of the same amount on the selected destination chain (Chain B) via the paired OFT Contract. To unlock the tokens in the source chain's OFT Adapter, you will call `OFT.send` (Chain B), triggering the token `_burn`, and sending a message via the protocol to `ERC20.safeTransfer` out of the Adapter to the receiving address (Chain A). ![OFT Example](/img/learn/oft-adapter-light.svg#gh-light-mode-only) ![OFT Example](/img/learn/oft-adapter-dark.svg#gh-dark-mode-only) `OFTAdapter.sol` supports ERC20 tokens, but itself does not inherit the ERC20 contract. Instead, you can call `OFTAdapter.token()` to see the connected ERC20 token. ![OFT Inheritance](/img/oft-adapter-inheritance-light.svg#gh-light-mode-only) ![OFT Inheritance](/img/oft-adapter-inheritance-dark.svg#gh-dark-mode-only) Using this design pattern, LayerZero can **extend** any fungible token to interoperate with other chains. The most widely used of these standards is `OFT.sol`, an extension of the [OApp Contract Standard](../oapp/overview.md) and the [ERC20 Token Standard](https://docs.openzeppelin.com/contracts/5.x/erc20). :::info If you prefer reading the contract code, see the OFT contract in the LayerZero Devtools [**OFT Package**](https://github.com/LayerZero-Labs/devtools/blob/main/packages/oft-evm/contracts/OFT.sol). :::

## Installation To start using the `OFT` and `OFTAdapter` contracts, you can install the [OFT package](https://www.npmjs.com/package/@layerzerolabs/oapp-evm) to an existing project: ```bash npm install @layerzerolabs/oft-evm ``` ```bash yarn add @layerzerolabs/oft-evm ``` ```bash pnpm add @layerzerolabs/oft-evm ``` ```bash forge init ``` ```bash forge install https://github.com/LayerZero-Labs/devtools ``` ```bash forge install https://github.com/LayerZero-Labs/layerzero-v2 ``` ```bash forge install OpenZeppelin/openzeppelin-contracts@v5.1.0 ``` Then add to your `foundry.toml` under `[profile.default]`: ```toml [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', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', ] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options ``` :::info 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 "resolutions": { "@openzeppelin/contracts": "^5.0.1", } ``` ::: :::tip LayerZero also provides [**create-lz-oapp**](../create-lz-oapp/start.md), 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 npx create-lz-oapp@latest ``` ::: ## Constructing an OFT Contract To create an OFT, deploy the OFT contract on every chain you want the token to exist on. If your token already exists on the chain you want to connect, you can deploy the OFT Adapter contract to act as an intermediary lockbox for the token. ```solidity // 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 _delegate ) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {} } ``` :::tip Remember to add the ERC20 `_mint` method either in the constructor or as a protected `mint` function before deploying. :::

This contract contains everything necessary to launch an omnichain ERC20 and can be deployed immediately! It also can be highly customized if you wish to add extra functionality. Under the hood, `OFT.sol` extends `ERC20.sol`, by inheriting `OFTCore.sol`. OFT also overrides `_debit` and `_credit` to use the ERC20 `_mint` and `_burn` methods: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { IOFT, OFTCore } from "./OFTCore.sol"; /** * @title OFT Contract * @dev OFT is an ERC-20 token that extends the functionality of the OFTCore contract. */ abstract contract OFT is OFTCore, ERC20 { /** * @dev Constructor for the OFT contract. * @param _name The name of the OFT. * @param _symbol The symbol of the OFT. * @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 // highlight-next-line ) ERC20(_name, _symbol) OFTCore(decimals(), _lzEndpoint, _delegate) {} /** * @dev Retrieves the address of the underlying ERC20 implementation. * @return The address of the OFT token. * * @dev In the case of OFT, address(this) and erc20 are the same contract. */ function token() public view returns (address) { return address(this); } /** * @notice Indicates whether the OFT contract requires approval of the 'token()' to send. * @return requiresApproval Needs approval of the underlying token implementation. * * @dev In the case of OFT where the contract IS the token, approval is NOT required. */ function approvalRequired() external pure virtual returns (bool) { return false; } /** * @dev Burns tokens from the sender's specified balance. * @param _from The address to debit the tokens from. * @param _amountLD The amount of tokens to send in local decimals. * @param _minAmountLD The minimum amount to send in local decimals. * @param _dstEid The destination chain ID. * @return amountSentLD The amount sent in local decimals. * @return amountReceivedLD The amount received in local decimals on the remote. */ function _debit( address _from, uint256 _amountLD, uint256 _minAmountLD, uint32 _dstEid ) internal virtual override returns (uint256 amountSentLD, uint256 amountReceivedLD) { (amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid); // @dev In NON-default OFT, amountSentLD could be 100, with a 10% fee, the amountReceivedLD amount is 90, // therefore amountSentLD CAN differ from amountReceivedLD. // @dev Default OFT burns on src. // highlight-next-line _burn(_from, amountSentLD); } /** * @dev Credits tokens to the specified address. * @param _to The address to credit the tokens to. * @param _amountLD The amount of tokens to credit in local decimals. * @dev _srcEid The source chain ID. * @return amountReceivedLD The amount of tokens ACTUALLY received in local decimals. */ function _credit( address _to, uint256 _amountLD, uint32 /*_srcEid*/ ) internal virtual override returns (uint256 amountReceivedLD) { if (_to == address(0x0)) _to = address(0xdead); // _mint(...) does not support address(0x0) // @dev Default OFT mints on dst. // highlight-next-line _mint(_to, _amountLD); // @dev In the case of NON-default OFT, the _amountLD MIGHT not be == amountReceivedLD. return _amountLD; } } ``` This design allows `OFT.sol` to facilitate cross-chain token transfers while maintaining compatibility with the ERC20 token standard and extensions. Any ERC20 compatible token library can be used with LayerZero's OFT Standard. By default, the OFT follows ERC20 convention and uses a value of `18` for decimals. To use a different value, you will need to override the `decimals()` function in your contract.
```solidity // 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) {} } ``` :::warning **There can only be one 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. :::

This contract contains everything necessary to launch an omnichain ERC20 and can be deployed immediately! It also can be highly customized if you wish to add extra functionality. Under the hood, `OFTAdapter.sol` uses the `SafeERC20.sol` library to handle transferring tokens to and from the Adapter contract by overriding OFTCore's `_debit` and `_credit` methods: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import { IERC20Metadata, IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IOFT, OFTCore } from "./OFTCore.sol"; /** * @title OFTAdapter Contract * @dev OFTAdapter is a contract that adapts an ERC-20 token to the OFT functionality. * * @dev For existing ERC20 tokens, this can be used to convert the token to crosschain compatibility. * @dev WARNING: ONLY 1 of these should exist for a given global mesh, * unless you make a NON-default implementation of OFT and needs to be done very carefully. * @dev WARNING: The default OFTAdapter implementation assumes LOSSLESS transfers, ie. 1 token in, 1 token out. * IF the 'innerToken' applies something like a transfer fee, the default will NOT work... * a pre/post balance check will need to be done to calculate the amountSentLD/amountReceivedLD. */ abstract contract OFTAdapter is OFTCore { // highlight-next-line using SafeERC20 for IERC20; IERC20 internal immutable innerToken; /** * @dev Constructor for the OFTAdapter contract. * @param _token The address of the ERC-20 token to be adapted. * @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 // highlight-next-line ) OFTCore(IERC20Metadata(_token).decimals(), _lzEndpoint, _delegate) { innerToken = IERC20(_token); } /** * @dev Retrieves the address of the underlying ERC20 implementation. * @return The address of the adapted ERC-20 token. * * @dev In the case of OFTAdapter, address(this) and erc20 are NOT the same contract. */ function token() public view returns (address) { return address(innerToken); } /** * @notice Indicates whether the OFT contract requires approval of the 'token()' to send. * @return requiresApproval Needs approval of the underlying token implementation. * * @dev In the case of default OFTAdapter, approval is required. * @dev In non-default OFTAdapter contracts with something like mint and burn privileges, it would NOT need approval. */ function approvalRequired() external pure virtual returns (bool) { return true; } /** * @dev Locks tokens from the sender's specified balance in this contract. * @param _from The address to debit from. * @param _amountLD The amount of tokens to send in local decimals. * @param _minAmountLD The minimum amount to send in local decimals. * @param _dstEid The destination chain ID. * @return amountSentLD The amount sent in local decimals. * @return amountReceivedLD The amount received in local decimals on the remote. * * @dev msg.sender will need to approve this _amountLD of tokens to be locked inside of the contract. * @dev WARNING: The default OFTAdapter implementation assumes LOSSLESS transfers, ie. 1 token in, 1 token out. * IF the 'innerToken' applies something like a transfer fee, the default will NOT work... * a pre/post balance check will need to be done to calculate the amountReceivedLD. */ function _debit( address _from, uint256 _amountLD, uint256 _minAmountLD, uint32 _dstEid ) internal virtual override returns (uint256 amountSentLD, uint256 amountReceivedLD) { (amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid); // @dev Lock tokens by moving them into this contract from the caller. // highlight-next-line innerToken.safeTransferFrom(_from, address(this), amountSentLD); } /** * @dev Credits tokens to the specified address. * @param _to The address to credit the tokens to. * @param _amountLD The amount of tokens to credit in local decimals. * @dev _srcEid The source chain ID. * @return amountReceivedLD The amount of tokens ACTUALLY received in local decimals. * * @dev WARNING: The default OFTAdapter implementation assumes LOSSLESS transfers, ie. 1 token in, 1 token out. * IF the 'innerToken' applies something like a transfer fee, the default will NOT work... * a pre/post balance check will need to be done to calculate the amountReceivedLD. */ function _credit( address _to, uint256 _amountLD, uint32 /*_srcEid*/ ) internal virtual override returns (uint256 amountReceivedLD) { // @dev Unlock the tokens and transfer to the recipient. // highlight-next-line innerToken.safeTransfer(_to, _amountLD); // @dev In the case of NON-default OFTAdapter, the amountLD MIGHT not be == amountReceivedLD. return _amountLD; } } ```
## Deployment Workflow 1. Deploy the `OFT` to all the chains you want to connect. 2. Since `OFT` extends `OApp`, call `OFT.setPeer` to whitelist each destination contract on every destination chain. ```solidity // The real endpoint ids will vary per chain, and can be found under "Supported Chains" uint32 aEid = 1; uint32 bEid = 2; MyOFT aOFT; MyOFT bOFT; function addressToBytes32(address _addr) public pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } // Call on both sides per pathway aOFT.setPeer(bEid, addressToBytes32(address(bOFT))); bOFT.setPeer(aEid, addressToBytes32(address(aOFT))); ``` 3. Set the DVN configuration, including optional settings such as block confirmations, security threshold, the Executor, max message size, and send/receive libraries. ```solidity EndpointV2.setSendLibrary(aOFT, bEid, newLib) EndpointV2.setReceiveLibrary(aOFT, bEid, newLib, gracePeriod) EndpointV2.setReceiveLibraryTimeout(aOFT, bEid, lib, gracePeriod) EndpointV2.setConfig(aOFT, sendLibrary, sendConfig) EndpointV2.setConfig(aOFT, receiveLibrary, receiveConfig) EndpointV2.setDelegate(delegate) ``` These custom configurations will be stored on-chain as part of `EndpointV2`, along with your respective `SendLibrary` and `ReceiveLibrary`: ```solidity // LayerZero V2 MessageLibManager.sol (part of EndpointV2.sol) mapping(address sender => mapping(uint32 dstEid => address lib)) internal sendLibrary; mapping(address receiver => mapping(uint32 srcEid => address lib)) internal receiveLibrary; mapping(address receiver => mapping(uint32 srcEid => Timeout)) public receiveLibraryTimeout; // LayerZero V2 SendLibBase.sol (part of SendUln302.sol) mapping(address oapp => mapping(uint32 eid => ExecutorConfig)) public executorConfigs; // LayerZero V2 UlnBase.sol (both in SendUln302.sol and ReceiveUln302.sol) mapping(address oapp => mapping(uint32 eid => UlnConfig)) internal ulnConfigs; // LayerZero V2 EndpointV2.sol mapping(address oapp => address delegate) public delegates; ``` You can find example scripts to make these calls under [Security and Executor Configuration](../configuration/dvn-executor-config.md). :::warning These configurations control the verification mechanisms of messages sent between your OApps. You should review the above settings carefully. If no configuration is set, the configuration will fallback to the default configurations set by LayerZero Labs. For example: ```solidity /// @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(); } } ``` :::

4. (**Recommended**) The OFT inherits `OAppOptionsType3`, meaning you can enforce specific gas settings when users call `aOFT.send`. ```solidity 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 aOFT.setEnforcedOptions(aEnforcedOptions); ``` 5. Required only for `OFTAdapter`: Approve your `OFTAdapter` as a spender of your `ERC20` token for the token amount you want to transfer by calling `ERC20.approve`. This comes standard in the [`ERC20` interface](https://eips.ethereum.org/EIPS/eip-20#methods), and is required when using an intermediary contract to spend token amounts on behalf of the caller. See more details about each setting below. ### OFTCore Most of the LayerZero cross-chain messaging logic can be found within `OFTCore.sol`. This contract implements the `OApp` related functions like `_lzSend`, `_lzReceive`, and `sendCompose`, while also defining the core OFT interface that every OFT variant should adhere to. `OFT.sol` overrides the [`_debit`](#adding-send-logic) and [`_credit`](#adding-receive-logic) methods found in `OFTCore.sol` to use the ERC20 internal `_burn` and `_mint` methods respectively during cross-chain token transfer. Other OFT variants will override `_debit` and `_credit` differently depending on implementation (e.g., [`OFTAdapter.sol`](../oft/quickstart.md) overrides `_debit` and `_credit` to use `ERC20.safeTransferFrom` to lock / unlock tokens from the OFT Adapter contract itself). You can also override these methods to add additional functionality to the base transfer logic, which will be explored below. ### 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, many non-EVM environments use `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`. :::info 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. ```solidity // @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 function sharedDecimals() public view virtual returns (uint8) { return 6; } ``` To modify this default, simply override the `sharedDecimals` function to return another value. :::caution Shared decimals also control how token transfer precision is calculated. ::: ### Token Transfer Precision The OFT Standard also handles differences in decimal precision before every cross-chain 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 Vanilla OFTs use a local decimal value of `18` (the norm for ERC20 tokens), and a shared decimal value of `6`. ``` 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 ``` :::tip 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. ```solidity /** * @dev Internal function to remove dust from the given local decimal amount. * @param _amountLD The amount in local decimals. * @return amountLD The amount after removing dust. * * @dev Prevents the loss of dust when moving amounts between chains with different decimals. * @dev eg. uint(123) with a conversion rate of 100 becomes uint(100). */ function _removeDust(uint256 _amountLD) internal view virtual returns (uint256 amountLD) { return (_amountLD / decimalConversionRate) * decimalConversionRate; } ``` :::tip In summary, this adjustment via the **`_removeDust`** function prevents OFT transfers from a potential loss of value due to rounding errors between different VMs, and should be called after determining the actual transfer amount (e.g., after deducting fees). ::: ### Adding Send Logic When calling the `send` function, `_debit` is invoked, triggering the OFT's internal ERC20 `_burn` method to be invoked. ```solidity /** * @dev Executes the send operation. * @param _sendParam The parameters for the send operation. * @param _fee The calculated fee for the send() operation. * - nativeFee: The native fee. * - lzTokenFee: The lzToken fee. * @param _refundAddress The address to receive any excess funds. * @return msgReceipt The receipt for the send operation. * @return oftReceipt The OFT receipt information. * * @dev MessagingReceipt: LayerZero msg receipt * - guid: The unique identifier for the sent message. * - nonce: The nonce of the sent message. * - fee: The LayerZero fee incurred for the message. */ function send( SendParam calldata _sendParam, MessagingFee calldata _fee, address _refundAddress ) external payable virtual returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) { // @dev Applies the token transfers regarding this send() operation. // - amountSentLD is the amount in local decimals that was ACTUALLY sent/debited from the sender. // - amountReceivedLD is the amount in local decimals that will be received/credited to the recipient on the remote OFT instance. (uint256 amountSentLD, uint256 amountReceivedLD) = _debit( msg.sender, _sendParam.amountLD, _sendParam.minAmountLD, _sendParam.dstEid ); // @dev Builds the options and OFT message to quote in the endpoint. (bytes memory message, bytes memory options) = _buildMsgAndOptions(_sendParam, amountReceivedLD); // @dev Sends the message to the LayerZero endpoint and returns the LayerZero msg receipt. msgReceipt = _lzSend(_sendParam.dstEid, message, options, _fee, _refundAddress); // @dev Formulate the OFT receipt. oftReceipt = OFTReceipt(amountSentLD, amountReceivedLD); emit OFTSent(msgReceipt.guid, _sendParam.dstEid, msg.sender, amountSentLD, amountReceivedLD); } ``` 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. All of the previous functions use the `_debitView` function to handle how many tokens should be debited on the source chain, versus credited on the destination. This function can be overridden, allowing your OFT to implement custom fees by changing the `amountSentLD` and `amountReceivedLD` amounts: ```solidity /** * @dev Internal function to mock the amount mutation from a OFT debit() operation. * @param _amountLD The amount to send in local decimals. * @param _minAmountLD The minimum amount to send in local decimals. * @dev _dstEid The destination endpoint ID. * @return amountSentLD The amount sent, in local decimals. * @return amountReceivedLD The amount to be received on the remote chain, in local decimals. * * @dev This is where things like fees would be calculated and deducted from the amount to be received on the remote. */ function _debitView( uint256 _amountLD, uint256 _minAmountLD, uint32 /*_dstEid*/ ) internal view virtual returns (uint256 amountSentLD, uint256 amountReceivedLD) { // @dev Remove the dust so nothing is lost on the conversion between chains with different decimals for the token. // highlight-next-line amountSentLD = _removeDust(_amountLD); // @dev The amount to send is the same as amount received in the default implementation. amountReceivedLD = amountSentLD; // @dev Check for slippage. if (amountReceivedLD < _minAmountLD) { revert SlippageExceeded(amountReceivedLD, _minAmountLD); } } ``` :::caution The highlighted line above demonstrates how the OFT is safe from overflow because it reduces the size of `_amountLD` to a value that fits within the expected range of the destination chain's precision by calling `_removeDust`. This method looks at the desired amount of tokens to transfer and only allows the sender to send values that meet the allowed decimal precision. If you add fees to `_debitView`, make sure you implement the fee before calling `_removeDust`, so that the OFT can still maintain the correct level of decimal precision. Review [**Token Transfer Precision**](#token-transfer-precision) to learn more about removing dust values. ::: ### Adding Receive Logic Similar to `send`, you can add custom logic when receiving an ERC20 token transfer on the destination chain by overriding the `_credit` function. ```solidity /** * @dev Credits tokens to the specified address. * @param _to The address to credit the tokens to. * @param _amountLD The amount of tokens to credit in local decimals. * @dev _srcEid The source chain ID. * @return amountReceivedLD The amount of tokens ACTUALLY received in local decimals. */ function _credit( address _to, uint256 _amountLD, uint32 /*_srcEid*/ ) internal virtual override returns (uint256 amountReceivedLD) { if (_to == address(0x0)) _to = address(0xdead); // _mint(...) does not support address(0x0) // @dev Default OFT mints on dst. _mint(_to, _amountLD); // @dev In the case of NON-default OFT, the _amountLD MIGHT not be == amountReceivedLD. return _amountLD; } ``` ### Setting Delegates In an OFT, a delegate can be assigned to implement custom configurations on behalf of the contract owner. This delegate gains the ability to handle various critical tasks such as setting configurations and skipping inbound packets for the OFT. By default, the contract owner is set as the delegate. The `setDelegate` function allows for changing this, but you should generally keep the contract owner as delegate. ```solidity function setDelegate(address _delegate) public onlyOwner { endpoint.setDelegate(_delegate); } ``` For instructions on how to implement custom configurations after setting your delegate, refer to the [OApp Configuration](../configuration/dvn-executor-config.md). ### Security and Governance Given the impact associated with deployment, configuration, and debugging functions, OFT owners may want to add additional security measures in place to call core contract functions instead of `onlyOwner`, such as: - **Governance Controls**: Implementing a governance mechanism where decisions to clear messages are voted upon by stakeholders. - **Multisig Deployment**: Deploying with a multisig wallet, preventing arbitrary actions by any one team member. - **Timelocks**: Using a timelock to delay the execution of the clear function, giving stakeholders time to react if the function is called inappropriately. :::info Any normal access control library can be added to the base OFT Standard. The only relevant difference is that these access controls will need to coordinate across multiple contract implementations, since a deployed OFT typically consists of an OFT contract on every connected chain. ::: ## Deployment & Usage You can now deploy your contracts and get one step closer to moving fungible tokens between chains. ### Setting Trusted Peers You should only connect your OFT deployments together after setting your DVN and Executor configuration (see the [Configuration Guide](../configuration/dvn-executor-config.md) or [`create-lz-oapp` CLI tool](../create-lz-oapp/configuring-pathways.md)). Once you've finished configuring your OFT, you can connect your OFT deployment to different chains by calling `setPeer`. The function takes 2 arguments: `_eid`, the endpoint ID for the destination chain that the other OFT contract lives on, and `_peer`, the destination OFT's contract address in `bytes32` format. ```solidity // @dev must-have configurations for standard OApps function setPeer(uint32 _eid, bytes32 _peer) public virtual onlyOwner { peers[_eid] = _peer; // Array of peer addresses by destination. emit PeerSet(_eid, _peer); // Event emitted each time a peer is set. } ``` :::caution `setPeer` opens your OFT to start receiving messages from the messaging channel, meaning you should configure any application settings you intend on changing prior to calling `setPeer`. :::

:::warning OFTs 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 burn source funds without a corresponding mint on destination. You can confirm the peer address is the expected destination OFT address by using the `isPeer` function. :::

The [LayerZero Endpoint](../../../concepts/protocol/layerzero-endpoint.md) will use this peer as the destination address when sending the cross-chain message: ```solidity // @dev the endpoint send method called by _lzSend endpoint.send{ value: messageValue }( MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0), _refundAddress ); ``` The destination Endpoint will check if the `_receiver` matches the OFT contract's expected peer before delivering the message on the destination chain: ```solidity function _initializable( Origin calldata _origin, address _receiver, uint64 _lazyInboundNonce ) internal view returns (bool) { return _lazyInboundNonce > 0 || // allowInitializePath already checked ILayerZeroReceiver(_receiver).allowInitializePath(_origin); } ``` To see if an address is the trusted peer you expect for a destination, you can read the `peers` mapping directly: ```solidity /** * @dev Internal function to check if peer is considered 'trusted' by the OApp. * @param _eid The endpoint ID to check. * @param _peer The peer to check. * @return Whether the peer passed is considered 'trusted' by the OApp. * * @dev Enables OAppPreCrimeSimulator to check whether a potential Inbound Packet is from a trusted source. */ function isPeer(uint32 _eid, bytes32 _peer) public view virtual override returns (bool) { return peers[_eid] == _peer; } ``` This can be useful for confirming whether `setPeer` has been called correctly and as expected. ### Message Execution Options `_options` are a generated bytes array with specific instructions for the [DVNs](../../../concepts/modular-security/security-stack-dvns.md) and [Executor](../../../concepts/permissionless-execution/executors.md) to use when handling the authentication and execution of received messages. You can find how to generate all the available `_options` in [Message Execution Options](../configuration/options.md), but for this tutorial we'll focus on how options work with OFT. - `ExecutorLzReceiveOption`: instructions for how much gas the Executor should use when calling `lzReceive` on the destination Endpoint. For example, usually to send a vanilla OFT to a destination chain you will need `60000` wei in native gas on destination. The options will look like the following: ```solidity _options = 0x0003010011010000000000000000000000000000ea60; ``` :::tip `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 Enforced Options Once you determine ideal message `_options`, you will want to make sure users adhere to it. In the case of OFT, you mostly want to make sure the gas is enough for transferring the ERC20 token, plus any additional logic. A typical OFT's `lzReceive` call 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: ```solidity _options = 0x0003010011010000000000000000000000000000ea60; ``` :::tip You can use the [**`create-lz-oapp`**](/v2/developers/evm/create-lz-oapp/configuring-pathways#adding-enforcedoptions) npx package to set `enforcedOptions` in a human readable format by defining your settings in your `layerzero.config.ts`. ::: The `setEnforcedOptions` function allows the contract owner to specify mandatory execution options, making sure that the application behaves as expected when users interact with it. ```solidity // inherited from `oapp/libs/OAppOptionsType3.sol`: /** * @dev Sets the enforced options for specific endpoint and message type combinations. * @param _enforcedOptions An array of EnforcedOptionParam structures specifying enforced options. * * @dev Only the owner/admin of the OApp can call this function. * @dev Provides a way for the OApp to enforce things like paying for PreCrime, AND/OR minimum dst lzReceive gas amounts etc. * @dev These enforced options can vary as the potential options/execution on the remote may differ as per the msgType. * eg. Amount of lzReceive() gas necessary to deliver a lzCompose() message adds overhead you dont want to pay * if you are only making a standard LayerZero message ie. lzReceive() WITHOUT sendCompose(). */ function setEnforcedOptions(EnforcedOptionParam[] calldata _enforcedOptions) public virtual onlyOwner { _setEnforcedOptions(_enforcedOptions); } function _setEnforcedOptions(EnforcedOptionParam[] memory _enforcedOptions) internal virtual { for (uint256 i = 0; i < _enforcedOptions.length; i++) { // @dev Enforced options are only available for optionType 3, as type 1 and 2 dont support combining. _assertOptionsType3(_enforcedOptions[i].options); enforcedOptions[_enforcedOptions[i].eid][_enforcedOptions[i].msgType] = _enforcedOptions[i].options; } emit EnforcedOptionSet(_enforcedOptions); } ``` To use `setEnforcedOptions`, we only need to pass one parameter: - `EnforcedOptionParam[]`: a struct specifying the execution options per message type and destination chain. ```solidity struct EnforcedOptionParam { uint32 eid; // destination endpoint id uint16 msgType; // the message type bytes options; // the execution option bytes array } ``` The OFT Standard only has handling for 2 message types: ```solidity // @dev execution types to handle different enforcedOptions uint16 internal constant SEND = 1; // a standard token transfer via send() uint16 internal constant SEND_AND_CALL = 2; // a composed token transfer via send() ``` Pass these values in when specifying the `msgType` for your `_options`. For best practice, generate this array off-chain and pass it as a parameter when configuring your OFT: ```solidity EnforcedOptionParam[] memory aEnforcedOptions = new EnforcedOptionParam[](1); // Send gas for lzReceive (A -> B). aEnforcedOptions[0] = EnforcedOptionParam({eid: bEid, msgType: SEND, options: OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0)}); // Call the setEnforcedOptions function aOFT.setEnforcedOptions(aEnforcedOptions); ``` :::caution When setting `enforcedOptions`, try not to unintentionally pass a duplicate `_options` argument to `extraOptions`. Passing identical `_options` in both `enforcedOptions` and `extraOptions` will cause the protocol to charge the caller twice on the source chain, because LayerZero interprets duplicate `_options` as two separate requests for gas. ::: #### Setting Extra Options Any `_options` passed in the `send` call itself should be considered `_extraOptions`. `_extraOptions` can specify additional handling within the same message type. These `_options` will then be combined with `enforcedOption` if set. If not needed in your application, you should pass an empty bytes array `0x`. ```solidity if (enforced.length > 0) { // combine extra options with enforced options // remove the first 2 bytes (TYPE_3) of extra options // should pack executor options last in enforced options (assuming most extra options are executor options only) // to save gas on grouping by worker id in message library uint16 extraOptionsType = uint16(bytes2(_extraOptions[0:2])); uint16 enforcedOptionsType = (uint16(uint8(enforced[0])) << 8) + uint8(enforced[1]); if (extraOptionsType != enforcedOptionsType) revert InvalidOptions(); options = bytes.concat(enforced, _extraOptions[2:]); } else { // no enforced options, use extra options directly options = _extraOptions; } ``` :::caution 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. ::: ### Estimating Gas Fees Now let's get an estimate of how much gas a transfer will cost to be sent and received. To do this we can call the `quoteSend` function to return an estimate from the Endpoint contract to use as a recommended `msg.value`. Arguments of the estimate function: 1. `SendParam`: what parameters should be used for the send call? ```solidity /** * @dev Struct representing token parameters for the OFT send() operation. */ struct SendParam { uint32 dstEid; // Destination endpoint ID. bytes32 to; // Recipient address. uint256 amountLD; // Amount to send in local decimals. uint256 minAmountLD; // Minimum amount to send in local decimals. 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 oftCmd; // The OFT command to be executed, unused in default OFT implementations. } ``` :::note Here is a link to further explain [Extra Message Options](#setting-extra-options) that would be used besides [`enforcedOptions`](#setting-enforced-options). ::: 2. `_payInLzToken`: what token will be used to pay for the transaction? ```solidity struct MessagingFee { uint nativeFee; // gas amount in native gas token uint lzTokenFee; // gas amount in ZRO token } ``` ```solidity /** * @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. * * @dev MessagingFee: LayerZero msg fee * - nativeFee: The native fee. * - lzTokenFee: The lzToken fee. */ function quoteSend( SendParam calldata _sendParam, bool _payInLzToken ) external view virtual returns (MessagingFee memory msgFee) { // @dev mock the amount to receive, this is the same operation used in the send(). // The quote is as similar as possible to the actual send() operation. (, uint256 amountReceivedLD) = _debitView(_sendParam.amountLD, _sendParam.minAmountLD, _sendParam.dstEid); // @dev Builds the options and OFT message to quote in the endpoint. (bytes memory message, bytes memory options) = _buildMsgAndOptions(_sendParam, amountReceivedLD); // @dev Calculates the LayerZero fee for the send() operation. return _quote(_sendParam.dstEid, message, options, _payInLzToken); } ``` ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` Below you can find the send method itself. ```solidity // @dev executes a cross-chain OFT swap via layerZero Endpoint function send( SendParam calldata _sendParam, MessagingFee calldata _fee, address _refundAddress ) external payable virtual returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) { // @dev Applies the token transfers regarding this send() operation. // - amountSentLD is the amount in local decimals that was ACTUALLY sent/debited from the sender. // - amountReceivedLD is the amount in local decimals that will be received/credited to the recipient on the remote OFT instance. (uint256 amountSentLD, uint256 amountReceivedLD) = _debit( msg.sender, _sendParam.amountLD, _sendParam.minAmountLD, _sendParam.dstEid ); // @dev Builds the options and OFT message to quote in the endpoint. (bytes memory message, bytes memory options) = _buildMsgAndOptions(_sendParam, amountReceivedLD); // @dev Sends the message to the LayerZero endpoint and returns the LayerZero msg receipt. msgReceipt = _lzSend(_sendParam.dstEid, message, options, _fee, _refundAddress); // @dev Formulate the OFT receipt. oftReceipt = OFTReceipt(amountSentLD, amountReceivedLD); emit OFTSent(msgReceipt.guid, _sendParam.dstEid, msg.sender, amountSentLD, amountReceivedLD); } ``` To do this, we only need to pass `send` a few inputs: 1. `SendParam`: what parameters should be used for the send call? ```solidity struct SendParam { uint32 dstEid; // Destination endpoint ID. bytes32 to; // Recipient address. uint256 amountLD; // Amount to send in local decimals. uint256 minAmountLD; // Minimum amount to send in local decimals. 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 oftCmd; // The OFT command to be executed, unused in default OFT implementations. } ``` :::info `extraOptions` allow a caller to define an additional amount of `gas_limit` and `msg.value` to deliver to the destination chain along with the required amount set by the contract owner (`enforcedOptions`). :::

2. `_fee`: what token will be used to pay for the transaction? ```solidity struct MessagingFee { uint nativeFee; // gas amount in native gas token uint lzTokenFee; // gas amount in ZRO token } ``` 3. `_refundAddress`: If the transaction fails on the source chain, where should funds be refunded? #### Optional: `_composedMsg` When sending an OFT, you can also include an optional `_composedMsg` parameter in the transaction to execute additional logic on the destination chain as part of the token transfer. ```solidity // @dev executes an omnichain OFT swap via layerZero Endpoint if (_composeMsg.length > 0) { // @dev Remote chains will want to know the composed function caller. // ALSO, the presence of a composeFrom msg.sender inside of the bytes array indicates the payload should // be composed. ie. this allows users to compose with an empty payload, vs it must be length > 0 _composeMsg = abi.encodePacked(OFTMsgCodec.addressToBytes32(msg.sender), _composeMsg); } msgReceipt = _sendInternal( _send, combineOptions(_send.dstEid, SEND_AND_CALL, _extraOptions), _msgFee, // message fee _refundAddress, // refund address for failed source tx _composeMsg // composed message ); ``` On the destination chain, the `_lzReceive` function will first process the token transfer, crediting the recipient's account with the specified amount, and then check if `_message.isComposed()`. ```solidity if (_message.isComposed()) { bytes memory composeMsg = OFTComposeMsgCodec.encode( _origin.nonce, // nonce of the origin transaction _origin.srcEid, // source endpoint id of the transaction amountLDReceive, // the token amount in local decimals to credit _message.composeMsg() // the composed message ); // @dev Stores the lzCompose payload that will be executed in a separate tx. // standardizes functionality for delivering/executing arbitrary contract invocation on some non evm chains. // @dev Composed toAddress is the same as the receiver of the oft/tokens endpoint.deliverComposedMessage(toAddress, _guid, composeMsg); } ``` If the message is composed, the contract retrieves and re-encodes the additional composed message information, then delivers the message to the endpoint, which will execute the additional logic as a separate transaction. #### Optional: `_oftCmd` The `_oftCmd` is a `bytes` array that can be used like a function selector on the destination chain that you can check for within `_lzReceive` similar to `lzCompose` for custom OFT implementations. ### `_lzReceive` tokens A successful `send` call will be delivered to the destination chain, invoking the provided `_lzReceive` method during execution: ```solidity function _lzReceive( Origin calldata _origin, bytes32 _guid, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) internal virtual override { // @dev sendTo is always a bytes32 as the remote chain initiating the call doesnt know remote chain address size address toAddress = _message.sendTo().bytes32ToAddress(); uint256 amountToCreditLD = _toLD(_message.amountSD()); uint256 amountReceivedLD = _credit(toAddress, amountToCreditLD, _origin.srcEid); if (_message.isComposed()) { bytes memory composeMsg = OFTComposeMsgCodec.encode( _origin.nonce, _origin.srcEid, amountReceivedLD, _message.composeMsg() ); // @dev Stores the lzCompose payload that will be executed in a separate tx. // standardizes functionality for executing arbitrary contract invocation on some non-evm chains. // @dev Composed toAddress is the same as the receiver of the oft/tokens // TODO need to document the index / understand how to use it properly endpoint.sendCompose(toAddress, _guid, 0, composeMsg); } emit OFTReceived(_guid, toAddress, amountToCreditLD, amountReceivedLD); } ``` #### `_credit`: When receiving the message on your destination contract, `_credit` is invoked, triggering the final steps to mint an ERC20 token on the destination to the specified address. ```solidity function _credit( address _to, uint256 _amountToCreditLD, uint32 /*_srcEid*/ ) internal virtual override returns (uint256 amountReceivedLD) { _mint(_to, _amountToCreditLD); return _amountToCreditLD; } ``` --- --- title: LayerZero V2 ONFT Quickstart sidebar_label: Omnichain NFT (ONFT) --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; The **Omnichain Non-Fungible Token (ONFT) Standard** allows **non-fungible tokens (NFTs)** to be transferred across multiple blockchains without asset wrapping or middlechains. ## ONFT Standard Overview - **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) ![ONFT Example](/img/learn/oft_mechanism_light.jpg#gh-light-mode-only) ![ONFT Example](/img/learn/oft_mechanism.jpg#gh-dark-mode-only) 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 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 cross-chain logic. - Unified supply across chains is maintained by burning on source, minting on destination. ### ONFT Adapter (Lock & Mint) ![ONFT Adapter Example](/img/learn/ONFTAdapterLight.svg#gh-light-mode-only) ![ONFT Adapter Example](/img/learn/ONFTAdapterDark.svg#gh-dark-mode-only) 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 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 cross-chain logic. ## ONFT Standard Overview ```solidity // 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 cross-chain 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 // 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 cross-chain 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 cross-chain 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`](../create-lz-oapp/start.md), 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 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 npm install @layerzerolabs/onft-evm ``` ```bash yarn add @layerzerolabs/onft-evm ``` ```bash pnpm add @layerzerolabs/onft-evm ``` ```bash forge init ``` ```bash forge install https://github.com/LayerZero-Labs/devtools ``` ```bash forge install https://github.com/LayerZero-Labs/layerzero-v2 ``` ```bash forge install OpenZeppelin/openzeppelin-contracts@v5.1.0 ``` Then add to your `foundry.toml` under `[profile.default]`: ```toml [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', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', ] # 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 "resolutions": { "@openzeppelin/contracts": "^5.0.1", } ``` ## Constructing an ONFT Contract 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 cross-chain compatible. ### ONFT721 Implementation Deploy an **ONFT** that inherits from `ONFT721`, which combines `ERC721` with the cross-chain 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 // 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 cross-chain compatible. ```solidity // 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 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 Set the DVN configuration, including block confirmations, security thresholds, executor settings, and messaging libraries: ```solidity 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.md). ### 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 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.md) 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 // 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.md) 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 cross-chain. 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 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 // @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 cross-chain: - `SendParam` struct with all the parameters needed to send the NFT cross-chain - `quoteSend` function to estimate the fee before sending the NFT cross-chain - `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 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 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 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 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 cross-chain 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 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 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 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 npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` 5. **Verify Setup** Verify that everything was wired up correctly: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` Verify configurations: ```bash 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 DVNs in production to ensure message verification is robust. 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 cross-chain 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.md) - [Security and Executor Configuration](../configuration/dvn-executor-config.md) - [Message Execution Options](../configuration/options.md) - [LayerZero Endpoint Addresses](../../../deployments/deployed-contracts.md) **You’re ready to build omnichain NFTs!** --- --- title: Omnichain Composability sidebar_label: Omnichain Composability description: Learn how to implement a composer contract to chain multiple cross-chain calls together. --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Cross-chain 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 cross-chain 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 cross-chain 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 // 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. :::caution Vertical composability can present potential **Atomicity Issues** in cross-chain 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 cross-chain messages. In cross-chain 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 cross-chain 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 cross-chain 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. :::info 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 cross-chain 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 cross-chain 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 cross-chain `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 npm install @layerzerolabs/oapp-evm ``` ```bash yarn add @layerzerolabs/oapp-evm ``` ```bash pnpm add @layerzerolabs/oapp-evm ``` ```bash forge install https://github.com/LayerZero-Labs/devtools ``` ```bash forge install https://github.com/LayerZero-Labs/layerzero-v2 ``` ```bash forge install OpenZeppelin/openzeppelin-contracts@v5.1.0 ``` Then add to your `foundry.toml` under `[profile.default]`: ```toml [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', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', ] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options ``` :::info 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 "resolutions": { "@openzeppelin/contracts": "^5.0.1", } ``` ::: ## Usage To implement a `composer` contract, simply inherit the `IOAppComposer.sol` interface from the `oapp-evm` package: ```solidity // 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 cross-chain 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 // 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 // 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.md) to learn more. :::caution 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 cross-chain token transfers. ```solidity // 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 // 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` contract encodes the `composeMsg` and sends it to the `composer`: ```solidity // 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 // 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. For example, see the following `composer` example which mocks an ERC20 token swap after receiving from an OFT: ```solidity // 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 cross-chain message via `_lzSend()` to a destination chain. 2. **Destination OApp:** Receives the cross-chain message via `_lzReceive()` and initiates composed calls using `EndpointV2.sendCompose()`: ```solidity /** * @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()`. --- --- title: Design Patterns & Extensions sidebar_label: OApp Patterns & Extensions --- Each design pattern functions as a distinct omnichain building block, capable of being used independently or in conjunction with others. | Message Pattern | Description | | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | | [ABA](#aba) | a nested send call from Chain A to Chain B that sends back again to the source chain (`A` -> `B` -> `A`) | | [Batch Send](#batch-send) | a single send that calls multiple destination chains | | [Composed](#composed) | a message that transfers from a source to destination chain and calls an external contract (`A` -> `B1` -> `B2`) | | [Composed ABA](#composed-aba) | transfers data from a source to destination, calls an external contract, and then calls back to the source (`A` -> `B1` -> `B2` -> `A`) | | [Message Ordering](#message-ordering) | enforce the ordered delivery of messages on execution post verification | | [Rate Limit](#rate-limiting) | rate limit the number of `send` calls for a given amount of `messages` or `tokens` transferred |

This modularity allows for the seamless integration and combination of patterns to suit specific developer requirements. ## ABA **AB** messaging refers to a one way send call from a source to destination blockchain. ![OFT Example](/img/learn/ABLight.svg#gh-light-mode-only) ![OFT Example](/img/learn/ABDark.svg#gh-dark-mode-only) In the [Getting Started Guide](../getting-started.md), we use this design pattern to send a string from Chain A to store on Chain B (`A` -> `B`). The **ABA** message pattern extends this simple logic by nesting another `_lzSend` call within the destination contract's `_lzReceive` function. You can think of this as a ping-pong style call, pinging a destination chain and ponging back to the original source (`A` -> `B` -> `A`). ![ABA Light](/img/learn/ABAlight.svg#gh-light-mode-only) ![ABA Dark](/img/learn/ABAdark.svg#gh-dark-mode-only)

This is particularly useful when actions on one blockchain depend on the state or confirmation of another, such as: - **Conditional Execution of Contracts**: A smart contract on chain A will only execute a function if a condition on chain B is met. It sends a message to chain B to check the condition and then receives a confirmation back before proceeding. - **Omnichain Data Feeds**: A contract on Chain A can fetch data from the destination (Chain B) to complete a process back on the source. - **Cross-chain Authentication**: A user or contract might authenticate on chain A, ping chain B to process something that requires this authentication, and then receive back a token or confirmation that the process was successful. ### Code Example This pattern demonstrates **vertical composability**, where the nested message contains both handling for the message receipt, as well as additional logic for a subsequent call that must all happen within one atomic transaction. ```solidity // 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 ABA contract for demonstrating LayerZero messaging between blockchains. * @notice THIS IS AN EXAMPLE CONTRACT. DO NOT USE THIS CODE IN PRODUCTION. * @dev This contract showcases a PingPong style call (A -> B -> A) using LayerZero's OApp Standard. */ contract ABA 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. uint16 public constant SEND = 1; uint16 public constant SEND_ABA = 2; /// @notice Emitted when a return message is successfully sent (B -> A). event ReturnMessageSent(string message, uint32 dstEid); /// @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 PingPong 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) {} function encodeMessage(string memory _message, uint16 _msgType, bytes memory _extraReturnOptions) public pure returns (bytes memory) { // Get the length of _extraReturnOptions uint256 extraOptionsLength = _extraReturnOptions.length; // Encode the entire message, prepend and append the length of extraReturnOptions return abi.encode(_message, _msgType, extraOptionsLength, _extraReturnOptions, extraOptionsLength); } /** * @notice Returns the estimated messaging fee for a given message. * @param _dstEid Destination endpoint ID where the message will be sent. * @param _msgType The type of message being sent. * @param _message The message content. * @param _extraSendOptions Gas options for receiving the send call (A -> B). * @param _extraReturnOptions Additional gas options for the return call (B -> A). * @param _payInLzToken Boolean flag indicating whether to pay in LZ token. * @return fee The estimated messaging fee. */ function quote( uint32 _dstEid, uint16 _msgType, string memory _message, bytes calldata _extraSendOptions, bytes calldata _extraReturnOptions, bool _payInLzToken ) public view returns (MessagingFee memory fee) { bytes memory payload = encodeMessage(_message, _msgType, _extraReturnOptions); bytes memory options = combineOptions(_dstEid, _msgType, _extraSendOptions); fee = _quote(_dstEid, payload, options, _payInLzToken); } /** * @notice Sends a message to a specified destination chain. * @param _dstEid Destination endpoint ID for the message. * @param _msgType The type of message to send. * @param _message The message content. * @param _extraSendOptions Options for sending the message, such as gas settings. * @param _extraReturnOptions Additional options for the return message. */ function send( uint32 _dstEid, uint16 _msgType, string memory _message, bytes calldata _extraSendOptions, // gas settings for A -> B bytes calldata _extraReturnOptions // gas settings for B -> A ) external payable { // Encodes the message before invoking _lzSend. require(bytes(_message).length <= 32, "String exceeds 32 bytes"); if (_msgType != SEND && _msgType != SEND_ABA) { revert InvalidMsgType(); } bytes memory options = combineOptions(_dstEid, _msgType, _extraSendOptions); _lzSend( _dstEid, encodeMessage(_message, _msgType, _extraReturnOptions), options, // Fee in native gas and ZRO token. MessagingFee(msg.value, 0), // Refund address in case of failed source message. payable(msg.sender) ); emit MessageSent(_message, _dstEid); } function decodeMessage(bytes calldata encodedMessage) public pure returns (string memory message, uint16 msgType, uint256 extraOptionsStart, uint256 extraOptionsLength) { extraOptionsStart = 256; // Starting offset after _message, _msgType, and extraOptionsLength string memory _message; uint16 _msgType; // Decode the first part of the message (_message, _msgType, extraOptionsLength) = abi.decode(encodedMessage, (string, uint16, uint256)); return (_message, _msgType, extraOptionsStart, extraOptionsLength); } /** * @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, uint16 _msgType, uint256 extraOptionsStart, uint256 extraOptionsLength) = decodeMessage(message); data = _data; if (_msgType == SEND_ABA) { string memory _newMessage = "Chain B says goodbye!"; bytes memory _options = combineOptions(_origin.srcEid, SEND, message[extraOptionsStart:extraOptionsStart + extraOptionsLength]); _lzSend( _origin.srcEid, abi.encode(_newMessage, SEND), // Future additions should make the data types static so that it is easier to find the array locations. _options, // Fee in native gas and ZRO token. MessagingFee(msg.value, 0), // Refund address in case of failed send call. // @dev Since the Executor makes the return call, this contract is the refund address. payable(address(this)) ); emit ReturnMessageSent(_newMessage, _origin.srcEid); } emit MessageReceived(data, _origin.srcEid, _origin.sender); } receive() external payable {} } ``` :::info This message pattern can also be considered an ABC type call (`A` -> `B` -> `C`), as the nested `_lzSend` can send to any new destination chain. ::: ## Batch Send The **Batch Send** design pattern, where a single transaction can initiate multiple `_lzSend` calls to various destination chains, is highly efficient for operations that need to propagate an action across several blockchains simultaneously. ![Batch Send Light](/img/learn/BatchSendLight.svg#gh-light-mode-only) ![Batch Send Dark](/img/learn/BatchSendDark.svg#gh-dark-mode-only) This can significantly reduce the operational overhead associated with performing the same action multiple times on different blockchains. It streamlines omnichain interactions by bundling them into a single transaction, making processes more efficient and easier to manage for example: - **Simultaneous Omnichain Updates**: When a system needs to update the same information across multiple chains (such as a change in governance parameters or updating oracle data), Batch Send can propagate the updates in one go. - **DeFi Strategies**: For DeFi protocols that operate on multiple chains, rebalancing liquidity pools or executing yield farming strategies can be done in batch to maintain parity across ecosystems. - **Aggregated Omnichain Data Posting**: Oracles or data providers that supply information to smart contracts on multiple chains can use Batch Send to post data such as price feeds, event outcomes, or other updates in a single transaction. ### Code Example ```solidity // 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 BatchSend contract for demonstrating multiple outbound cross-chain calls using LayerZero. * @notice THIS IS AN EXAMPLE CONTRACT. DO NOT USE THIS CODE IN PRODUCTION. * @dev This contract showcases how to send multiple cross-chain calls with one source function call using LayerZero's OApp Standard. */ contract BatchSend 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) {} 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, bytes calldata _extraSendOptions, bool _payInLzToken ) public view returns (MessagingFee memory totalFee) { bytes memory encodedMessage = abi.encode(_message); 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 { if (_msgType != SEND) { revert InvalidMsgType(); } // Calculate the total messaging fee required. 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; // Ensure the current call has enough allocated fee from msg.value. require(remainingValue >= 0, "Insufficient fee for this destination"); _lzSend( _dstEids[i], _encodedMessage, options, fee, payable(msg.sender) ); emit MessageSent(_message, _dstEids[i]); } } /** * @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); } } ``` ## Composed A composed message refers to an application that invokes the Endpoint method, `sendCompose`, to deliver a composed call to a destination contract via `lzCompose`. ![Composed Light](/img/learn/Composed-Light.svg#gh-light-mode-only) ![Composed Dark](/img/learn/Composed-Dark.svg#gh-dark-mode-only) This pattern demonstrates **horizontal composability**, which differs from vertical composability in that the external call is now containerized as a new message packet; enabling the application to ensure that certain receipt logic remains separate from the external call itself. :::info Since each composable call is created as a separate message packet via `lzCompose`, this pattern can be extended for as many steps as your application needs (`B1` -> `B2` -> `B3`, etc). :::

This pattern can be particularly powerful for orchestrating complex interactions and processes on the destination chain that need contract logic to be handled in independent steps, such as: - **Omnichain DeFi Strategies**: A smart contract could trigger a token transfer on the destination chain and then automatically interact with a DeFi protocol to lend, borrow, provide liquidity, stake, etc. executing a series of financial strategies across chains. - **NFT Interactions**: An NFT could be transferred to another chain, and upon arrival, it could trigger a contract to issue a license, register a domain, or initiate a subscription service linked to the NFT's ownership. - **DAO Coordination**: A DAO could send funds to another chain's contract and compose a message to execute specific DAO-agreed upon investments or funding of public goods.

### Composing an OApp There are 3 relevant contract interactions when composing an OApp: 1. **Source OApp**: the OApp sending a cross-chain message via `_lzSend` to a destination. 2. **Destination OApp(s)**: the OApp receiving a cross-chain message via `_lzReceive` and calling `sendCompose`. 3. **Composed Receiver(s)**: the contract interface implementing business logic to handle receiving a composed message via `lzCompose`. ### Sending Message The sending OApp is **required** to pass specific [Composed Message Execution Options](#composed-message-execution-options) (more on this below) for the `sendCompose` call, but is **not required** to pass any input parameters for the call itself (however this pattern may be useful depending on what arbitrary action you wish to trigger when composing). For example, this `send` function packs the destination `_composedAddress` for the destination OApp to decode and use for the actual composed call. ```solidity /// @notice Sends a message from the source to destination chain. /// @param _dstEid Destination chain's endpoint ID. /// @param _message The message to send. /// @param _composedAddress The contract you wish to deliver a composed call to. /// @param _options Message execution options (e.g., for sending gas to destination). function send( uint32 _dstEid, string memory _message, address _composedAddress, // the destination contract implementing ILayerZeroComposer bytes calldata _options ) external payable returns(MessagingReceipt memory receipt) { // Encodes the message before invoking _lzSend. bytes memory _payload = abi.encode(_message, _composedAddress); receipt = _lzSend( _dstEid, _payload, _options, // Fee in native gas and ZRO token. MessagingFee(msg.value, 0), // Refund address in case of failed source message. payable(msg.sender) ); } ``` ### Sending Compose The receiving OApp invokes the LayerZero Endpoint's `sendCompose` method as part of your OApp's `_lzReceive` business logic. The `sendCompose` method takes the following inputs: 1. `_to`: the contract implementing the [`ILayerZeroComposer`](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/protocol/contracts/interfaces/ILayerZeroComposer.sol) receive interface. 2. `_guid`: the global unique identifier of the source message (provided standard by `lzReceive`). 3. `_index`: the index of the composed message (used for pricing different gas execution amounts along different composed legs of the transaction). ```solidity /// @dev the Oapp sends the lzCompose message to the endpoint /// @dev the composer MUST assert the sender because anyone can send compose msg with this function /// @dev with the same GUID, the Oapp can send compose to multiple _composer at the same time /// @dev authenticated by the msg.sender /// @param _to the address which will receive the composed message /// @param _guid the message guid /// @param _message the message function sendCompose(address _to, bytes32 _guid, uint16 _index, bytes calldata _message) external { // must have not been sent before if (composeQueue[msg.sender][_to][_guid][_index] != NO_MESSAGE_HASH) revert Errors.ComposeExists(); composeQueue[msg.sender][_to][_guid][_index] = keccak256(_message); emit ComposeSent(msg.sender, _to, _guid, _index, _message); } ``` This means that when a packet is received (`_lzReceive`) by the Destination OApp, it will send (`sendCompose`) a new composed packet via the destination LayerZero Endpoint. ```solidity /// @dev Called when data is received from the protocol. It overrides the equivalent function in the parent contract. /// Protocol messages are defined as packets, comprised of the following parameters. /// @param _origin A struct containing information about where the packet came from. /// @param _guid A global unique identifier for tracking the packet. /// @param payload Encoded message. function _lzReceive( Origin calldata _origin, bytes32 _guid, bytes calldata payload, address, // Executor address as specified by the OApp. bytes calldata // Any extra data or options to trigger on receipt. ) internal override { // Decode the payload to get the message (string memory _message, address _composedAddress) = abi.decode(payload, (string, address)); // Storing data in the destination OApp data = _message; // Send a composed message[0] to a composed receiver endpoint.sendCompose(_composedAddress, _guid, 0, payload); } ``` :::info The above `sendCompose` call hardcodes `_index` to `0` and simply forwards the same `payload` as `_lzReceive` to `lzCompose`, however these inputs can also be dynamically adjusted depending on the number and type of composed calls you wish to make. ::: #### Composed Message Execution Options You can decide both the `_gas` and `msg.value` that should be used for the composed call(s), depending on the type and quantity of messages you intend to send. Your configured Executor will use the `_options` provided in the original `_lzSend` call to determine the gas limit and amount of `msg.value` to include per message `_index`: ```javascript // addExecutorLzComposeOption(uint16 _index, uint128 _gas, uint128 _value) Options.newOptions() .addExecutorLzReceiveOption(50000, 0) .addExecutorComposeOption(0, 30000, 0) .addExecutorComposeOption(1, 30000, 0); ``` It's important to remember that gas costs may vary depending on the destination chain. For example, all new Ethereum transactions cost `21000` wei, but other chains may have lower or higher opcode costs, or entirely different gas mechanisms. You can read more about generating `_options` and the role of `_index` in [Message Execution Options](../configuration/options.md#lzcompose-option). ### Receiving Compose The destination must implement the `ILayerZeroComposer` interface to handle receiving the composed message. From there, you can decide any additional composed business logic to execute within `lzCompose`, as shown below: ```solidity // 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. require(_oApp == oApp, "!oApp"); require(msg.sender == endpoint, "!endpoint"); // Decode the payload to get the message (string memory message, ) = abi.decode(_message, (string, address)); data = message; } } ``` ### Further Reading For more advanced implementations of `sendCompose` and `lzCompose`: - Review the [`OmniCounter.sol`](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/oapp/contracts/oapp/examples/OmniCounter.sol#L43) for sending composed messages to the same OApp implementation. - Read the [OFT Composing](../oft/oft-patterns-extensions.md#composed-oft) section to see how to implement composed business logic into your OFTs. ## Composed ABA The **Composed ABA** design pattern enables sophisticated omnichain communication by allowing for an operation to be performed as part of the receive logic on the destination chain (`B1`), a follow-up action or call containerized as an independent step within `lzCompose` (`B2`), which then sends back to the source chain (`A`). ![ComposedABA Light](/img/learn/ComposedABAlight.svg#gh-light-mode-only) ![ComposedABA Dark](/img/learn/ComposedABAdark.svg#gh-dark-mode-only) :::info This message pattern can also be considered a Composed ABC type call (`A` -> `B1` -> `B2` -> `C`), as the nested `_lzSend` can send to any new destination chain. :::

This pattern demonstrates a complex, multi-step, process across blockchains where each step requires its own atomic logic to execute without depending on separate execution logic. Here are some use cases that could benefit from a Composed ABA design pattern: - **Omnichain Data Verification**: Chain A sends a request to chain B to verify a set of data. Once verified, a contract on chain B executes an action based on this data and sends a signal back to chain A to either proceed with the next step or record the verification. - **Omnichain Collateral Management**: When collateral on chain A is locked or released, a corresponding contract on chain B could be called to issue a loan or unlock additional funds. Confirmation of the action is then sent back to chain A to complete the process. - **Multi-Step Contract Interaction for Games and Collectibles**: In a gaming scenario, an asset (like an NFT) could be sent from chain A to B, triggering a contract on B that could unlock a new level or feature in a game, with a confirmation or reward then sent back to chain A. ## Message Ordering LayerZero offers both [**unordered delivery**](#unordered-delivery) and [**ordered delivery**](#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. ![Lazy Nonce Enforcement Light](/img/learn/lazy-nonce-enforcement-light.svg#gh-light-mode-only) ![Lazy Nonce Enforcement Dark](/img/learn/lazy-nonce-enforcement-dark.svg#gh-dark-mode-only) 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**. ![Strict Nonce Enforcement Light](/img/learn/strict-nonce-enforcement-light.svg#gh-light-mode-only) ![Strict Nonce Enforcement Dark](/img/learn/strict-nonce-enforcement-dark.svg#gh-dark-mode-only) 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. ![Strict Nonce Enforcement Fail Light](/img/learn/strict-nonce-enforcement-fail-light.svg#gh-light-mode-only) ![Strict Nonce Enforcement Fail Dark](/img/learn/strict-nonce-enforcement-fail-dark.svg#gh-dark-mode-only) 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. :::info In these cases, strict nonce enforcement can be used to provide consistency, fairness, and censorship-resistance to maintain system integrity. ::: ### Code Example 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`. :::caution 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](../configuration/options.md) an `ExecutorOrderedExecutionOption` in your `_lzSend` call: ```solidity // appends "01000104", the ExecutorOrderedExecutionOption, to your options bytes array _options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0).addExecutorOrderedExecutionOption(); ``` Implement strict nonce enforcement via function override: ```solidity 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); // Call the internal function with the correct parameters super._lzReceive(_origin, _guid, _message, _executor, _extraData); } } ``` ## Rate Limiting The `RateLimiter.sol` is used to control the number of cross-chain messages that can be sent within a certain time window, ensuring that the OApp is not spammed by too many transactions at once. It's particularly useful for: - **Preventing Denial of Service Attacks**: By setting thresholds on the number of messages that can be processed within a given timeframe, the `RateLimiter` acts as a safeguard against DoS attacks, where malicious actors might attempt to overload an OApp with a flood of transactions. This protection ensures that the OApp remains accessible and functional for legitimate users, even under attempted attacks. - **Regulatory Compliance**: Some applications may need to enforce limits to comply with legal or regulatory requirements. The `RateLimiter` is only useful under specific application use cases. It will not be necessary for most OApps and can even be counterproductive for more generic applications: - **Low Traffic Applications**: If your application doesn't expect high volumes of traffic, implementing a rate limiter might be unnecessary overhead. - **Critical Systems Requiring Immediate Transactions**: For systems where transactions need to be processed immediately without delay, rate limiting could hinder performance. ### Installation To begin working with LayerZero contracts, you can install the [OApp npm package](https://www.npmjs.com/package/@layerzerolabs/oapp-evm?activeTab=code) to an existing project: ``` npm install @layerzerolabs/oapp-evm ``` ### Usage Import the `RateLimiter.sol` contract into your OApp contract file and inherit the contract: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { OApp } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import { RateLimiter } from "@layerzerolabs/oapp-evm/contracts/oapp/utils/RateLimiter.sol"; contract MyOApp is OApp, RateLimiter { // ...contract } ``` #### Initializing Rate Limits In the constructor of your contract, initialize the rate limits using `_setRateLimits` with an array of `RateLimitConfig` structs. **Example:** ```solidity constructor( RateLimitConfig[] memory _rateLimitConfigs, address _lzEndpoint, address _delegate ) OApp(_lzEndpoint, _delegate) { _setRateLimits(_rateLimitConfigs); } // ...Rest of contract code ``` **`RateLimitConfig` Struct:** ```solidity struct RateLimitConfig { uint32 dstEid; // destination endpoint ID uint256 limit; // arbitrary limit of messages/tokens to transfer uint256 window; // window of time before limit resets } ``` #### Setting Rate Limits Provide functions to set or update rate limits dynamically. This can include a function to adjust individual or multiple rate limits and a mechanism to authorize who can make these changes (typically restricted to the contract owner or a specific role). ```solidity /** * @dev Sets the rate limits based on RateLimitConfig array. Only callable by the owner or the rate limiter. * @param _rateLimitConfigs An array of RateLimitConfig structures defining the rate limits. */ function setRateLimits( RateLimitConfig[] calldata _rateLimitConfigs ) external { if (msg.sender != rateLimiter && msg.sender != owner()) revert OnlyRateLimiter(); _setRateLimits(_rateLimitConfigs); } ``` #### Checking Rate Limits During Send Calls Before processing transactions, use `_checkAndUpdateRateLimit` to ensure the transaction doesn't exceed the set limits. This function should be called in any transactional functions, such as message passing or token transfers. #### Message Passing ```solidity function send( uint32 _dstEid, string memory _message, bytes calldata _options ) external payable { // highlight-next-line _checkAndUpdateRateLimit(_dstEid, 1); // updating the rate limit per message sent bytes memory _payload = abi.encode(_message); // Encodes message as bytes. _lzSend( _dstEid, // Destination chain's endpoint ID. _payload, // Encoded message payload being sent. _options, // Message execution options (e.g., gas to use on destination). MessagingFee(msg.value, 0), // Fee struct containing native gas and ZRO token. payable(msg.sender) // The refund address in case the send call reverts. ); } ``` #### Token Transfers ```solidity /** * @dev Checks and updates the rate limit before initiating a token transfer. * @param _amountLD The amount of tokens to be transferred. * @param _minAmountLD The minimum amount of tokens expected to be received. * @param _dstEid The destination endpoint identifier. * @return amountSentLD The actual amount of tokens sent. * @return amountReceivedLD The actual amount of tokens received. */ function _debit( uint256 _amountLD, uint256 _minAmountLD, uint32 _dstEid ) internal virtual override returns (uint256 amountSentLD, uint256 amountReceivedLD) { // highlight-next-line _checkAndUpdateRateLimit(_dstEid, _amountLD); return super._debit(_amountLD, _minAmountLD, _dstEid); } ``` --- --- title: OFT Patterns and Extensions sidebar_label: OFT Patterns & Extensions --- The Omnichain Fungible Token (OFT) Standard can be extended to support several different use cases, similar to the ERC20 token standard. In addition to the [OApp Design Patterns and Extensions](../oapp/message-design-patterns.md), the following examples demonstrate how to modify your OFT contract for specific use cases. | Message Pattern | Description | | ----------------------------- | ---------------------------------------------------------------------------------------------------- | | [Composed OFT](#composed-oft) | a composed call made after the OFT delivers the token transfer | | [OFT Alt](#oft-alt) | a variant of the OFT standard that supports `EndpointV2Alt` for paying in an alternative ERC20 token | ## Composed OFT A composed message refers to an OApp that invokes the LayerZero Endpoint method `sendCompose` to deliver a composed call to another contract on the destination chain via `lzCompose`. Because OFT inherits the base OApp implementation, you can also send composed messages within your OFT receive logic. ![Composed Light](/img/learn/Composed-Light.svg#gh-light-mode-only) ![Composed Dark](/img/learn/Composed-Dark.svg#gh-dark-mode-only) If you are not familiar with how [OApp Composing](../oapp/message-design-patterns.md) works, review that section first before continuing here. ### Composing an OFT The OFT Standard comes pre-packaged with methods for delivering composed calls to the destination OFT contract for handling. 1. **Source OFT**: The Source OFT specifies in the `send` call a composed message in `bytes` for delivering `to`. You can think of this the same as how `_lzSend` sends arbitrary bytes to a destination, which the destination contract uses in the `_lzReceive` business logic. 2. **Destination OFT(s)**: When the send call is received by the destination OFT, the internal `_lzReceive` function in `OFTCore.sol` handles the delivery of tokens along with the composed call. 3. **Composed Receiver(s)**: the contract interface implementing business logic to handle receiving a composed message via `lzCompose`. ### Sending Token When sending a token from source to destination, the caller has the option to specify an additional `composeMsg` in bytes. ```solidity /** * @dev Struct representing token parameters for the OFT send() operation. */ struct SendParam { uint32 dstEid; // Destination endpoint ID. bytes32 to; // Recipient address. uint256 amountLD; // Amount to send in local decimals. uint256 minAmountLD; // Minimum amount to send in local decimals. 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 oftCmd; // The OFT command to be executed, unused in default OFT implementations. } function send( SendParam calldata _sendParam, MessagingFee calldata _fee, address _refundAddress ) external payable virtual returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) {} ``` Depending on your implementation, this composed message field can be used to pass any arbitrary information as bytes along with your token to the destination address. #### Composed Message Execution Options You will need to pass both an `lzReceiveOption` and `lzComposeOption` as either your [enforced](../oft/quickstart.md#setting-enforced-options) or extra options for this call to succeed. You can decide both the `_gas` and `msg.value` that should be used for the composed call(s), depending on the type and quantity of messages you intend to send. Your configured Executor will use the `_options` provided in the original `_lzSend` call to determine the gas limit and amount of `msg.value` to include per message `_index`: ```javascript // addExecutorLzComposeOption(uint16 _index, uint128 _gas, uint128 _value) Options.newOptions() .addExecutorLzReceiveOption(50000, 0) .addExecutorLzComposeOption(0, 30000, 0) .addExecutorLzComposeOption(1, 30000, 0); ``` It's important to remember that gas values may vary depending on the destination chain. For example, all new Ethereum transactions cost `21000` wei, but other chains may have lower or higher opcode costs, or entirely different gas mechanisms. You can read more about generating `_options` and the role of `_index` in [Message Execution Options](../configuration/options.md#lzcompose-option). ### Sending Compose By default, the destination OFT's `_lzReceive` method will check if the message is composed, and then deliver those arbitrary bytes to the specified `toAddress`: ```solidity // @dev Internal function to handle the receive on the LayerZero endpoint. if (_message.isComposed()) { // @dev Proprietary composeMsg format for the OFT. bytes memory composeMsg = OFTComposeMsgCodec.encode( _origin.nonce, _origin.srcEid, amountReceivedLD, _message.composeMsg() ); // @dev Stores the lzCompose payload that will be executed in a separate tx. // Standardizes functionality for executing arbitrary contract invocation on some non-evm chains. // @dev The off-chain executor will listen and process the msg based on the src-chain-callers compose options passed. // @dev The index is used when a OApp needs to compose multiple msgs on lzReceive. // For default OFT implementation there is only 1 compose msg per lzReceive, thus its always 0. endpoint.sendCompose(toAddress, _guid, 0 /* the index of the composed message*/, composeMsg); } ``` As shown in the `sendCompose` comments, the base OFT implementation only allows for 1 composed message per `lzReceive` call. To add additional composed calls, you will need to override the `_lzReceive` method and add custom composed logic. ### Receiving Compose The receiving address of the cross-chain token transfer will need to implement custom business logic to handle the composed message, for example, consider this mock contract that swaps an inbound OFT for an ERC20: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.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 /// @dev This contract mocks an ERC20 token swap in response to an OFT being received (lzReceive) on the destination chain. /// @notice The contract is designed to interact with LayerZero's Omnichain Fungible Token (OFT) Standard, /// allowing it to respond to cross-chain OFT mint events with a token swap action. contract SwapMock is IOAppComposer { using SafeERC20 for IERC20; IERC20 public erc20; address public immutable endpoint; address public immutable oApp; /// @notice Emitted when a token swap is executed. /// @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. /// @param _erc20 The address of the ERC20 token that will be used in swaps. /// @param _endpoint 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. /// @dev Decodes the message payload to perform a token swap. /// This method expects the encoded compose message to contain the swap amount and recipient address. /// @param _oApp The address of the originating OApp. /// @param /*_guid*/ The globally unique identifier of the message (unused in this mock). /// @param _message The encoded message content in the format of the OFTComposeMsgCodec. /// @param /*Executor*/ Executor address (unused in this mock). /// @param /*Executor Data*/ Additional data for checking for a specific executor (unused in this mock). function lzCompose( address _oApp, bytes32 /*_guid*/, bytes calldata _message, address /*Executor*/, bytes calldata /*Executor Data*/ ) external payable override { require(_oApp == oApp, "!oApp"); require(msg.sender == endpoint, "!endpoint"); // Extract the composed message from the delivered message using the MsgCodec address _receiver = abi.decode(OFTComposeMsgCodec.composeMsg(_message), (address)); uint256 _amountLD = OFTComposeMsgCodec.amountLD(_message); // Execute the token swap by transferring the specified amount to the receiver erc20.safeTransfer(_receiver, _amountLD); // Emit an event to log the token swap details emit Swapped(_receiver, address(erc20), _amountLD); } } ``` You will need to use the `OFTComposeMsgCodec` to extract the `composeMsg` and `_amountLD` from the overall message, before decoding it. :::tip The above example enforces that the `_amountLD` was deposited to this contract! The OFT Standard will only credit tokens and call `sendCompose` to the `_toAddress` provided on the source chain: ```solidity // @dev Credit the amountLD to the recipient and return the ACTUAL amount the recipient received in local decimals uint256 amountReceivedLD = _credit(toAddress, _toLD(_message.amountSD()), _origin.srcEid); endpoint.sendCompose(toAddress, _guid, 0 /* the index of the composed message*/, composeMsg); ``` ::: ### Further Reading - Review the [`OFT.sol`](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/oapp/contracts/oft/OFTCore.sol#L272) implementation and [unit tests](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/oapp/test/OFT.t.sol#L112) for handling composed messages. ## OFT Alt When deploying OApps, you might encounter scenarios where the native gas token cannot be used to pay the LayerZero Endpoint to send a message. These Endpoints have been deployed using the [`EndpointV2Alt.sol`](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/protocol/contracts/EndpointV2Alt.sol) contract, so that they can use an alternative ERC20 token on the same chain to pay for cross-chain messages. Because these Endpoints do not use the native gas token, some changes must be made to your OApp contracts (including OFT). For example, the `OFTAlt.sol` demonstrates this implementation fully, which you can reference when modifying your other OApp-based contracts: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { OFTAlt } from "../OFTAlt.sol"; contract MyOFT is OFTAlt { constructor( string memory _name, string memory _symbol, address _lzEndpoint, address _delegate ) OFTAlt(_name, _symbol, _lzEndpoint, _delegate) { // constructor logic ... } } ``` ### Contract Changes At a high level, only a few changes to your OApp are needed to interact with the `EndpointV2Alt.sol` contract: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { SafeERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { MessagingParams } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { MessagingFee, MessagingReceipt } from "../interfaces/IOFT.sol"; import { OFT } from "../OFT.sol"; contract OFTAlt is OFT { using SafeERC20 for IERC20; error LzAltTokenUnavailable(); constructor( string memory _name, string memory _symbol, address _lzEndpoint, address _delegate ) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {} /** * @dev Internal function to interact with the LayerZero EndpointV2.send() for sending a message. * @param _dstEid The destination endpoint ID. * @param _message The message payload. * @param _options Additional options for the message. * @param _fee The calculated LayerZero fee for the message. * - nativeFee: The native fee. * - lzTokenFee: The lzToken fee. * @param _refundAddress The address to receive any excess fee values sent to the endpoint. * @return receipt The receipt for the sent message. * - guid: The unique identifier for the sent message. * - nonce: The nonce of the sent message. * - fee: The LayerZero fee incurred for the message. */ function _lzSend( uint32 _dstEid, bytes memory _message, bytes memory _options, MessagingFee memory _fee, address _refundAddress ) internal virtual override returns (MessagingReceipt memory receipt) { // @dev Push corresponding fees to the endpoint, any excess is sent back to the _refundAddress from the endpoint. _payNative(_fee.nativeFee); if (_fee.lzTokenFee > 0) _payLzToken(_fee.lzTokenFee); return // solhint-disable-next-line check-send-result endpoint.send( MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0), _refundAddress ); } /** * @dev Internal function to pay the alt token fee associated with the message. * @param _nativeFee The alt token fee to be paid. * * @dev If the OApp needs to initiate MULTIPLE LayerZero messages in a single transaction, * this will need to be overridden because alt token would contain multiple lzFees. */ function _payNative(uint256 _nativeFee) internal virtual override returns (uint256 nativeFee) { address nativeErc20 = endpoint.nativeToken(); if (nativeErc20 == address(0)) revert LzAltTokenUnavailable(); // Pay Alt token fee by sending tokens to the endpoint. IERC20(nativeErc20).safeTransferFrom(msg.sender, address(endpoint), _nativeFee); } } ``` #### Pass OpenZeppelin Ownable ```solidity constructor( string memory _name, string memory _symbol, address _lzEndpoint, address _delegate // highlight-next-line ) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {} ``` Make sure to pass [OpenZeppelin's Ownable](https://docs.openzeppelin.com/contracts/5.x/api/access#Ownable) modifier to the constructor. The access control has already been applied, but must be explicitly passed in the constructor to compile successfully. #### Using SafeERC20 ```solidity using SafeERC20 for IERC20; ``` You should include the [SafeERC20 library](https://docs.openzeppelin.com/contracts/5.x/api/token/erc20#SafeERC20) for safely interacting with ERC20 tokens. This is crucial for ensuring that token transfers handle potential errors like reverts or exceptions. #### Error Handling ```solidity error LzAltTokenUnavailable(); ``` A custom error `LzAltTokenUnavailable` which is used to handle cases where the native ERC20 token for fee payment is not set in the `EndpointV2Alt` contract. #### Override `_payNative` ```solidity /** * @dev Internal function to pay the alt token fee associated with the message. * @param _nativeFee The alt token fee to be paid. * * @dev If the OApp needs to initiate MULTIPLE LayerZero messages in a single transaction, * this will need to be overridden because alt token would contain multiple lzFees. */ function _payNative(uint256 _nativeFee) internal virtual override returns (uint256 nativeFee) { // highlight-start address nativeErc20 = endpoint.nativeToken(); if (nativeErc20 == address(0)) revert LzAltTokenUnavailable(); // Pay Alt token fee by sending tokens to the endpoint. IERC20(nativeErc20).safeTransferFrom(msg.sender, address(endpoint), _nativeFee); // highlight-end } ``` You should override the `_payNative` function to handle paying using an ERC20 token. This function checks if the ERC20 token address is set (`nativeErc20`), reverts if not, and performs a `safeTransferFrom` to transfer the fee from the `sender` to the `endpoint`. This ensures that the contract can handle fees in the specified ERC20 token by the `EndpointV2Alt`. #### Override `_lzSend` ```solidity /** * @dev Internal function to interact with the LayerZero EndpointV2.send() for sending a message. * @param _dstEid The destination endpoint ID. * @param _message The message payload. * @param _options Additional options for the message. * @param _fee The calculated LayerZero fee for the message. * - nativeFee: The native fee. * - lzTokenFee: The lzToken fee. * @param _refundAddress The address to receive any excess fee values sent to the endpoint. * @return receipt The receipt for the sent message. * - guid: The unique identifier for the sent message. * - nonce: The nonce of the sent message. * - fee: The LayerZero fee incurred for the message. */ function _lzSend( uint32 _dstEid, bytes memory _message, bytes memory _options, MessagingFee memory _fee, address _refundAddress ) internal virtual override returns (MessagingReceipt memory receipt) { // highlight-start // @dev Push corresponding fees to the endpoint, any excess is sent back to the _refundAddress from the endpoint. _payNative(_fee.nativeFee); if (_fee.lzTokenFee > 0) _payLzToken(_fee.lzTokenFee); return // solhint-disable-next-line check-send-result endpoint.send( MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0), _refundAddress ); // highlight-end } ``` To apply the changes made in `_payNative`, you should also override `_lzSend` to handle the ERC20 token fee. :::info Because `_lzSend` now uses an ERC20 token as payment, you must now [**approve**](https://docs.openzeppelin.com/contracts/2.x/api/token/erc20#IERC20-approve-address-uint256-) the OFT as a spender of your ERC20 token. ::: --- --- title: Best Practices for Contract Ownership sidebar_label: Contract Ownership --- LayerZero’s Contract Standards inherit the [OpenZeppelin Ownable Standard](https://docs.openzeppelin.com/contracts/5.x/access-control) by default. This allows for flexible and secure administration of deployed contracts, such as OApp or OFT. However, decisions around transferring or renouncing ownership must be made carefully, especially when dealing with critical contracts. ## Why Ownership Matters When you deploy a contract, such as an OFT token, the deployer is set as the initial owner. As the owner, you have the ability to configure many administrative settings, including: - **Peer Management:** Setting peers for cross-chain operations. - **Delegate Controls:** Managing delegate addresses. - **Enforced Options:** Configuring options that govern contract behavior. - **Message Inspectors:** Overseeing message processing and security checks. These controls are essential for ensuring the secure operation of your LayerZero contracts. ## Recommended Best Practices 1. **Retain Ownership with a Secure Multisig:** - **Do not renounce ownership** of critical contracts like the OFT. Instead, transfer ownership to a multisig wallet. - A multisig setup requires multiple signatures (or approvals) for administrative actions, reducing the risk of a single point of failure. - Use a high enough quorum to ensure that no single party can unilaterally change settings. 2. **Maintain Flexibility:** - Retaining ownership allows you to adjust peers, delegates, and other settings as your cross-chain protocols evolve. - This flexibility can be critical for adding new networks or responding to chain level disruptions. 3. **Document and Audit:** - Clearly document the ownership and administration process for your contracts. - Regularly audit the multisig wallet and its quorum settings to ensure they meet current security and governance standards. ## Example: Transfer of Ownership LayerZero’s contracts follow the `Ownable` pattern. For example, here’s how you can transfer ownership of an OFT token contract: ```solidity // 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, Ownable { constructor( string memory _name, string memory _symbol, address _lzEndpoint, address _delegate ) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {} } ``` ```typescript // Transferring ownership in your deployment script or via a web3 interface: tx = await(await oft.transferOwnership(newAddress)).wait(); ``` By transferring ownership to a secure multisig wallet (or another trusted address), you ensure that the contract remains under strong administrative control even as you delegate responsibilities or make system-wide changes. ## Summary - **Retain Ownership:** Do not renounce ownership on critical LayerZero contracts (like the BNB OFT token). - **Use Secure Multisig:** Always maintain ownership through a properly configured multisig wallet to allow for necessary administrative controls. - **Stay Flexible:** Keeping control allows you to update settings such as peers, delegates, and message inspectors as needed. This approach secures your contract administration while ensuring you can respond to any changes or issues that arise in a rapidly evolving cross-chain environment. --- --- title: Read External State (LayerZero Read) sidebar_label: Read Overview (lzRead) toc_min_heading_level: 2 toc_max_heading_level: 5 --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; **LayerZero Read** extends the existing LayerZero omnichain messaging protocol to enable developers to not only send cross-chain messages, but also **request** and **retrieve** on-chain state from other supported blockchains. This new capability transforms how cross-chain data is accessed, manipulated, and computed, allowing developers to offload complex computations from on-chain applications to [Decentralized Verifier Networks (DVNs)](../../../concepts/modular-security/security-stack-dvns.md). ## Blockchain Query Language `lzRead` is the implementation of LayerZero's **Blockchain Query Language** — a universal, standardized language for constructing, retrieving, and processing data requests on-chain across multiple blockchains and off-chain sources. Using `lzRead`, developers can configure secure, low-latency data queries within smart contracts and manage custom security settings, offering significant flexibility in balancing security, cost, and data accuracy. The first implementation of BQL is `ReadCodecV1.sol`, enabling smart contracts to request data in the form of `calldata`, `public` state variables, and non state-changing function calls (i.e., `view` and `pure` functions). As `lzRead` evolves, additional capabilities such as querying event logs, private state variables, and even external data sources may be integrated, further extending the versatility of `lzRead` as a comprehensive data sourcing utility for on-chain applications. ## Workflow ![OFT Example](/img/lzRead-diagram-light.svg#gh-light-mode-only) ![OFT Example](/img/lzRead-diagram-dark.svg#gh-dark-mode-only) 1. **Request Definition**: An `OApp` sends a `lzRead` command by calling `EndpointV2.send()` with a read-specific `eid` argument called a channel identifier, and encoding the specific target chain and the block from which the state needs to be retrieved. This command is processed using a custom Send Library (`ReadLib1002`) that serializes the request and directs it to the appropriate chain via the application's configured `DVN(s)`. 2. **DVN Fetch and Return**: The `DVN` assigned to the request fetches the data from an archival node on the target chain. The DVN executes `eth_call` on the node with the provided calldata for the target chain, and optionally processes the return data off-chain via compute logic defined by the application in a target `OApp.lzMap()` and `OApp.lzReduce()` function, before finally delivering the return data hash to be validated by the configured `DVN threshold` in the Receive Library (in this case the same `ReadLib1002`). 3. **Response Handling (lzReceive)**: Once the response hash is verified, the `Executor` delivers the actual response data to `OApp.lzReceive()` on the original requesting chain. The same receive workflow used by LayerZero's V2 protocol is triggered via `EndpointV2.lzReceive()`. 4. **Execution and Custom Logic**: The `OApp` processes the response data using the logic defined in the application smart contract via an internal `_lzReceive()`, allowing the developer to customize how the retrieved state is processed and used. In this way, **lzRead** transforms the normal messaging workflow from cross-chain message delivery to a `request` and `response` model. | | **Description** | **endpoint.send** | **endpoint.lzReceive** | | ------------------------- | ------------------------------------------------------ | ----------------- | ---------------------- | | **Omnichain Message** | The `bytes` sent match the `bytes` received. | `bytes _message` | `bytes _message` | | **Omnichain Read** | The `bytes` request differs from the `bytes` response. | `bytes _request` | `bytes _response` | Instead of sending a source message and using Decentralized Verifier Networks (DVNs) to deliver to the destination, you can now use Decentralized Verifier Networks (DVNs) to read directly from nodes on the destination blockchain. ## Supported Chains `lzRead` requires a read compatible Message Library and DVN, meaning you can only use `lzRead` on chains with both supported implementations deployed. You can find these Message Libraries (`ReadLib1002`) and Compatible DVNs under ["Read Paths"](../../../deployments/read-contracts.md). ![Read DVNs Light](/img/read-compatible-dvn-light.png#gh-light-mode-only) ![Read DVNs Dark](/img/read-compatible-dvn-dark.png#gh-dark-mode-only) :::info Each DVN must also support the target chain to read from (i.e., running an archival node for the target data chain). ::: ## Installation To start using LayerZero contracts, you can install the [OApp package](https://github.com/LayerZero-Labs/devtools/tree/main/packages/oapp-evm) to an existing project: ```bash npm install @layerzerolabs/oapp-evm ``` ```bash yarn add @layerzerolabs/oapp-evm ``` ```bash pnpm add @layerzerolabs/oapp-evm ``` ```bash forge init ``` ```bash forge install https://github.com/LayerZero-Labs/devtools ``` ```bash forge install https://github.com/LayerZero-Labs/layerzero-v2 ``` ```bash forge install OpenZeppelin/openzeppelin-contracts@v5.1.0 ``` Then add to your `foundry.toml` under `[profile.default]`: ```toml [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', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', ] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options ``` :::info 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 "resolutions": { "@openzeppelin/contracts": "^5.0.1", } ``` ::: ## Usage To start using `lzRead`, you will need to: 1. Decide what origin chains to deploy your application on and what target data chains to read data from. 1. Decide which `lzRead` compatible DVNs support your target chains. 1. Deploy an `lzRead` compatible OApp. 1. Set your application's send and receive library to `ReadLib1002` via `EndpointV2.setSendLibrary()` and `EndpointV2.setReceiveLibrary()`. 1. Set your application's DVN Config via `EndpointV2.setConfig()`. ### Inherit OAppRead Begin by inheriting the `OAppRead.sol` contract in your own smart contract. Familiarize yourself with the `OAppRead` implementation to understand how it interacts with LayerZero’s endpoint for both message and read commands. ```solidity import { OAppRead } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppRead.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; contract MyOApp is OAppRead { constructor(address _endpoint, address _delegate) OAppRead(_endpoint, _delegate) Ownable(_delegate) {} } ``` ### Build Read Query To retrieve data from other chains and process it when returned, you need to at minimum create a read request. Below is a step-by-step guide on how to construct these read requests, using an example where we read the price of a Uniswap V3 pool token from multiple chains. #### Getting the Command As mentioned above, **lzRead** uses the same send interface as traditional message passing via `EndpointV2.send()`. To start reading data from target chains, you will encode your requests as `bytes` using the `ReadCodecV1.sol` and pass the encoding as the `bytes _message` parameter in the send interface. From here, we'll refer to the `_message` as the **read command**, or `_cmd`. Here's an example of how you can initiate the read request: ```solidity /** * @notice Sends a read request to LayerZero, querying Uniswap QuoterV2 for WETH/USDC prices on configured chains. * @param _extraOptions Additional messaging options, including gas and fee settings. * @return receipt The LayerZero messaging receipt for the request. */ function readAverageUniswapPrice( bytes calldata _extraOptions ) external payable returns (MessagingReceipt memory receipt) { bytes memory cmd = getCmd(); return _lzSend( READ_CHANNEL, cmd, combineOptions(READ_CHANNEL, READ_MSG_TYPE, _extraOptions), MessagingFee(msg.value, 0), payable(msg.sender) ); } ``` For example, this `getCmd()` creates multiple read requests: ```solidity /** * @notice Constructs a command to query the Uniswap QuoterV2 for WETH/USDC prices on all configured chains. * @return cmd The encoded command to request Uniswap quotes. */ function getCmd() public view returns (bytes memory) { uint256 pairCount = targetEids.length; EVMCallRequestV1[] memory readRequests = new EVMCallRequestV1[](pairCount); for (uint256 i = 0; i < pairCount; i++) { uint32 targetEid = targetEids[i]; ChainConfig memory config = chainConfigs[targetEid]; // Define the QuoteExactInputSingleParams IQuoterV2.QuoteExactInputSingleParams memory params = IQuoterV2.QuoteExactInputSingleParams({ tokenIn: config.tokenInAddress, tokenOut: config.tokenOutAddress, amountIn: 1 ether, // amountIn: 1 WETH fee: config.fee, sqrtPriceLimitX96: 0 // No price limit }); // @notice Encode the function call // @dev From Uniswap Docs, this function is not marked view because it relies on calling non-view // functions and reverting to compute the result. It is also not gas efficient and should not // be called on-chain. We take advantage of lzRead to call this function off-chain and get the result // returned back on-chain to the OApp's _lzReceive method. // https://docs.uniswap.org/contracts/v3/reference/periphery/interfaces/IQuoterV2 bytes memory callData = abi.encodeWithSelector(IQuoterV2.quoteExactInputSingle.selector, params); readRequests[i] = EVMCallRequestV1({ appRequestLabel: uint16(i + 1), targetEid: targetEid, isBlockNum: false, blockNumOrTimestamp: uint64(block.timestamp), confirmations: config.confirmations, to: config.quoterAddress, callData: callData }); } EVMCallComputeV1 memory computeSettings = EVMCallComputeV1({ computeSetting: 2, // lzMap() and lzReduce() targetEid: ILayerZeroEndpointV2(endpoint).eid(), isBlockNum: false, blockNumOrTimestamp: uint64(block.timestamp), confirmations: 15, to: address(this) }); return ReadCodecV1.encode(0, readRequests, computeSettings); } ``` :::tip Following best practices, you should create a dedicated function to construct your specific command requests. This involves creating an array of `EVMCallRequestV1` structs, each representing a read operation on a specific chain and contract. ::: :::info `_lzSend` is an internal `virtual` method provided in the `OApp` contract standard, which can be used to invoke the `endpoint.send` if certain security checks pass: ```solidity function _lzSend( uint32 _dstEid, bytes memory _message, bytes memory _options, MessagingFee memory _fee, address _refundAddress ) internal virtual returns (MessagingReceipt memory receipt) { // @dev Push corresponding fees to the endpoint, any excess is sent back to the _refundAddress from the endpoint. uint256 messageValue = _payNative(_fee.nativeFee); if (_fee.lzTokenFee > 0) _payLzToken(_fee.lzTokenFee); return // solhint-disable-next-line check-send-result endpoint.send{ value: messageValue }( MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0), _refundAddress ); } ``` If using the `OApp` standard, consider `_lzSend` the recommended way to call `EndpointV2.send()`. :::

#### Call Requests A properly formatted read request is of type `EVMCallRequestV1`, which includes the following fields: ```solidity readRequests[i] = EVMCallRequestV1({ appRequestLabel: uint16(i + 1), targetEid: targetEid, isBlockNum: false, blockNumOrTimestamp: uint64(block.timestamp), confirmations: config.confirmations, to: config.quoterAddress, callData: callData }); ``` | Name | Type | Description | | ------------------- | --------- | --------------------------------------------------------------------------------------------------------------- | | appRequestLabel | `uint16` | A label to identify the request within your application logic. Useful for tracking response types. | | targetEid | `uint32` | The Endpoint ID of the target chain where the data is to be read from. | | isBlockNum | `bool` | A boolean indicating whether `blockNumOrTimestamp` is a block number (`true`) or a timestamp (`false`). | | blockNumOrTimestamp | `uint64` | Specifies the `block.number` or `block.timestamp` on the target chain (`targetEid`) at which to read the state. | | confirmations | `uint16` | The number of confirmations required to wait for the block or timestamp finality on the target chain. | | to | `address` | The target contract address on the destination chain (e.g., the Uniswap V3 Quoter contract). | | callData | `bytes` | The ABI-encoded function calldata. | The `callData` field contains the encoded function call that will be executed on the target contract. For example, `IQuoterV2.quoteExactInputSingle.selector` specifies the `quoteExactInputSingle` function in the Uniswap V3 `QuoterV2` contract to request a price quote for swapping `tokenIn` to `tokenOut` on the specified Uniswap V3 pool. :::warning Your target function calldata for the call request should **never revert**. If the target function ever reverts, the DVN can never complete the verification workflow, and the message will be stuck in-flight and block the pathway until the OApp delegate calls `EndpointV2.skip()` on the reverted nonce. Ensure that your target function either uses `try-catch` to handle reverts, or be confident that your call parameters will never trigger a revert. ::: :::info The `EVMCallRequestV1` struct is limited to chain `calldata` because it encodes function calls into a format that can be transmitted and understood across chains using the EVM's ABI encoding. This limitation means that `lzRead` can only handle data types compatible with `calldata`, specifically value types and fixed-size data structures. ::: #### Optional: Compute Logic You can optionally specify compute instructions to handle processing the return data after the read request. **Compute Logic** allows you to transform and condense the data retrieved by your DVNs, ensuring that only the most relevant and actionable information is sent back to your application on the origin chain. Compute logic should be formatted as an `EVMCallComputeV1` struct: ```solidity EVMCallComputeV1 memory computeSettings = EVMCallComputeV1({ computeSetting: 2, // lzMap() and lzReduce() targetEid: ILayerZeroEndpointV2(endpoint).eid(), isBlockNum: false, blockNumOrTimestamp: uint64(block.timestamp), confirmations: 15, to: address(this) }); ``` | Name | Type | Description | | ------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | computeSetting | `uint8` | Specifies the compute operations to perform, such as `lzMap()` (0), `lzReduce()`, (1), or `lzMap()` AND `lzReduce()` (2). More on this later. | | targetEid | `uint32` | The Endpoint ID of the target chain where the `lzMap()` or `lzReduce()` implementation can be read from. | | isBlockNum | `bool` | A boolean indicating whether `blockNumOrTimestamp` is a block number (`true`) or a timestamp (`false`). | | blockNumOrTimestamp | `uint64` | Specifies the `block.number` or `block.timestamp` on the target chain at which to read the state. | | confirmations | `uint16` | The number of confirmations required to wait for the block or timestamp finality on the target chain. | | to | `address` | The target contract address to view `lzMap()` and / or `lzReduce()`. | :::info Note: The next section will dive deeper into how to define the compute logic within `lzMap()` and `lzReduce()`. ::: #### Encoding Command The `ReadCodecV1.encode()` has been overloaded to support both the `readRequest` and `evmCompute`. If you only need to read the raw data returned by your call requests, you can finish the encoding and omit the compute logic. ````solidity ```solidity int16 appLabel = 0; // Application label (set as needed) // Encoding the command without compute logic return ReadCodecV1.encode(appLabel, readRequests); ```` If you have defined the command's `EVMCallRequestV1` and the `EVMCallComputeV1`, you can use the `ReadCodecV1` to encode the command to end your getter function. ```solidity int16 appLabel = 0; // Application label (set as needed) // Encoding the command return ReadCodecV1.encode(appLabel, readRequests, evmCompute); ``` :::caution Different chains have different interpretations of `block.number` and `block.timestamp`. For example, when calling `block.number` on Arbitrum L2 the value returned is the block number on Ethereum L1, rather than `block.number` of the Arbitrum chain itself. Consider how these edge cases specific to each `targetEid` impact your application before finalizing the encoding process for your command from `EVMCallRequestV1` and `EVMCallComputeV1`. ::: ### Optional: Declare Compute Logic Compute logic is executed off-chain via your application's configured Decentralized Verifier Networks (DVNs). To define specific compute logic, you must target a contract that implements either `IOAppMapper` or `IOAppReducer`, which define the `lzMap()` and `lzReduce()` functions. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; /// @title IOAppMapper Interface /// @notice Defines the lzMap function for mapping operations interface IOAppMapper { /** * @notice Processes a single mapping operation * @param _request The request data in bytes * @param _response The response data in bytes * @return A bytes array resulting from the mapping operation */ function lzMap( bytes calldata _request, bytes calldata _response ) external view returns (bytes memory); } /// @title IOAppReducer Interface /// @notice Defines the lzReduce function for reducing operations interface IOAppReducer { /** * @notice Processes a reduction operation over multiple responses * @param _cmd The command data in bytes * @param _responses An array of response data in bytes * @return A bytes array resulting from the reduction operation */ function lzReduce( bytes calldata _cmd, bytes[] calldata _responses ) external view returns (bytes memory); } ``` These optional `view` or `pure` functions enable your DVNs to read and compute state changes based on the `_response` or `_responses` from each request. #### Compute Settings When you declare an `EVMCallComputeV1`, you also select a compute setting for whether your configured DVNs should mutate the `_response` data using `lzMap()` (`0`), `lzReduce()`, (`1`), both `lzMap()` and `lzReduce()` (`2`), or no compute setting (`3`). ```solidity uint8 internal constant MAP_ONLY = 0; uint8 internal constant REDUCE_ONLY = 1; uint8 internal constant MAP_AND_REDUCE = 2; EVMCallComputeV1 memory evmCompute = EVMCallComputeV1({ // highlight-next-line computeSetting: MAP_AND_REDUCE, // lzMap() and lzReduce() targetEid: eid, // Current chain's EID isBlockNum: true, blockNumOrTimestamp: uint64(block.number), confirmations: 2, to: address(this) // Compute executed in this contract }); ``` - If only `lzMap()`, the returned data to `OApp._lzReceive()` will be the concatenation of every return output for each `_request` your `lzMap()` processes. You will need to track the `index` of each request made in the command to later decode in your receive logic. - If only `lzReduce`, the `lzReduce()` implementation will intake the concatenation in order of how the `EVMCallRequestV1[]` were made as a `bytes` argument, and return a single `bytes` output. You can use `lzReduce()` to aggregate all responses. - If both, data will be manipulated on a per request level first via `lzMap()` and an aggregate level via `lzReduce()`, before being returned to `OApp._lzReceive()`. Implementation details can be found below. #### Mapping Requests Mapping refers to defining how to format or process the data returned by each individual request. In the Uniswap example, the `lzMap()` simplifies the returned data by only decoding the `amountOut` for use in the `lzReduce()` step. ```solidity /** * @notice Processes individual Uniswap QuoterV2 responses, encoding the result. * @param _response The response from the read request. * @return Encoded token output amount (USDC amount). */ function lzMap(bytes calldata, bytes calldata _response) external pure returns (bytes memory) { require(_response.length >= 32, "Invalid response length"); // quoteExactInputSingle returns multiple values // Decode the response to extract amountOut (uint256 amountOut, , , ) = abi.decode(_response, (uint256, uint160, uint32, uint256)); return abi.encode(amountOut); } ``` :::info The decoding above only decodes to demonstrate how an `lzMap()` can intake a `_response`, mutate the returned `bytes` per `_request`, and return a new encoded output depending on the needs of your application. ::: #### Reducing Requests Once you've mapped the individual responses, the `lzReduce()` function aggregates these mapped responses to produce a final result. In the Uniswap example, `lzReduce()` calculates the average `amountOut` from all responses. ```solidity /** * @notice Aggregates individual token output amounts to compute an average. * @param _responses Array of mapped responses containing token output amounts. * @return Encoded average token output amount. */ function lzReduce(bytes calldata, bytes[] calldata _responses) external pure returns (bytes memory) { require(_responses.length == 3, "Expected responses from 3 chains"); uint256 total = 0; for (uint256 i = 0; i < _responses.length; i++) { uint256 amountOut = abi.decode(_responses[i], (uint256)); total += amountOut; } uint256 averageAmountOut = total / _responses.length; return abi.encode(averageAmountOut); } ``` ### Creating Request Options `lzRead` uses a new options type, `addExecutorLzReadOption` to send requests for target data. ```solidity OptionsBuilder.newOptions().addExecutorLzReadOption(100_000, 64, 0) ``` If unfamiliar with `_options`, you can read the full scope in [Execution Options](../configuration/options.md), but for reference this send parameter allows you to deliver an amount of `gasLimit` and/or native gas token automatically to `endpoint.lzReceive` from your configured Executor. For `lzRead`, an additional requirement for `_options` is to profile the `calldata` size of your returned data type: ```solidity OptionsBuilder.newOptions().addExecutorLzReadOption(GAS_LIMIT, CALLDATA_SIZE, MSG_VALUE) ``` These `_options` can be enforced as usual by inheriting the [Enforced Options Helper](../oapp/overview.md#optional-enforced-options) in your `OAppRead` contract: ```solidity import { OAppRead } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppRead.sol"; import { OAppOptionsType3, EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; contract MyOAppRead is OAppRead, OAppOptionsType3 {} ``` :::caution Adding any other `_options` types than the `lzReadOption` type in `endpoint.quote()` or `endpoint.send()` call will cause the transaction to revert on source. ::: :::caution The Executor will not deliver the request if the `ACTUAL_CALLDATA_SIZE > OPTION_CALLDATA_SIZE`. You will need to manually execute the transaction via `endpoint.lzReceive()`. To avoid this, estimate the return calldata size from your target function, and enforce that you add at least the calldata size to your message `options`. ::: ### Receive Responses Finally, you need to define your internal handler for incoming messages from the LayerZero protocol. This is where you'll process the final aggregated result and use it within your contract. ```solidity function _lzReceive( Origin calldata _origin, bytes32 _guid, bytes calldata _message, address _executor, bytes calldata _extraData ) internal ``` In the Uniswap example, the handler processes the message and emits an event with the average token price: ```solidity /// @notice Emitted when the average amount out is computed and received. event AggregatedPrice(uint256 averageAmountOut); /** * @notice Handles the aggregated average price from Uniswap V3 pool responses received from LayerZero. * @dev Emits the AggregatedPrice event with the calculated average amount. * @param _message Encoded average token output amount. */ function _lzReceive( Origin calldata /*_origin*/, bytes32 /*_guid*/, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { require(_message.length == 32, "Invalid message length"); uint256 averagePrice = abi.decode(_message, (uint256)); emit AggregatedPrice(averagePrice); } ``` By using `lzRead`, you can drastically simplify the amount of logic needed for updating state on-chain. ### Setting Libraries and DVNs You MUST call `EndpointV2.setSendLibrary()`, `EndpointV2.setReceiveLibrary()`, and `EndpointV2.setConfig()` with a valid `ReadLib1002` and supporting `DVN` configuration. Set your configuration for the Read Library and required DVNs that support `lzRead`. [See all available DVNs](../../../deployments/dvn-addresses.md). You can set your DVN configuration either via the [LayerZero CLI tool](../create-lz-oapp/start.md) or via [example scripts](../configuration/dvn-executor-config.md). ### Setting Read Channel Once your configuration has been applied, simply call `OApp.setReadChannel()` for the specific channel configuration to set and begin sending messages! Channels let you define multiple types of read logic per application. You can specify per request label, per application label, or per channel. ```solidity // OAppRead.sol // Only Owner function setReadChannel(uint32 _channelId, bool _active) public virtual onlyOwner { _setPeer(_channelId, _active ? AddressCast.toBytes32(address(this)) : bytes32(0)); } ``` To see the available channels for each target data chain, see [Read Paths](../../../deployments/read-contracts.md). ### Debugging Read Commands When working with `lzRead`, you might encounter issues where read commands (Requests and Compute logic) fail to execute as expected. LayerZero provides tools and best practices to help you debug these scenarios effectively. When checking LayerZero Scan for the read status, you may encounter the following errors. #### Malformed Command Malformed indicates that the read command was constructed incorrectly and does not adhere to the required serialization format. This can be due to: - Incorrect packing of the command parameters. - Errors in encoding the `calldata`. - Misuse of the `ReadCodecV1.sol` library during command construction. To resolve a **Malformed Command**, review how the command was packed on the origin chain by examining your `getCmd` implementation. Ensure that you are correctly using the `ReadCodecV1.sol` library for serialization. #### Unresolvable Command The target data chain cannot fulfill the read request due to issues related to invoking the `calldata` on the target chain, such as: **Contract Address Issues**: - The specified contract does not exist on the target chain. - The contract address is incorrect for the given block number or timestamp. **Method Identifier Issues**: - The calldata contains an invalid or non-existent method selector. **State Issues**: - The contract is deployed, but the state at the specified `block.number` or `block.timestamp` does not support the requested operation. To resolve an **Unresolvable Command**: - Ensure that the contract exists at the specified address on the target chain. - Verify that the contract is deployed and initialized correctly. - Double-check the contract address and ensure it matches the target chain's deployment. - Confirm that the `block.number` or `block.timestamp` specified in the read request corresponds to a state where the contract is available. - Ensure that the method selector used in the `calldata` exists in the target contract. - Verify that the function signatures match and that the target contract implements the requested methods. You can use the [Read CLI Debugging Task](./read-cli.md#debugging-malformed-or-unresolvable-commands) to help with the debugging process by ensuring the command decodes and reads the target data chain correctly. ### Dual Messaging Mode Since `lzRead` is an extension to the base messaging protocol, determine the type of cross-chain functionality your app needs: 1. **LayerZero Messaging**: For standard cross-chain messages without needing to read external state. 2. **LayerZero Reads (lzRead)**: To retrieve and use on-chain state from other blockchains. 3. **Hybrid (Both)**: To implement both messaging and read capabilities. Here’s an example of how `OAppRead` is structured to handle both message-based and read-based responses by implementing a `_messageLzReceive` and `_readLzReceive`: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {OAppRead} from "@layerzerolabs/oapp-evm/contracts/oapp/OAppRead.sol"; import {Origin} from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; /// @title MsgAndReadExample /// @notice An example contract that extends OAppRead to handle both messaging and read capabilities. /// @dev Inherits from OApp and adds functionality specific to lzRead. contract MsgAndReadExample is OAppRead { /// lzRead responses are sent from arbitrary channels with Endpoint IDs in the range of /// `eid > 4294965694` (which is `type(uint32).max - 1600`). uint32 constant READ_CHANNEL_EID_THRESHOLD = 4294965694; /// @param _endpoint The address of the LayerZero endpoint. /// @param _delegate The address of the delegate contract. constructor(address _endpoint, address _delegate) OAppRead(_endpoint, _delegate) Ownable(_delegate) {} /// @notice Internal function to handle incoming messages and read responses. /// @dev Filters messages based on `srcEid` to determine the type of incoming data. /// @param _origin The origin information containing the source Endpoint ID (`srcEid`). /// @param _guid The unique identifier for the received message. /// @param _message The encoded message data. /// @param _executor The executor address. /// @param _extraData Additional data. function _lzReceive( Origin calldata _origin, bytes32 _guid, bytes calldata _message, address _executor, bytes calldata _extraData ) internal virtual override { /** * @dev The `srcEid` (source Endpoint ID) is used to determine the type of incoming message. * - If `srcEid` is greater than READ_CHANNEL_EID_THRESHOLD (4294965694), * it corresponds to arbitrary channel IDs for lzRead responses. * - All other `srcEid` values correspond to standard LayerZero messages. */ if (_origin.srcEid > READ_CHANNEL_EID_THRESHOLD) { // Handle lzRead responses from arbitrary channels. _readLzReceive(_origin, _guid, _message, _executor, _extraData); } else { // Handle standard LayerZero messages. _messageLzReceive(_origin, _guid, _message, _executor, _extraData); } } /// @notice Internal function to handle standard LayerZero messages. /// @dev _origin The origin information (unused in this implementation). /// @dev _guid The unique identifier for the received message (unused in this implementation). /// @param _message The encoded message data. /// @dev _executor The executor address (unused in this implementation). /// @dev _extraData Additional data (unused in this implementation). function _messageLzReceive( Origin calldata /* _origin */, bytes32 /* _guid */, bytes calldata _message, address /* _executor */, bytes calldata /* _extraData */ ) internal virtual { // Implement message handling logic here. bool _messageDoSomething = abi.decode(_message, (bool)); } /// @notice Internal function to handle lzRead responses. /// @dev _origin The origin information (unused in this implementation). /// @dev _guid The unique identifier for the received message (unused in this implementation). /// @param _message The encoded message data. /// @dev _executor The executor address (unused in this implementation). /// @dev _extraData Additional data (unused in this implementation). function _readLzReceive( Origin calldata /* _origin */, bytes32 /* _guid */, bytes calldata _message, address /* _executor */, bytes calldata /* _extraData */ ) internal virtual { // Implement lzRead response handling logic here. bool _readDoSomething = abi.decode(_message, (bool)); } } ``` --- --- title: Read CLI Setup Guide sidebar_label: Read CLI Setup Guide --- To start leveraging LayerZero Read (`lzRead`), LayerZero offers CLI examples that streamline the setup and configuration process, similar to the standard CLI examples provided for other LayerZero functionalities. ## Using the Read CLI To begin using `lzRead`, follow the steps below. These instructions utilize the same commands as the standard setup for OApps but include minor adjustments specific to configuring read capabilities. ### Create Your lzRead Repo Run the following command to create a new LayerZero OApp with read capabilities enabled: ``` LZ_ENABLE_READ_EXAMPLE=1 npx create-lz-oapp@latest ``` This command initializes a new project with the necessary configurations to support lzRead. The project creation wizard will guide you through selecting a template and setting up your development environment. ```bash ✔ Where do you want to start your project? … ./my-lz-read-oapp ✔ Which example would you like to use as a starting point? › OApp Read ✔ What package manager would you like to use in your project? › pnpm ``` This will set up a repository with example contracts, cross-chain unit tests for read operations, custom LayerZero read configuration files, deployment scripts, and more. Follow the normal setup process defined above (adding networks to your `hardhat.config.ts`, adding your `MNEMONIC` or` PRIVATE_KEY` to `.env`, etc.) ```typescript // hardhat.config.ts import { EndpointId } from '@layerzerolabs/lz-definitions'; networks: { ethereum: { eid: EndpointId.ETHEREUM_V2_MAINNET, url: process.env.RPC_URL_ETHEREUM, accounts, }, arbitrum: { eid: EndpointId.ARBITRUM_V2_MAINNET, url: process.env.RPC_URL_ARBITRUM, accounts, }, polygon: { eid: EndpointId.POLYGON_V2_MAINNET, url: process.env.RPC_URL_POLYGON, accounts, }, }, ``` Refer to the [LayerZero Endpoint Addresses](../../../deployments/deployed-contracts.md) to ensure the networks you add have deployed endpoints. To see a list of available commands, run `npx hardhat`: ```bash lz:read:resolve-command Task for debugging read commands lz:oapp-read:wire Wire LayerZero Read OApp lz:oapp-read:config:get Get Read OApp configuration lz:oapp-read:config:init Initialize Read OApp configuration lz:oapp-read:config:get:channel Get information of read channels for networks ``` Each command serves a specific purpose in managing and configuring your lzRead setup. All of the standard CLI methods available in the [LayerZero CLI Setup Guide](../create-lz-oapp/start.md) can also be found in this newly initialized project. Take some time to familiarize yourself with the project commands and layout. In general each of the `lzRead` CLI methods have been modified from the base CLI, so all `lz:oapp:` and `lz:oapp:read` methods should behave similarly. ### Configure LayerZero Config Unlike standard OApp configurations, `lzRead` requires specific settings in the `layerzero.config.ts` file. Instead of configuring `connections`, you'll focus solely on defining `contracts` and `read channels`: ```typescript // layerzero.config.ts import {ChannelId, EndpointId} from '@layerzerolabs/lz-definitions'; import {OAppReadOmniGraphHardhat} from '@layerzerolabs/oapp-evm'; import {ethers} from 'ethers'; const arbitrumContract: OmniPointHardhat = { eid: EndpointId.ARBITRUM_V2_MAINNET, contractName: 'UniswapV3QuoteDemo', }; const config: OAppReadOmniGraphHardhat = { contracts: [ { contract: arbitrumContract, // Dummy contract address config: { readLibrary: '0xbcd4CADCac3F767C57c4F402932C4705DF62BEFf', readChannels: [ { channelId: ChannelId.READ_CHANNEL_1, active: true, }, ], readConfig: { ulnConfig: { requiredDVNs: ['0x1308151a7ebac14f435d3ad5ff95c34160d539a5'], executor: '0x31CAe3B7fB82d847621859fb1585353c5720660D', }, }, }, }, ], connections: [], // No connections needed for read-only setup }; export default config; ``` You can generate this config file based on the networks specified in your `hardhat.config.ts` by running: ```bash npx hardaht lz:oapp-read:config:init --contract-name --oapp-config ``` This will produce a new `layerzero.config.ts` file based on the `READ_CONTRACT_NAME` and available networks in your hardhat project. Key configuration details include: - `contracts`: Define the contracts you intend to interact with. In this example, the origin chain Arbitrum has the child `OAppRead` contract address. - `readLibrary`: The address of the Read Library (`ReadLib1002`) contract deployed on your network. - `readChannels`: Specify the read channels you want to activate. - `readConfig`: Configure the Read Library settings, including required Decentralized Verifier Networks (DVNs) and the executor address. ### Wire Your Read OApp After configuring `layerzero.config.ts`, execute the following command to wire your Read OApp: ``` npx hardhat lz:oapp-read:wire --oapp-config layerzero.config.ts ``` This command sets up the necessary connections and configurations based on your `layerzero.config.ts` file, enabling your OApp to perform read operations. ### Debugging Malformed or Unresolvable Commands LayerZero provides a Hardhat task to assist in debugging and resolving read commands. This task helps identify and troubleshoot issues with your read commands. To resolve a read command, run the following command in your terminal: ```bash npx hardhat lz:read:resolve-command --command ``` If the command is correctly formed and resolvable, the task will provide the expected target data. If the command is [Malformed](./overview.md#debugging-read-commands) or [Unresolvable](./overview.md#debugging-read-commands), the task will output relevant error messages to help you pinpoint the issue. Based on the feedback from the resolver task, make necessary adjustments to your command construction or target contract configurations. ### Testing Contracts Ensuring the reliability of your `lzRead` setup involves thorough testing. LayerZero provides a `TestHelper` tailored for Foundry unit tests, enabling you to simulate cross-chain reads in your tests. For lzRead, this helper has been extended to support read-specific functionalities. Some limitations of this testing method should be understood: - **Command Validation**: Does not simulate whether a command is `malformed` or `unresolvable`. To fully ensure contract functionality, it's recommended to conduct tests on mainnet even after unit testing, to ensure that the mocked state in your unit tests matches the behaviour found on-chain. --- --- title: Supported Data Types sidebar_label: Supported Data Types --- LayerZero Read currently supports a variety of data types for reading external state via `calldata` from `EVMCallRequestV1`. The following types of `calldata` methods are supported: 1. **Public State Variables**: Direct access to `public` state variables on target contracts. 2. **View or Pure Functions**: Functions that do not modify the blockchain state and only return data. 3. **Non-View or Pure Functions Returning Data**: Functions that are not marked as `view` or `pure`, but do not alter the on-chain state and only return data. These supported data types enable developers to fetch and utilize external state data efficiently without incurring unnecessary gas costs or affecting the target blockchain's state. ## Function Types See the simple data type examples below for reference on how to implement `EVMCallRequestV1` in your `OAppRead` application. ### State Variables Public state variables in Solidity automatically generate `getter` functions, making them easily accessible for read operations. LayerZero Read can directly interact with these `getter` functions to retrieve the current state: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; /** * @title ExampleContract * @notice A simple contract with a public state variable. */ contract ExampleContract { // a public data variable on the target data chain to read from uint256 public data; constructor(uint256 _data) { data = _data; } } ``` To read the data variable using `lzRead`, you can encode the getter function call as follows: ```solidity bytes memory callData = abi.encodeWithSelector(ExampleContract.data.selector); readRequests[i] = EVMCallRequestV1({ appRequestLabel: uint16(i + 1), // arbitrary request label, for OApp filtering purposes targetEid: targetEid, isBlockNum: false, blockNumOrTimestamp: uint64(block.timestamp), confirmations: 15, // Example set block confirmations to wait to 15 blocks to: 0x1234567890123456789012345678901234567890, // Dummy address where ExampleContract is deployed callData: callData }); ``` ### View or Pure Functions `view` and `pure` functions are ideal for read operations as they do not modify the blockchain state. LayerZero Read can seamlessly interact with these functions to retrieve necessary data. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; /** * @title MathContract * @notice A contract with a pure function for mathematical operations. */ contract MathContract { /** * @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; } } ``` To read the result of the `add` function using `lzRead`: ```solidity bytes memory callData = abi.encodeWithSelector(MathContract.add.selector, 5, 10); readRequests[i] = EVMCallRequestV1({ appRequestLabel: uint16(i + 1), targetEid: targetEid, isBlockNum: false, blockNumOrTimestamp: uint64(block.timestamp), confirmations: 15, // Set to 15 blocks to: 0x1234567890123456789012345678901234567890, // Dummy contract address callData: callData }); ``` ## Non-View or Pure Functions Returning Data Some functions are not marked as `view` or `pure`, but still do not modify the on-chain state. These functions can also be utilized with `lzRead` as long as they only return data without performing state changes. For example, Uniswap V3's `IQuoterV2` relies on calling non-view functions and reverting to compute the result. This is not gas efficient and should not be called on-chain, making `lzRead` a great option for retrieving the state: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; /// @title QuoterV2 Interface /// @notice Supports quoting the calculated amounts from exact input or exact output swaps. /// @notice For each pool also tells you the number of initialized ticks crossed and the sqrt price of the pool after the swap. /// @dev These functions are not marked view because they rely on calling non-view functions and reverting /// to compute the result. They are also not gas efficient and should not be called on-chain. interface IQuoterV2 { /// @notice Returns the amount out received for a given exact input but for a swap of a single pool /// @param params The params for the quote, encoded as `QuoteExactInputSingleParams` /// tokenIn The token being swapped in /// tokenOut The token being swapped out /// fee The fee of the token pool to consider for the pair /// amountIn The desired input amount /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap /// @return amountOut The amount of `tokenOut` that would be received /// @return sqrtPriceX96After The sqrt price of the pool after the swap /// @return initializedTicksCrossed The number of initialized ticks that the swap crossed /// @return gasEstimate The estimate of the gas that the swap consumes function quoteExactInputSingle(QuoteExactInputSingleParams memory params) external returns ( uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate ); } ``` To read a quote for an `amountOut` for a specific token pair function using `lzRead`: ```solidity // Define the QuoteExactInputSingleParams IQuoterV2.QuoteExactInputSingleParams memory params = IQuoterV2.QuoteExactInputSingleParams({ tokenIn: config.tokenInAddress, tokenOut: config.tokenOutAddress, amountIn: 1 ether, // amountIn: 1 WETH fee: config.fee, sqrtPriceLimitX96: 0 // No price limit }); // @notice Encode the function call // @dev From Uniswap Docs, this function is not marked view because it relies on calling non-view // functions and reverting to compute the result. It is also not gas efficient and should not // be called on-chain. We take advantage of lzRead to call this function off-chain and get the result // returned back on-chain to the OApp's _lzReceive method. // https://docs.uniswap.org/contracts/v3/reference/periphery/interfaces/IQuoterV2 bytes memory callData = abi.encodeWithSelector(IQuoterV2.quoteExactInputSingle.selector, params); readRequests[i] = EVMCallRequestV1({ appRequestLabel: uint16(i + 1), targetEid: targetEid, isBlockNum: false, blockNumOrTimestamp: uint64(block.timestamp), confirmations: config.confirmations, to: config.quoterAddress, callData: callData }); ``` --- # Solidity API ## EndpointV2 ### lzToken ```solidity address lzToken ``` This stores the address of the LayerZero token, which may be used for paying messaging fees. It enables applications to settle cross-chain communication costs using LayerZero's native token, where applicable. ### delegates ```solidity 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 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 cross-chain 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 function quote(struct MessagingParams _params, address _sender) external view returns (struct MessagingFee) ``` This function returns a fee estimate for sending a cross-chain 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 cross-chain messages. ### initialize ```solidity 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 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 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 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 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 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 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 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 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 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 address blockedLibrary ``` The `blockedLibrary` is a specific library that is no longer allowed for sending or receiving messages. ### registeredLibraries ```solidity address[] registeredLibraries ``` An array storing the addresses of all libraries that are registered and can be used for message sending or receiving. ### isRegisteredLibrary ```solidity 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 cross-chain communication. ### EMPTY_PAYLOAD_HASH ```solidity 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 event PacketSent(bytes encodedPayload, bytes options, address sendLibrary) ``` Emitted when a message packet is sent to a destination endpoint. ### PacketVerified ```solidity event PacketVerified(struct Origin origin, address receiver, bytes32 payloadHash) ``` Emitted when a message packet is verified on the destination endpoint. ### PacketDelivered ```solidity event PacketDelivered(struct Origin origin, address receiver) ``` Emitted when a message packet is successfully delivered to the destination receiver. ### LzReceiveAlert ```solidity 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 event LzTokenSet(address token) ``` Emitted when the LayerZero token address is set or updated. ### DelegateSet ```solidity event DelegateSet(address sender, address delegate) ``` Emitted when a delegate is authorized by an OApp to configure LayerZero settings. ### quote ```solidity function quote(struct MessagingParams _params, address _sender) external view returns (struct MessagingFee) ``` ### send ```solidity function send(struct MessagingParams _params, address _refundAddress) external payable returns (struct MessagingReceipt) ``` ### verify ```solidity function verify(struct Origin _origin, address _receiver, bytes32 _payloadHash) external ``` ### verifiable ```solidity function verifiable(struct Origin _origin, address _receiver) external view returns (bool) ``` ### initializable ```solidity function initializable(struct Origin _origin, address _receiver) external view returns (bool) ``` ### lzReceive ```solidity function lzReceive(struct Origin _origin, address _receiver, bytes32 _guid, bytes _message, bytes _extraData) external payable ``` ### clear ```solidity function clear(address _oapp, struct Origin _origin, bytes32 _guid, bytes _message) external ``` ### setLzToken ```solidity function setLzToken(address _lzToken) external ``` ### lzToken ```solidity function lzToken() external view returns (address) ``` ### nativeToken ```solidity function nativeToken() external view returns (address) ``` ### setDelegate ```solidity function setDelegate(address _delegate) external ``` ## ILayerZeroReceiver This interface defines the core message-receiving functionality on LayerZero to be implemented by receiver applications. ### allowInitializePath ```solidity function allowInitializePath(struct Origin _origin) external view returns (bool) ``` Returns whether the path from the origin can be initialized. ### nextNonce ```solidity function nextNonce(uint32 _eid, bytes32 _sender) external view returns (uint64) ``` Returns the next nonce for a sender on the specified endpoint. ### lzReceive ```solidity 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 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 function setConfig(address _oapp, struct SetConfigParam[] _config) external ``` Allows an OApp (Omnichain Application) to set configuration parameters for a specific messaging library. ### getConfig ```solidity 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 function isSupportedEid(uint32 _eid) external view returns (bool) ``` Checks if the messaging library supports a specific endpoint ID (`_eid`). ### version ```solidity 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 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 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 struct Timeout { address lib; uint256 expiry; } ``` The `Timeout` struct defines the expiration settings for a messaging library that has been changed. ### LibraryRegistered ```solidity event LibraryRegistered(address newLib) ``` Emitted when a new library is registered. ### DefaultSendLibrarySet ```solidity event DefaultSendLibrarySet(uint32 eid, address newLib) ``` Emitted when the default send library is set for a specific endpoint. ### DefaultReceiveLibrarySet ```solidity event DefaultReceiveLibrarySet(uint32 eid, address newLib) ``` Emitted when the default receive library is set for a specific endpoint. ### DefaultReceiveLibraryTimeoutSet ```solidity event DefaultReceiveLibraryTimeoutSet(uint32 eid, address oldLib, uint256 expiry) ``` Emitted when a timeout is set for the default receive library. ### SendLibrarySet ```solidity event SendLibrarySet(address sender, uint32 eid, address newLib) ``` Emitted when a send library is set for an OApp. ### ReceiveLibrarySet ```solidity event ReceiveLibrarySet(address receiver, uint32 eid, address newLib) ``` Emitted when a receive library is set for an OApp. ### ReceiveLibraryTimeoutSet ```solidity event ReceiveLibraryTimeoutSet(address receiver, uint32 eid, address oldLib, uint256 timeout) ``` Emitted when a receive library timeout is set for an OApp. ### registerLibrary ```solidity function registerLibrary(address _lib) external ``` Registers a new messaging library that will be available for endpoints. ### isRegisteredLibrary ```solidity function isRegisteredLibrary(address _lib) external view returns (bool) ``` Checks if a messaging library is registered. ### getRegisteredLibraries ```solidity function getRegisteredLibraries() external view returns (address[]) ``` Returns a list of all registered libraries. ### setDefaultSendLibrary ```solidity function setDefaultSendLibrary(uint32 _eid, address _newLib) external ``` Sets the default send library for a specific endpoint. ### defaultSendLibrary ```solidity function defaultSendLibrary(uint32 _eid) external view returns (address) ``` Gets the current default send library for a specific endpoint. ### setDefaultReceiveLibrary ```solidity 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 function defaultReceiveLibrary(uint32 _eid) external view returns (address) ``` Gets the current default receive library for a specific endpoint. ### setDefaultReceiveLibraryTimeout ```solidity function setDefaultReceiveLibraryTimeout(uint32 _eid, address _lib, uint256 _expiry) external ``` Sets the timeout for a default receive library. ### defaultReceiveLibraryTimeout ```solidity function defaultReceiveLibraryTimeout(uint32 _eid) external view returns (address lib, uint256 expiry) ``` Gets the default receive library timeout for a specific endpoint. ### isSupportedEid ```solidity function isSupportedEid(uint32 _eid) external view returns (bool) ``` ### isValidReceiveLibrary ```solidity function isValidReceiveLibrary(address _receiver, uint32 _eid, address _lib) external view returns (bool) ``` ### setSendLibrary ```solidity function setSendLibrary(address _oapp, uint32 _eid, address _newLib) external ``` Sets a send library for an OApp for a specific endpoint. ### getSendLibrary ```solidity function getSendLibrary(address _sender, uint32 _eid) external view returns (address lib) ``` ### isDefaultSendLibrary ```solidity function isDefaultSendLibrary(address _sender, uint32 _eid) external view returns (bool) ``` ### setReceiveLibrary ```solidity function setReceiveLibrary(address _oapp, uint32 _eid, address _newLib, uint256 _gracePeriod) external ``` ### getReceiveLibrary ```solidity function getReceiveLibrary(address _receiver, uint32 _eid) external view returns (address lib, bool isDefault) ``` ### setReceiveLibraryTimeout ```solidity function setReceiveLibraryTimeout(address _oapp, uint32 _eid, address _lib, uint256 _expiry) external ``` ### receiveLibraryTimeout ```solidity function receiveLibraryTimeout(address _receiver, uint32 _eid) external view returns (address lib, uint256 expiry) ``` ### setConfig ```solidity function setConfig(address _oapp, address _lib, struct SetConfigParam[] _params) external ``` ### getConfig ```solidity function getConfig(address _oapp, address _lib, uint32 _eid, uint32 _configType) external view returns (bytes config) ``` ## IMessagingChannel ### InboundNonceSkipped ```solidity event InboundNonceSkipped(uint32 srcEid, bytes32 sender, address receiver, uint64 nonce) ``` ### PacketNilified ```solidity event PacketNilified(uint32 srcEid, bytes32 sender, address receiver, uint64 nonce, bytes32 payloadHash) ``` ### PacketBurnt ```solidity event PacketBurnt(uint32 srcEid, bytes32 sender, address receiver, uint64 nonce, bytes32 payloadHash) ``` ### eid ```solidity function eid() external view returns (uint32) ``` ### skip ```solidity function skip(address _oapp, uint32 _srcEid, bytes32 _sender, uint64 _nonce) external ``` ### nilify ```solidity function nilify(address _oapp, uint32 _srcEid, bytes32 _sender, uint64 _nonce, bytes32 _payloadHash) external ``` ### burn ```solidity function burn(address _oapp, uint32 _srcEid, bytes32 _sender, uint64 _nonce, bytes32 _payloadHash) external ``` ### nextGuid ```solidity function nextGuid(address _sender, uint32 _dstEid, bytes32 _receiver) external view returns (bytes32) ``` ### inboundNonce ```solidity function inboundNonce(address _receiver, uint32 _srcEid, bytes32 _sender) external view returns (uint64) ``` ### outboundNonce ```solidity function outboundNonce(address _sender, uint32 _dstEid, bytes32 _receiver) external view returns (uint64) ``` ### inboundPayloadHash ```solidity function inboundPayloadHash(address _receiver, uint32 _srcEid, bytes32 _sender, uint64 _nonce) external view returns (bytes32) ``` ### lazyInboundNonce ```solidity function lazyInboundNonce(address _receiver, uint32 _srcEid, bytes32 _sender) external view returns (uint64) ``` ## IMessagingComposer ### ComposeSent ```solidity event ComposeSent(address from, address to, bytes32 guid, uint16 index, bytes message) ``` ### ComposeDelivered ```solidity event ComposeDelivered(address from, address to, bytes32 guid, uint16 index) ``` ### LzComposeAlert ```solidity event LzComposeAlert(address from, address to, address executor, bytes32 guid, uint16 index, uint256 gas, uint256 value, bytes message, bytes extraData, bytes reason) ``` ### composeQueue ```solidity function composeQueue(address _from, address _to, bytes32 _guid, uint16 _index) external view returns (bytes32 messageHash) ``` ### sendCompose ```solidity function sendCompose(address _to, bytes32 _guid, uint16 _index, bytes _message) external ``` ### lzCompose ```solidity function lzCompose(address _from, address _to, bytes32 _guid, uint16 _index, bytes _message, bytes _extraData) external payable ``` ## IMessagingContext ### isSendingMessage ```solidity function isSendingMessage() external view returns (bool) ``` ### getSendContext ```solidity function getSendContext() external view returns (uint32 dstEid, address sender) ``` ## Packet ```solidity 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 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 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 function setTreasury(address _treasury) external ``` Sets the treasury address to receive collected fees. ### withdrawFee ```solidity function withdrawFee(address _to, uint256 _amount) external ``` Withdraws native token fees collected by the contract. ### withdrawLzTokenFee ```solidity 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 error AddressCast_InvalidSizeForAddress() ``` Thrown when the size of the byte array for an address is invalid. ### AddressCast_InvalidAddress ```solidity error AddressCast_InvalidAddress() ``` Thrown when an invalid address is provided. ### toBytes32 ```solidity function toBytes32(bytes _addressBytes) internal pure returns (bytes32 result) ``` Casts a byte array to a `bytes32` representation of an address. ### toBytes32 ```solidity function toBytes32(address _address) internal pure returns (bytes32 result) ``` Casts an `address` to its `bytes32` representation. ### toBytes ```solidity 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 function toAddress(bytes32 _addressBytes32) internal pure returns (address result) ``` Casts a `bytes32` representation of an address back to an address. ### toAddress ```solidity 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 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 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 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 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 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 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 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 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 error LZ_LzTokenUnavailable() ``` ### LZ_InvalidReceiveLibrary ```solidity error LZ_InvalidReceiveLibrary() ``` ### LZ_InvalidNonce ```solidity error LZ_InvalidNonce(uint64 nonce) ``` ### LZ_InvalidArgument ```solidity error LZ_InvalidArgument() ``` ### LZ_InvalidExpiry ```solidity error LZ_InvalidExpiry() ``` ### LZ_InvalidAmount ```solidity error LZ_InvalidAmount(uint256 required, uint256 supplied) ``` ### LZ_OnlyRegisteredOrDefaultLib ```solidity error LZ_OnlyRegisteredOrDefaultLib() ``` ### LZ_OnlyRegisteredLib ```solidity error LZ_OnlyRegisteredLib() ``` ### LZ_OnlyNonDefaultLib ```solidity error LZ_OnlyNonDefaultLib() ``` ### LZ_Unauthorized ```solidity error LZ_Unauthorized() ``` ### LZ_DefaultSendLibUnavailable ```solidity error LZ_DefaultSendLibUnavailable() ``` ### LZ_DefaultReceiveLibUnavailable ```solidity error LZ_DefaultReceiveLibUnavailable() ``` ### LZ_PathNotInitializable ```solidity error LZ_PathNotInitializable() ``` ### LZ_PathNotVerifiable ```solidity error LZ_PathNotVerifiable() ``` ### LZ_OnlySendLib ```solidity error LZ_OnlySendLib() ``` ### LZ_OnlyReceiveLib ```solidity error LZ_OnlyReceiveLib() ``` ### LZ_UnsupportedEid ```solidity error LZ_UnsupportedEid() ``` ### LZ_UnsupportedInterface ```solidity error LZ_UnsupportedInterface() ``` ### LZ_AlreadyRegistered ```solidity error LZ_AlreadyRegistered() ``` ### LZ_SameValue ```solidity error LZ_SameValue() ``` ### LZ_InvalidPayloadHash ```solidity error LZ_InvalidPayloadHash() ``` ### LZ_PayloadHashNotFound ```solidity error LZ_PayloadHashNotFound(bytes32 expected, bytes32 actual) ``` ### LZ_ComposeNotFound ```solidity error LZ_ComposeNotFound(bytes32 expected, bytes32 actual) ``` ### LZ_ComposeExists ```solidity error LZ_ComposeExists() ``` ### LZ_SendReentrancy ```solidity error LZ_SendReentrancy() ``` ### LZ_NotImplemented ```solidity error LZ_NotImplemented() ``` ### LZ_InsufficientFee ```solidity error LZ_InsufficientFee(uint256 requiredNative, uint256 suppliedNative, uint256 requiredLzToken, uint256 suppliedLzToken) ``` ### LZ_ZeroLzTokenFee ```solidity error LZ_ZeroLzTokenFee() ``` ## GUID ### generate ```solidity function generate(uint64 _nonce, uint32 _srcEid, address _sender, uint32 _dstEid, bytes32 _receiver) internal pure returns (bytes32) ``` ## Transfer ### ADDRESS_ZERO ```solidity address ADDRESS_ZERO ``` ### Transfer_NativeFailed ```solidity error Transfer_NativeFailed(address _to, uint256 _value) ``` ### Transfer_ToAddressIsZero ```solidity error Transfer_ToAddressIsZero() ``` ### native ```solidity function native(address _to, uint256 _value) internal ``` ### token ```solidity function token(address _token, address _to, uint256 _value) internal ``` ### nativeOrToken ```solidity function nativeOrToken(address _token, address _to, uint256 _value) internal ``` ## BlockedMessageLib ### supportsInterface ```solidity function supportsInterface(bytes4 interfaceId) public view returns (bool) ``` _See `IERC165` and `supportsInterface`._ ### version ```solidity function version() external pure returns (uint64 major, uint8 minor, uint8 endpointVersion) ``` ### messageLibType ```solidity function messageLibType() external pure returns (enum MessageLibType) ``` ### isSupportedEid ```solidity function isSupportedEid(uint32) external pure returns (bool) ``` ### fallback ```solidity fallback() external ``` ## BitMaps ### get ```solidity function get(BitMap256 bitmap, uint8 index) internal pure returns (bool) ``` _Returns whether the bit at `index` is set._ ### set ```solidity function set(BitMap256 bitmap, uint8 index) internal pure returns (BitMap256) ``` _Sets the bit at `index`._ ## ExecutorOptions ### WORKER_ID ```solidity uint8 WORKER_ID ``` ### OPTION_TYPE_LZRECEIVE ```solidity uint8 OPTION_TYPE_LZRECEIVE ``` ### OPTION_TYPE_NATIVE_DROP ```solidity uint8 OPTION_TYPE_NATIVE_DROP ``` ### OPTION_TYPE_LZCOMPOSE ```solidity uint8 OPTION_TYPE_LZCOMPOSE ``` ### OPTION_TYPE_ORDERED_EXECUTION ```solidity uint8 OPTION_TYPE_ORDERED_EXECUTION ``` ### Executor_InvalidLzReceiveOption ```solidity error Executor_InvalidLzReceiveOption() ``` ### Executor_InvalidNativeDropOption ```solidity error Executor_InvalidNativeDropOption() ``` ### Executor_InvalidLzComposeOption ```solidity error Executor_InvalidLzComposeOption() ``` ### nextExecutorOption ```solidity 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 function decodeLzReceiveOption(bytes _option) internal pure returns (uint128 gas, uint128 value) ``` ### decodeNativeDropOption ```solidity function decodeNativeDropOption(bytes _option) internal pure returns (uint128 amount, bytes32 receiver) ``` ### decodeLzComposeOption ```solidity function decodeLzComposeOption(bytes _option) internal pure returns (uint16 index, uint128 gas, uint128 value) ``` ### encodeLzReceiveOption ```solidity function encodeLzReceiveOption(uint128 _gas, uint128 _value) internal pure returns (bytes) ``` ### encodeNativeDropOption ```solidity function encodeNativeDropOption(uint128 _amount, bytes32 _receiver) internal pure returns (bytes) ``` ### encodeLzComposeOption ```solidity function encodeLzComposeOption(uint16 _index, uint128 _gas, uint128 _value) internal pure returns (bytes) ``` ## PacketV1Codec ### PACKET_VERSION ```solidity uint8 PACKET_VERSION ``` ### encode ```solidity function encode(struct Packet _packet) internal pure returns (bytes encodedPacket) ``` ### encodePacketHeader ```solidity function encodePacketHeader(struct Packet _packet) internal pure returns (bytes) ``` ### encodePayload ```solidity function encodePayload(struct Packet _packet) internal pure returns (bytes) ``` ### header ```solidity function header(bytes _packet) internal pure returns (bytes) ``` ### version ```solidity function version(bytes _packet) internal pure returns (uint8) ``` ### nonce ```solidity function nonce(bytes _packet) internal pure returns (uint64) ``` ### srcEid ```solidity function srcEid(bytes _packet) internal pure returns (uint32) ``` ### sender ```solidity function sender(bytes _packet) internal pure returns (bytes32) ``` ### senderAddressB20 ```solidity function senderAddressB20(bytes _packet) internal pure returns (address) ``` ### dstEid ```solidity function dstEid(bytes _packet) internal pure returns (uint32) ``` ### receiver ```solidity function receiver(bytes _packet) internal pure returns (bytes32) ``` ### receiverB20 ```solidity function receiverB20(bytes _packet) internal pure returns (address) ``` ### guid ```solidity function guid(bytes _packet) internal pure returns (bytes32) ``` ### message ```solidity function message(bytes _packet) internal pure returns (bytes) ``` ### payload ```solidity function payload(bytes _packet) internal pure returns (bytes) ``` ### payloadHash ```solidity 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 address endpoint ``` Holds the address of the LayerZero endpoint on this chain. ### localEid ```solidity uint32 localEid ``` A unique identifier (Eid) for the local chain. ### LZ_MessageLib_OnlyEndpoint ```solidity error LZ_MessageLib_OnlyEndpoint() ``` Error thrown when a function is accessed by a non-endpoint address. ### onlyEndpoint ```solidity modifier onlyEndpoint() ``` A modifier ensuring that only the LayerZero endpoint can call specific functions. ### constructor ```solidity 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 constructor(address _endpoint) internal ``` Initializes the contract with the LayerZero endpoint. ### supportsInterface ```solidity function supportsInterface(bytes4 _interfaceId) public view virtual returns (bool) ``` Determines whether the contract supports a specific interface. ### messageLibType ```solidity 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 struct WorkerOptions { uint8 workerId; bytes options; } ``` Defines options for specific worker configurations (e.g., a worker ID and additional options). ## SetDefaultExecutorConfigParam ```solidity 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 struct ExecutorConfig { uint32 maxMessageSize; address executor; } ``` ## SendLibBase _base contract for both SendLibBaseE1 and SendLibBaseE2_ ### TREASURY_MAX_COPY ```solidity uint16 TREASURY_MAX_COPY ``` ### treasuryGasLimit ```solidity uint256 treasuryGasLimit ``` ### treasuryNativeFeeCap ```solidity uint256 treasuryNativeFeeCap ``` ### treasury ```solidity address treasury ``` ### executorConfigs ```solidity mapping(address => mapping(uint32 => struct ExecutorConfig)) executorConfigs ``` ### fees ```solidity mapping(address => uint256) fees ``` ### ExecutorFeePaid ```solidity event ExecutorFeePaid(address executor, uint256 fee) ``` ### TreasurySet ```solidity event TreasurySet(address treasury) ``` ### DefaultExecutorConfigsSet ```solidity event DefaultExecutorConfigsSet(struct SetDefaultExecutorConfigParam[] params) ``` ### ExecutorConfigSet ```solidity event ExecutorConfigSet(address oapp, uint32 eid, struct ExecutorConfig config) ``` ### TreasuryNativeFeeCapSet ```solidity event TreasuryNativeFeeCapSet(uint256 newTreasuryNativeFeeCap) ``` ### LZ_MessageLib_InvalidMessageSize ```solidity error LZ_MessageLib_InvalidMessageSize(uint256 actual, uint256 max) ``` ### LZ_MessageLib_InvalidAmount ```solidity error LZ_MessageLib_InvalidAmount(uint256 requested, uint256 available) ``` ### LZ_MessageLib_TransferFailed ```solidity error LZ_MessageLib_TransferFailed() ``` ### LZ_MessageLib_InvalidExecutor ```solidity error LZ_MessageLib_InvalidExecutor() ``` ### LZ_MessageLib_ZeroMessageSize ```solidity error LZ_MessageLib_ZeroMessageSize() ``` ### constructor ```solidity constructor(address _endpoint, uint32 _localEid, uint256 _treasuryGasLimit, uint256 _treasuryNativeFeeCap) internal ``` ### setDefaultExecutorConfigs ```solidity function setDefaultExecutorConfigs(struct SetDefaultExecutorConfigParam[] _params) external ``` ### setTreasuryNativeFeeCap ```solidity function setTreasuryNativeFeeCap(uint256 _newTreasuryNativeFeeCap) external ``` _the new value can not be greater than the old value, i.e. down only_ ### getExecutorConfig ```solidity function getExecutorConfig(address _oapp, uint32 _remoteEid) public view returns (struct ExecutorConfig rtnConfig) ``` ### \_assertMessageSize ```solidity function _assertMessageSize(uint256 _actual, uint256 _max) internal pure ``` ### \_payExecutor ```solidity function _payExecutor(address _executor, uint32 _dstEid, address _sender, uint256 _msgSize, bytes _executorOptions) internal returns (uint256 executorFee) ``` ### \_payTreasury ```solidity function _payTreasury(address _sender, uint32 _dstEid, uint256 _totalNativeFee, bool _payInLzToken) internal returns (uint256 treasuryNativeFee, uint256 lzTokenFee) ``` ### \_quote ```solidity 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 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 function _parseTreasuryResult(uint256 _totalNativeFee, bool _payInLzToken, bool _success, bytes _result) internal view returns (uint256 nativeFee, uint256 lzTokenFee) ``` ### \_debitFee ```solidity function _debitFee(uint256 _amount) internal ``` _authenticated by msg.sender only_ ### \_setTreasury ```solidity function _setTreasury(address _treasury) internal ``` ### \_setExecutorConfig ```solidity function _setExecutorConfig(uint32 _remoteEid, address _oapp, struct ExecutorConfig _config) internal ``` ### \_quoteVerifier ```solidity 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 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 event NativeFeeWithdrawn(address worker, address receiver, uint256 amount) ``` ### LzTokenFeeWithdrawn ```solidity event LzTokenFeeWithdrawn(address lzToken, address receiver, uint256 amount) ``` ### LZ_MessageLib_NotTreasury ```solidity error LZ_MessageLib_NotTreasury() ``` ### LZ_MessageLib_CannotWithdrawAltToken ```solidity error LZ_MessageLib_CannotWithdrawAltToken() ``` ### constructor ```solidity constructor(address _endpoint, uint256 _treasuryGasLimit, uint256 _treasuryNativeFeeCap) internal ``` ### supportsInterface ```solidity function supportsInterface(bytes4 _interfaceId) public view virtual returns (bool) ``` ### send ```solidity function send(struct Packet _packet, bytes _options, bool _payInLzToken) public virtual returns (struct MessagingFee, bytes) ``` ### setTreasury ```solidity function setTreasury(address _treasury) external ``` ### withdrawFee ```solidity function withdrawFee(address _to, uint256 _amount) external ``` _E2 only_ ### withdrawLzTokenFee ```solidity 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 function quote(struct Packet _packet, bytes _options, bool _payInLzToken) external view returns (struct MessagingFee) ``` ### messageLibType ```solidity function messageLibType() external pure virtual returns (enum MessageLibType) ``` ### \_payWorkers ```solidity function _payWorkers(struct Packet _packet, bytes _options) internal returns (bytes encodedPacket, uint256 totalNativeFee) ``` 1/ handle executor 2/ handle other workers ### \_payVerifier ```solidity function _payVerifier(struct Packet _packet, struct WorkerOptions[] _options) internal virtual returns (uint256 otherWorkerFees, bytes encodedPacket) ``` ### receive ```solidity receive() external payable virtual ``` ## Treasury ### nativeBP ```solidity uint256 nativeBP ``` ### lzTokenFee ```solidity uint256 lzTokenFee ``` ### lzTokenEnabled ```solidity bool lzTokenEnabled ``` ### LZ_Treasury_LzTokenNotEnabled ```solidity error LZ_Treasury_LzTokenNotEnabled() ``` ### getFee ```solidity function getFee(address, uint32, uint256 _totalFee, bool _payInLzToken) external view returns (uint256) ``` ### payFee ```solidity function payFee(address, uint32, uint256 _totalFee, bool _payInLzToken) external payable returns (uint256) ``` ### setLzTokenEnabled ```solidity function setLzTokenEnabled(bool _lzTokenEnabled) external ``` ### setNativeFeeBP ```solidity function setNativeFeeBP(uint256 _nativeBP) external ``` ### setLzTokenFee ```solidity function setLzTokenFee(uint256 _lzTokenFee) external ``` ### withdrawLzToken ```solidity function withdrawLzToken(address _messageLib, address _lzToken, address _to, uint256 _amount) external ``` ### withdrawNativeFee ```solidity function withdrawNativeFee(address _messageLib, address payable _to, uint256 _amount) external ``` ### withdrawToken ```solidity function withdrawToken(address _token, address _to, uint256 _amount) external ``` ### \_getFee ```solidity function _getFee(uint256 _totalFee, bool _payInLzToken) internal view returns (uint256) ``` ## Worker ### MESSAGE_LIB_ROLE ```solidity bytes32 MESSAGE_LIB_ROLE ``` ### ALLOWLIST ```solidity bytes32 ALLOWLIST ``` ### DENYLIST ```solidity bytes32 DENYLIST ``` ### ADMIN_ROLE ```solidity bytes32 ADMIN_ROLE ``` ### workerFeeLib ```solidity address workerFeeLib ``` ### allowlistSize ```solidity uint64 allowlistSize ``` ### defaultMultiplierBps ```solidity uint16 defaultMultiplierBps ``` ### priceFeed ```solidity address priceFeed ``` ### supportedOptionTypes ```solidity mapping(uint32 => uint8[]) supportedOptionTypes ``` ### constructor ```solidity 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 modifier onlyAcl(address _sender) ``` ### hasAcl ```solidity 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 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 function setPriceFeed(address _priceFeed) external ``` #### Parameters | Name | Type | Description | | ----------- | ------- | ------------------ | | \_priceFeed | address | price feed address | ### setWorkerFeeLib ```solidity function setWorkerFeeLib(address _workerFeeLib) external ``` #### Parameters | Name | Type | Description | | -------------- | ------- | ---------------------- | | \_workerFeeLib | address | worker fee lib address | ### setDefaultMultiplierBps ```solidity function setDefaultMultiplierBps(uint16 _multiplierBps) external ``` #### Parameters | Name | Type | Description | | --------------- | ------ | --------------------------------- | | \_multiplierBps | uint16 | default multiplier for worker fee | ### withdrawFee ```solidity 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 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 function setSupportedOptionTypes(uint32 _eid, uint8[] _optionTypes) external ``` ### getSupportedOptionTypes ```solidity function getSupportedOptionTypes(uint32 _eid) external view returns (uint8[]) ``` ### \_grantRole ```solidity 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 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 function renounceRole(bytes32, address) public pure ``` _overrides AccessControl to disable renouncing of roles_ ## TargetParam ```solidity struct TargetParam { uint8 idx; address addr; } ``` ## DVNParam ```solidity struct DVNParam { uint16 idx; address addr; } ``` ## IExecutor ### DstConfigParam ```solidity struct DstConfigParam { uint32 dstEid; uint64 lzReceiveBaseGas; uint64 lzComposeBaseGas; uint16 multiplierBps; uint128 floorMarginUSD; uint128 nativeCap; } ``` ### DstConfig ```solidity struct DstConfig { uint64 lzReceiveBaseGas; uint16 multiplierBps; uint128 floorMarginUSD; uint128 nativeCap; uint64 lzComposeBaseGas; } ``` ### ExecutionParams ```solidity struct ExecutionParams { address receiver; struct Origin origin; bytes32 guid; bytes message; bytes extraData; uint256 gasLimit; } ``` ### NativeDropParams ```solidity struct NativeDropParams { address receiver; uint256 amount; } ``` ### DstConfigSet ```solidity event DstConfigSet(struct IExecutor.DstConfigParam[] params) ``` ### NativeDropApplied ```solidity event NativeDropApplied(struct Origin origin, uint32 dstEid, address oapp, struct IExecutor.NativeDropParams[] params, bool[] success) ``` ### dstConfig ```solidity function dstConfig(uint32 _dstEid) external view returns (uint64, uint16, uint128, uint128, uint64) ``` ## IExecutorFeeLib ### FeeParams ```solidity struct FeeParams { address priceFeed; uint32 dstEid; address sender; uint256 calldataSize; uint16 defaultMultiplierBps; } ``` ### Executor_NoOptions ```solidity error Executor_NoOptions() ``` ### Executor_NativeAmountExceedsCap ```solidity error Executor_NativeAmountExceedsCap(uint256 amount, uint256 cap) ``` ### Executor_UnsupportedOptionType ```solidity error Executor_UnsupportedOptionType(uint8 optionType) ``` ### Executor_InvalidExecutorOptions ```solidity error Executor_InvalidExecutorOptions(uint256 cursor) ``` ### Executor_ZeroLzReceiveGasProvided ```solidity error Executor_ZeroLzReceiveGasProvided() ``` ### Executor_ZeroLzComposeGasProvided ```solidity error Executor_ZeroLzComposeGasProvided() ``` ### Executor_EidNotSupported ```solidity error Executor_EidNotSupported(uint32 eid) ``` ### getFeeOnSend ```solidity function getFeeOnSend(struct IExecutorFeeLib.FeeParams _params, struct IExecutor.DstConfig _dstConfig, bytes _options) external returns (uint256 fee) ``` ### getFee ```solidity function getFee(struct IExecutorFeeLib.FeeParams _params, struct IExecutor.DstConfig _dstConfig, bytes _options) external view returns (uint256 fee) ``` ## ILayerZeroExecutor ### assignJob ```solidity function assignJob(uint32 _dstEid, address _sender, uint256 _calldataSize, bytes _options) external returns (uint256 price) ``` ### getFee ```solidity function getFee(uint32 _dstEid, address _sender, uint256 _calldataSize, bytes _options) external view returns (uint256 price) ``` ## ILayerZeroTreasury ### getFee ```solidity function getFee(address _sender, uint32 _dstEid, uint256 _totalNativeFee, bool _payInLzToken) external view returns (uint256 fee) ``` ### payFee ```solidity function payFee(address _sender, uint32 _dstEid, uint256 _totalNativeFee, bool _payInLzToken) external payable returns (uint256 fee) ``` ## IWorker ### SetWorkerLib ```solidity event SetWorkerLib(address workerLib) ``` ### SetPriceFeed ```solidity event SetPriceFeed(address priceFeed) ``` ### SetDefaultMultiplierBps ```solidity event SetDefaultMultiplierBps(uint16 multiplierBps) ``` ### SetSupportedOptionTypes ```solidity event SetSupportedOptionTypes(uint32 dstEid, uint8[] optionTypes) ``` ### Withdraw ```solidity event Withdraw(address lib, address to, uint256 amount) ``` ### Worker_NotAllowed ```solidity error Worker_NotAllowed() ``` ### Worker_OnlyMessageLib ```solidity error Worker_OnlyMessageLib() ``` ### Worker_RoleRenouncingDisabled ```solidity error Worker_RoleRenouncingDisabled() ``` ### setPriceFeed ```solidity function setPriceFeed(address _priceFeed) external ``` ### priceFeed ```solidity function priceFeed() external view returns (address) ``` ### setDefaultMultiplierBps ```solidity function setDefaultMultiplierBps(uint16 _multiplierBps) external ``` ### defaultMultiplierBps ```solidity function defaultMultiplierBps() external view returns (uint16) ``` ### withdrawFee ```solidity function withdrawFee(address _lib, address _to, uint256 _amount) external ``` ### setSupportedOptionTypes ```solidity function setSupportedOptionTypes(uint32 _eid, uint8[] _optionTypes) external ``` ### getSupportedOptionTypes ```solidity function getSupportedOptionTypes(uint32 _eid) external view returns (uint8[]) ``` ## SafeCall _copied from https://github.com/nomad-xyz/ExcessivelySafeCall/blob/main/src/ExcessivelySafeCall.sol._ ### safeCall ```solidity 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 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 event Executed(uint32 vid, address target, bytes callData, uint256 expiration, bytes signatures) ``` ### vid ```solidity uint32 vid ``` ### constructor ```solidity constructor(uint32 _vid) public ``` ### execute ```solidity function execute(struct ExecuteParam[] _params) external ``` ### verify ```solidity function verify(bytes _packetHeader, bytes32 _payloadHash, uint64 _confirmations) external ``` ## ExecutorMock ### NativeDropMeta ```solidity event NativeDropMeta(uint32 srcEid, bytes32 sender, uint64 nonce, uint32 dstEid, address oapp, uint256 nativeDropGasLimit) ``` ### NativeDropped ```solidity event NativeDropped(address receiver, uint256 amount) ``` ### Executed301 ```solidity event Executed301(bytes packet, uint256 gasLimit) ``` ### Executed302 ```solidity event Executed302(uint32 srcEid, bytes32 sender, uint64 nonce, address receiver, bytes32 guid, bytes message, bytes extraData, uint256 gasLimit) ``` ### dstEid ```solidity uint32 dstEid ``` ### constructor ```solidity constructor(uint32 _dstEid) public ``` ### nativeDrop ```solidity function nativeDrop(struct Origin _origin, uint32 _dstEid, address _oapp, struct IExecutor.NativeDropParams[] _nativeDropParams, uint256 _nativeDropGasLimit) external payable ``` ### nativeDropAndExecute301 ```solidity function nativeDropAndExecute301(struct Origin _origin, struct IExecutor.NativeDropParams[] _nativeDropParams, uint256 _nativeDropGasLimit, bytes _packet, uint256 _gasLimit) external payable ``` ### execute301 ```solidity function execute301(bytes _packet, uint256 _gasLimit) external ``` ### nativeDropAndExecute302 ```solidity function nativeDropAndExecute302(struct IExecutor.NativeDropParams[] _nativeDropParams, uint256 _nativeDropGasLimit, struct IExecutor.ExecutionParams _executionParams) external payable ``` ### \_nativeDrop ```solidity function _nativeDrop(struct Origin _origin, uint32 _dstEid, address _oapp, struct IExecutor.NativeDropParams[] _nativeDropParams, uint256 _nativeDropGasLimit) internal ``` ## LzReceiveParam ```solidity struct LzReceiveParam { struct Origin origin; address receiver; bytes32 guid; bytes message; bytes extraData; uint256 gas; uint256 value; } ``` ## NativeDropParam ```solidity struct NativeDropParam { address _receiver; uint256 _amount; } ``` ## IReceiveUlnView ### verifiable ```solidity function verifiable(bytes _packetHeader, bytes32 _payloadHash) external view returns (enum VerificationState) ``` ## Verification ```solidity struct Verification { bool submitted; uint64 confirmations; } ``` ## ReceiveUlnBase _includes the utility functions for checking ULN states and logics_ ### hashLookup ```solidity mapping(bytes32 => mapping(bytes32 => mapping(address => struct Verification))) hashLookup ``` ### PayloadVerified ```solidity event PayloadVerified(address dvn, bytes header, uint256 confirmations, bytes32 proofHash) ``` ### LZ_ULN_InvalidPacketHeader ```solidity error LZ_ULN_InvalidPacketHeader() ``` ### LZ_ULN_InvalidPacketVersion ```solidity error LZ_ULN_InvalidPacketVersion() ``` ### LZ_ULN_InvalidEid ```solidity error LZ_ULN_InvalidEid() ``` ### LZ_ULN_Verifying ```solidity error LZ_ULN_Verifying() ``` ### verifiable ```solidity function verifiable(struct UlnConfig _config, bytes32 _headerHash, bytes32 _payloadHash) external view returns (bool) ``` ### assertHeader ```solidity function assertHeader(bytes _packetHeader, uint32 _localEid) external pure ``` ### \_verify ```solidity function _verify(bytes _packetHeader, bytes32 _payloadHash, uint64 _confirmations) internal ``` _per DVN signing function_ ### \_verified ```solidity function _verified(address _dvn, bytes32 _headerHash, bytes32 _payloadHash, uint64 _requiredConfirmation) internal view returns (bool verified) ``` ### \_verifyAndReclaimStorage ```solidity function _verifyAndReclaimStorage(struct UlnConfig _config, bytes32 _headerHash, bytes32 _payloadHash) internal ``` ### \_assertHeader ```solidity function _assertHeader(bytes _packetHeader, uint32 _localEid) internal pure ``` ### \_checkVerifiable ```solidity 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 event DVNFeePaid(address[] requiredDVNs, address[] optionalDVNs, uint256[] fees) ``` ### \_splitUlnOptions ```solidity function _splitUlnOptions(bytes _options) internal pure returns (bytes, struct WorkerOptions[]) ``` ### \_payDVNs ```solidity function _payDVNs(mapping(address => uint256) _fees, struct Packet _packet, struct WorkerOptions[] _options) internal returns (uint256 totalFee, bytes encodedPacket) ``` ---------- pay and assign jobs ---------- ### \_assignJobs ```solidity function _assignJobs(mapping(address => uint256) _fees, struct UlnConfig _ulnConfig, struct ILayerZeroDVN.AssignJobParam _param, bytes dvnOptions) internal returns (uint256 totalFee, uint256[] dvnFees) ``` ### \_quoteDVNs ```solidity function _quoteDVNs(address _sender, uint32 _dstEid, struct WorkerOptions[] _options) internal view returns (uint256 totalFee) ``` ---------- quote ---------- ### \_getFees ```solidity function _getFees(struct UlnConfig _config, uint32 _dstEid, address _sender, bytes[] _optionsArray, uint8[] _dvnIds) internal view returns (uint256 totalFee) ``` ## UlnConfig ```solidity struct UlnConfig { uint64 confirmations; uint8 requiredDVNCount; uint8 optionalDVNCount; uint8 optionalDVNThreshold; address[] requiredDVNs; address[] optionalDVNs; } ``` ## SetDefaultUlnConfigParam ```solidity struct SetDefaultUlnConfigParam { uint32 eid; struct UlnConfig config; } ``` ## UlnBase _includes the utility functions for checking ULN states and logics_ ### DEFAULT ```solidity uint8 DEFAULT ``` ### NIL_DVN_COUNT ```solidity uint8 NIL_DVN_COUNT ``` ### NIL_CONFIRMATIONS ```solidity uint64 NIL_CONFIRMATIONS ``` ### ulnConfigs ```solidity mapping(address => mapping(uint32 => struct UlnConfig)) ulnConfigs ``` ### LZ_ULN_Unsorted ```solidity error LZ_ULN_Unsorted() ``` ### LZ_ULN_InvalidRequiredDVNCount ```solidity error LZ_ULN_InvalidRequiredDVNCount() ``` ### LZ_ULN_InvalidOptionalDVNCount ```solidity error LZ_ULN_InvalidOptionalDVNCount() ``` ### LZ_ULN_AtLeastOneDVN ```solidity error LZ_ULN_AtLeastOneDVN() ``` ### LZ_ULN_InvalidOptionalDVNThreshold ```solidity error LZ_ULN_InvalidOptionalDVNThreshold() ``` ### LZ_ULN_InvalidConfirmations ```solidity error LZ_ULN_InvalidConfirmations() ``` ### LZ_ULN_UnsupportedEid ```solidity error LZ_ULN_UnsupportedEid(uint32 eid) ``` ### DefaultUlnConfigsSet ```solidity event DefaultUlnConfigsSet(struct SetDefaultUlnConfigParam[] params) ``` ### UlnConfigSet ```solidity event UlnConfigSet(address oapp, uint32 eid, struct UlnConfig config) ``` ### setDefaultUlnConfigs ```solidity 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 function getUlnConfig(address _oapp, uint32 _remoteEid) public view returns (struct UlnConfig rtnConfig) ``` ### getAppUlnConfig ```solidity 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 function _setUlnConfig(uint32 _remoteEid, address _oapp, struct UlnConfig _param) internal ``` ### \_isSupportedEid ```solidity 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 function _assertSupportedEid(uint32 _remoteEid) internal view ``` ## ExecuteParam ```solidity struct ExecuteParam { uint32 vid; address target; bytes callData; uint256 expiration; bytes signatures; } ``` ## ISendLibBase ### fees ```solidity function fees(address _worker) external view returns (uint256) ``` ## IReceiveUln ### verify ```solidity function verify(bytes _packetHeader, bytes32 _payloadHash, uint64 _confirmations) external ``` ## ReceiveLibParam ```solidity 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 error DVNAdapter_InsufficientBalance(uint256 actual, uint256 requested) ``` ### DVNAdapter_NotImplemented ```solidity error DVNAdapter_NotImplemented() ``` ### DVNAdapter_MissingRecieveLib ```solidity error DVNAdapter_MissingRecieveLib(address sendLib, uint32 dstEid) ``` ### ReceiveLibsSet ```solidity event ReceiveLibsSet(struct ReceiveLibParam[] params) ``` ### MAX_CONFIRMATIONS ```solidity 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 mapping(address => mapping(uint32 => bytes32)) receiveLibs ``` _receive lib to call verify() on at destination_ ### constructor ```solidity constructor(address _roleAdmin, address[] _admins, uint16 _defaultMultiplierBps) internal ``` ### setReceiveLibs ```solidity 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 function _getAndAssertReceiveLib(address _sendLib, uint32 _dstEid) internal view returns (bytes32 lib) ``` ### \_encode ```solidity function _encode(bytes32 _receiveLib, bytes _packetHeader, bytes32 _payloadHash) internal pure returns (bytes) ``` ### \_encodeEmpty ```solidity function _encodeEmpty() internal pure returns (bytes) ``` ### \_decodeAndVerify ```solidity function _decodeAndVerify(uint32 _srcEid, bytes _payload) internal ``` ### \_withdrawFeeFromSendLib ```solidity function _withdrawFeeFromSendLib(address _sendLib, address _to) internal ``` ### \_assertBalanceAndWithdrawFee ```solidity function _assertBalanceAndWithdrawFee(address _sendLib, uint256 _messageFee) internal ``` ### receive ```solidity receive() external payable ``` _to receive refund_ ## DVNAdapterMessageCodec ### DVNAdapter_InvalidMessageSize ```solidity error DVNAdapter_InvalidMessageSize() ``` ### PACKET_HEADER_SIZE ```solidity uint256 PACKET_HEADER_SIZE ``` ### MESSAGE_SIZE ```solidity uint256 MESSAGE_SIZE ``` ### encode ```solidity function encode(bytes32 _receiveLib, bytes _packetHeader, bytes32 _payloadHash) internal pure returns (bytes payload) ``` ### decode ```solidity function decode(bytes _message) internal pure returns (address receiveLib, bytes packetHeader, bytes32 payloadHash) ``` ### srcEid ```solidity function srcEid(bytes _message) internal pure returns (uint32) ``` ## IDVN ### DstConfigParam ```solidity struct DstConfigParam { uint32 dstEid; uint64 gas; uint16 multiplierBps; uint128 floorMarginUSD; } ``` ### DstConfig ```solidity struct DstConfig { uint64 gas; uint16 multiplierBps; uint128 floorMarginUSD; } ``` ### SetDstConfig ```solidity event SetDstConfig(struct IDVN.DstConfigParam[] params) ``` ### dstConfig ```solidity function dstConfig(uint32 _dstEid) external view returns (uint64, uint16, uint128) ``` ## IDVNFeeLib ### FeeParams ```solidity struct FeeParams { address priceFeed; uint32 dstEid; uint64 confirmations; address sender; uint64 quorum; uint16 defaultMultiplierBps; } ``` ### DVN_UnsupportedOptionType ```solidity error DVN_UnsupportedOptionType(uint8 optionType) ``` ### DVN_EidNotSupported ```solidity error DVN_EidNotSupported(uint32 eid) ``` ### getFeeOnSend ```solidity function getFeeOnSend(struct IDVNFeeLib.FeeParams _params, struct IDVN.DstConfig _dstConfig, bytes _options) external payable returns (uint256 fee) ``` ### getFee ```solidity function getFee(struct IDVNFeeLib.FeeParams _params, struct IDVN.DstConfig _dstConfig, bytes _options) external view returns (uint256 fee) ``` ## ILayerZeroDVN ### AssignJobParam ```solidity struct AssignJobParam { uint32 dstEid; bytes packetHeader; bytes32 payloadHash; uint64 confirmations; address sender; } ``` ### assignJob ```solidity function assignJob(struct ILayerZeroDVN.AssignJobParam _param, bytes _options) external payable returns (uint256 fee) ``` ### getFee ```solidity 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 function verify(bytes _packetHeader, bytes32 _payloadHash, uint64 _confirmations) external ``` for each dvn to verify the payload _this function signature 0x0223536e_ ### commitVerification ```solidity function commitVerification(bytes _packetHeader, bytes32 _payloadHash) external ``` verify the payload at endpoint, will check if all DVNs verified ## DVNOptions ### WORKER_ID ```solidity uint8 WORKER_ID ``` ### OPTION_TYPE_PRECRIME ```solidity uint8 OPTION_TYPE_PRECRIME ``` ### DVN_InvalidDVNIdx ```solidity error DVN_InvalidDVNIdx() ``` ### DVN_InvalidDVNOptions ```solidity error DVN_InvalidDVNOptions(uint256 cursor) ``` ### groupDVNOptionsByIdx ```solidity 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 function _insertDVNOptions(bytes[] _dvnOptions, uint8[] _dvnIndices, uint8 _dvnIdx, bytes _newOptions) internal pure ``` ### getNumDVNs ```solidity 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 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 uint16 TYPE_1 ``` ### TYPE_2 ```solidity uint16 TYPE_2 ``` ### TYPE_3 ```solidity uint16 TYPE_3 ``` ### LZ_ULN_InvalidWorkerOptions ```solidity error LZ_ULN_InvalidWorkerOptions(uint256 cursor) ``` ### LZ_ULN_InvalidWorkerId ```solidity error LZ_ULN_InvalidWorkerId(uint8 workerId) ``` ### LZ_ULN_InvalidLegacyType1Option ```solidity error LZ_ULN_InvalidLegacyType1Option() ``` ### LZ_ULN_InvalidLegacyType2Option ```solidity error LZ_ULN_InvalidLegacyType2Option() ``` ### LZ_ULN_UnsupportedOptionType ```solidity error LZ_ULN_UnsupportedOptionType(uint16 optionType) ``` ### decode ```solidity 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 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 mapping(uint32 => uint256) addressSizes ``` ### AddressSizeSet ```solidity event AddressSizeSet(uint16 eid, uint256 size) ``` ### AddressSizeConfig_InvalidAddressSize ```solidity error AddressSizeConfig_InvalidAddressSize() ``` ### AddressSizeConfig_AddressSizeAlreadySet ```solidity error AddressSizeConfig_AddressSizeAlreadySet() ``` ### setAddressSize ```solidity function setAddressSize(uint16 _eid, uint256 _size) external ``` ## ILayerZeroReceiveLibrary ### setConfig ```solidity function setConfig(uint16 _chainId, address _userApplication, uint256 _configType, bytes _config) external ``` ### getConfig ```solidity function getConfig(uint16 _chainId, address _userApplication, uint256 _configType) external view returns (bytes) ``` ## SetDefaultExecutorParam ```solidity 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 mapping(address => mapping(uint32 => address)) executors ``` ### defaultExecutors ```solidity mapping(uint32 => address) defaultExecutors ``` ### PacketDelivered ```solidity event PacketDelivered(struct Origin origin, address receiver) ``` ### InvalidDst ```solidity event InvalidDst(uint16 srcChainId, bytes32 srcAddress, address dstAddress, uint64 nonce, bytes32 payloadHash) ``` ### DefaultExecutorsSet ```solidity event DefaultExecutorsSet(struct SetDefaultExecutorParam[] params) ``` ### ExecutorSet ```solidity event ExecutorSet(address oapp, uint32 eid, address executor) ``` ### LZ_MessageLib_InvalidExecutor ```solidity error LZ_MessageLib_InvalidExecutor() ``` ### LZ_MessageLib_OnlyExecutor ```solidity error LZ_MessageLib_OnlyExecutor() ``` ### constructor ```solidity constructor(address _endpoint, uint32 _localEid) internal ``` ### setDefaultExecutors ```solidity function setDefaultExecutors(struct SetDefaultExecutorParam[] _params) external ``` ### getExecutor ```solidity function getExecutor(address _oapp, uint32 _remoteEid) public view returns (address) ``` ### \_setExecutor ```solidity function _setExecutor(uint32 _remoteEid, address _oapp, address _executor) internal ``` ### \_execute ```solidity 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 uint256 CONFIG_TYPE_EXECUTOR ``` ### CONFIG_TYPE_ULN ```solidity uint256 CONFIG_TYPE_ULN ``` ### LZ_ULN_InvalidConfigType ```solidity error LZ_ULN_InvalidConfigType(uint256 configType) ``` ### constructor ```solidity constructor(address _endpoint, uint32 _localEid) public ``` ### setConfig ```solidity function setConfig(uint16 _eid, address _oapp, uint256 _configType, bytes _config) external ``` ### commitVerification ```solidity 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 function verify(bytes _packetHeader, bytes32 _payloadHash, uint64 _confirmations) external ``` ### getConfig ```solidity function getConfig(uint16 _eid, address _oapp, uint256 _configType) external view returns (bytes) ``` ### version ```solidity function version() external pure returns (uint64 major, uint8 minor, uint8 endpointVersion) ``` ## VerificationState ```solidity enum VerificationState { Verifying, Verifiable, Verified } ``` ## IReceiveUln301 ### assertHeader ```solidity function assertHeader(bytes _packetHeader, uint32 _localEid) external pure ``` ### addressSizes ```solidity function addressSizes(uint32 _dstEid) external view returns (uint256) ``` ### endpoint ```solidity function endpoint() external view returns (address) ``` ### verifiable ```solidity function verifiable(struct UlnConfig _config, bytes32 _headerHash, bytes32 _payloadHash) external view returns (bool) ``` ### getUlnConfig ```solidity function getUlnConfig(address _oapp, uint32 _remoteEid) external view returns (struct UlnConfig rtnConfig) ``` ## ReceiveUln301View ### endpoint ```solidity contract ILayerZeroEndpoint endpoint ``` ### receiveUln301 ```solidity contract IReceiveUln301 receiveUln301 ``` ### localEid ```solidity uint32 localEid ``` ### initialize ```solidity function initialize(address _endpoint, uint32 _localEid, address _receiveUln301) external ``` ### executable ```solidity function executable(bytes _packetHeader, bytes32 _payloadHash) public view returns (enum ExecutionState) ``` ### verifiable ```solidity 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 contract INonceContract nonceContract ``` ### treasuryFeeHandler ```solidity contract ITreasuryFeeHandler treasuryFeeHandler ``` ### lzToken ```solidity address lzToken ``` ### PacketSent ```solidity event PacketSent(bytes encodedPayload, bytes options, uint256 nativeFee, uint256 lzTokenFee) ``` ### NativeFeeWithdrawn ```solidity event NativeFeeWithdrawn(address user, address receiver, uint256 amount) ``` ### LzTokenSet ```solidity event LzTokenSet(address token) ``` ### constructor ```solidity constructor(address _endpoint, uint256 _treasuryGasLimit, uint256 _treasuryNativeFeeCap, address _nonceContract, uint32 _localEid, address _treasuryFeeHandler) internal ``` ### send ```solidity 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 function setLzToken(address _lzToken) external ``` ### setTreasury ```solidity function setTreasury(address _treasury) external ``` ### withdrawFee ```solidity function withdrawFee(address _to, uint256 _amount) external ``` ### estimateFees ```solidity function estimateFees(uint16 _dstEid, address _sender, bytes _message, bool _payInLzToken, bytes _options) external view returns (uint256 nativeFee, uint256 lzTokenFee) ``` ### \_assertPath ```solidity function _assertPath(address _sender, bytes _path, uint256 remoteAddressSize) internal pure ``` _path = remoteAddress + localAddress._ ### \_payLzTokenFee ```solidity function _payLzTokenFee(address _sender, uint256 _lzTokenFee) internal ``` ### \_outbound ```solidity 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 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 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 uint256 CONFIG_TYPE_EXECUTOR ``` ### CONFIG_TYPE_ULN ```solidity uint256 CONFIG_TYPE_ULN ``` ### LZ_ULN_InvalidConfigType ```solidity error LZ_ULN_InvalidConfigType(uint256 configType) ``` ### constructor ```solidity constructor(address _endpoint, uint256 _treasuryGasLimit, uint256 _treasuryGasForFeeCap, address _nonceContract, uint32 _localEid, address _treasuryFeeHandler) public ``` ### setConfig ```solidity function setConfig(uint16 _eid, address _oapp, uint256 _configType, bytes _config) external ``` ### getConfig ```solidity function getConfig(uint16 _eid, address _oapp, uint256 _configType) external view returns (bytes) ``` ### version ```solidity function version() external pure returns (uint64 major, uint8 minor, uint8 endpointVersion) ``` ### isSupportedEid ```solidity function isSupportedEid(uint32 _eid) external view returns (bool) ``` ### \_quoteVerifier ```solidity function _quoteVerifier(address _sender, uint32 _dstEid, struct WorkerOptions[] _options) internal view returns (uint256) ``` ### \_payVerifier ```solidity function _payVerifier(struct Packet _packet, struct WorkerOptions[] _options) internal virtual returns (uint256 otherWorkerFees, bytes encodedPacket) ``` ### \_splitOptions ```solidity function _splitOptions(bytes _options) internal pure returns (bytes, struct WorkerOptions[]) ``` _this function will split the options into executorOptions and validationOptions_ ## TreasuryFeeHandler ### endpoint ```solidity contract ILayerZeroEndpoint endpoint ``` ### LZ_TreasuryFeeHandler_OnlySendLibrary ```solidity error LZ_TreasuryFeeHandler_OnlySendLibrary() ``` ### LZ_TreasuryFeeHandler_OnlyOnSending ```solidity error LZ_TreasuryFeeHandler_OnlyOnSending() ``` ### LZ_TreasuryFeeHandler_InvalidAmount ```solidity error LZ_TreasuryFeeHandler_InvalidAmount(uint256 required, uint256 supplied) ``` ### constructor ```solidity constructor(address _endpoint) public ``` ### payFee ```solidity 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 error LZ_MessageLib_InvalidPath() ``` ### LZ_MessageLib_InvalidSender ```solidity error LZ_MessageLib_InvalidSender() ``` ### LZ_MessageLib_InsufficientMsgValue ```solidity error LZ_MessageLib_InsufficientMsgValue() ``` ### LZ_MessageLib_LzTokenPaymentAddressMustBeSender ```solidity error LZ_MessageLib_LzTokenPaymentAddressMustBeSender() ``` ### setLzToken ```solidity function setLzToken(address _lzToken) external ``` ### setTreasury ```solidity function setTreasury(address _treasury) external ``` ### withdrawFee ```solidity function withdrawFee(address _to, uint256 _amount) external ``` ### version ```solidity function version() external view returns (uint64 major, uint8 minor, uint8 endpointVersion) ``` ## INonceContract ### increment ```solidity function increment(uint16 _chainId, address _ua, bytes _path) external returns (uint64) ``` ## ITreasuryFeeHandler ### payFee ```solidity function payFee(address _lzToken, address _sender, uint256 _required, uint256 _supplied, address _treasury) external ``` ## IUltraLightNode301 ### commitVerification ```solidity function commitVerification(bytes _packet, uint256 _gasLimit) external ``` ## NonceContractMock ### OnlySendLibrary ```solidity error OnlySendLibrary() ``` ### endpoint ```solidity contract ILayerZeroEndpoint endpoint ``` ### outboundNonce ```solidity mapping(uint16 => mapping(bytes => uint64)) outboundNonce ``` ### constructor ```solidity constructor(address _endpoint) public ``` ### increment ```solidity 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 uint32 CONFIG_TYPE_ULN ``` _CONFIG_TYPE_ULN=2 here to align with SendUln302/ReceiveUln302/ReceiveUln301_ ### LZ_ULN_InvalidConfigType ```solidity error LZ_ULN_InvalidConfigType(uint32 configType) ``` ### constructor ```solidity constructor(address _endpoint) public ``` ### supportsInterface ```solidity function supportsInterface(bytes4 _interfaceId) public view returns (bool) ``` ### setConfig ```solidity function setConfig(address _oapp, struct SetConfigParam[] _params) external ``` ### commitVerification ```solidity 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 function verify(bytes _packetHeader, bytes32 _payloadHash, uint64 _confirmations) external ``` _for dvn to verify the payload_ ### getConfig ```solidity function getConfig(uint32 _eid, address _oapp, uint32 _configType) external view returns (bytes) ``` ### isSupportedEid ```solidity function isSupportedEid(uint32 _eid) external view returns (bool) ``` ### version ```solidity function version() external pure returns (uint64 major, uint8 minor, uint8 endpointVersion) ``` ## VerificationState ```solidity enum VerificationState { Verifying, Verifiable, Verified, NotInitializable } ``` ## IReceiveUln302 ### assertHeader ```solidity function assertHeader(bytes _packetHeader, uint32 _localEid) external pure ``` ### verifiable ```solidity function verifiable(struct UlnConfig _config, bytes32 _headerHash, bytes32 _payloadHash) external view returns (bool) ``` ### getUlnConfig ```solidity function getUlnConfig(address _oapp, uint32 _remoteEid) external view returns (struct UlnConfig rtnConfig) ``` ## ReceiveUln302View ### receiveUln302 ```solidity contract IReceiveUln302 receiveUln302 ``` ### localEid ```solidity uint32 localEid ``` ### initialize ```solidity function initialize(address _endpoint, address _receiveUln302) external ``` ### verifiable ```solidity function verifiable(bytes _packetHeader, bytes32 _payloadHash) external view returns (enum VerificationState) ``` _a ULN verifiable requires it to be endpoint verifiable and committable_ ### \_endpointVerifiable ```solidity 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 uint32 CONFIG_TYPE_EXECUTOR ``` ### CONFIG_TYPE_ULN ```solidity uint32 CONFIG_TYPE_ULN ``` ### LZ_ULN_InvalidConfigType ```solidity error LZ_ULN_InvalidConfigType(uint32 configType) ``` ### constructor ```solidity constructor(address _endpoint, uint256 _treasuryGasLimit, uint256 _treasuryGasForFeeCap) public ``` ### setConfig ```solidity function setConfig(address _oapp, struct SetConfigParam[] _params) external ``` ### getConfig ```solidity function getConfig(uint32 _eid, address _oapp, uint32 _configType) external view returns (bytes) ``` ### version ```solidity function version() external pure returns (uint64 major, uint8 minor, uint8 endpointVersion) ``` ### isSupportedEid ```solidity function isSupportedEid(uint32 _eid) external view returns (bool) ``` ### \_quoteVerifier ```solidity function _quoteVerifier(address _sender, uint32 _dstEid, struct WorkerOptions[] _options) internal view returns (uint256) ``` ### \_payVerifier ```solidity function _payVerifier(struct Packet _packet, struct WorkerOptions[] _options) internal returns (uint256 otherWorkerFees, bytes encodedPacket) ``` ### \_splitOptions ```solidity 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 bytes32 MESSAGE_LIB_ROLE ``` ### ALLOWLIST ```solidity bytes32 ALLOWLIST ``` ### DENYLIST ```solidity bytes32 DENYLIST ``` ### ADMIN_ROLE ```solidity bytes32 ADMIN_ROLE ``` ### workerFeeLib ```solidity address workerFeeLib ``` ### allowlistSize ```solidity uint64 allowlistSize ``` ### defaultMultiplierBps ```solidity uint16 defaultMultiplierBps ``` ### priceFeed ```solidity address priceFeed ``` ### supportedOptionTypes ```solidity mapping(uint32 => uint8[]) supportedOptionTypes ``` ### \_\_Worker_init ```solidity 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 function __Worker_init_unchained(address[] _messageLibs, address _priceFeed, uint16 _defaultMultiplierBps, address _roleAdmin, address[] _admins) internal ``` ### onlyAcl ```solidity modifier onlyAcl(address _sender) ``` ### hasAcl ```solidity 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 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 function setPriceFeed(address _priceFeed) external ``` #### Parameters | Name | Type | Description | | ----------- | ------- | ------------------ | | \_priceFeed | address | price feed address | ### setWorkerFeeLib ```solidity function setWorkerFeeLib(address _workerFeeLib) external ``` #### Parameters | Name | Type | Description | | -------------- | ------- | ---------------------- | | \_workerFeeLib | address | worker fee lib address | ### setDefaultMultiplierBps ```solidity function setDefaultMultiplierBps(uint16 _multiplierBps) external ``` #### Parameters | Name | Type | Description | | --------------- | ------ | --------------------------------- | | \_multiplierBps | uint16 | default multiplier for worker fee | ### withdrawFee ```solidity 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 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 function setSupportedOptionTypes(uint32 _eid, uint8[] _optionTypes) external ``` ### getSupportedOptionTypes ```solidity function getSupportedOptionTypes(uint32 _eid) external view returns (uint8[]) ``` ### \_grantRole ```solidity 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 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 function renounceRole(bytes32, address) public pure ``` _overrides AccessControl to disable renouncing of roles_ --- --- title: Transaction Pricing sidebar_label: Fees --- Every transaction using LayerZero has four main cost elements, one for each component that enables cross-chain messaging: 1. an initial source blockchain transaction. 2. the fee paid to the OApp's configured [Security Stack](../../../concepts/modular-security/security-stack-dvns.md). 3. the configured [Executor](../../../concepts/permissionless-execution/executors.md) fee for executing the message on the destination chain. 4. the cost of purchasing the specified amount of destination gas token(s) for the Executor's destination transaction. The source chain's native gas token quote for the messaging fee is calculated using following formula: $$ \text{GAS} \times \text{DESTINATION\_GAS\_PRICE} \times \frac{\text{DESTINATION\_NATIVE\_TOKEN\_PRICE}}{\text{SOURCE\_NATIVE\_TOKEN\_PRICE}} $$ ### Gas Amount Because the source chain has no concept of the destination chain's state, you must specify the amount of gas in `wei` you anticipate will be necessary for executing your `_lzReceive` or `lzCompose` method on the destination smart contract. LayerZero provides robust [Message Execution Options](../configuration/options.md), which allow users to provide detailed instructions regarding the gas limit and `msg.value` the Executor uses for message delivery on the destination chain per function call: ```solidity // addExecutorLzReceiveOption(GAS_LIMIT, MSG_VALUE) // addExecutorLzComposeOption(INDEX, GAS_LIMIT, MSG_VALUE) bytes memory options = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(60000, 0) .addExecutorComposeOption(0, 30000, 0) ``` :::caution The amount of gas units (`wei`) that your contract's `_lzReceive` or `lzCompose` methods consume can be dynamic depending on the destination chain. Different blockchains have different opcode costs and gas mechanisms that can fluctuate (e.g., sequencer fees, proof fees, etc). To mitigate the risk of transactions stalling due to `OUT-OF-GAS` issues on the destination, it is advisable to test gas costs for your `_lzReceive` or `lzCompose` contract logic, and incorporate a gas buffer by allocating additional gas upfront depending on the chain. ::: ### Quote Mechanism The LayerZero Endpoint provides an on-chain quote mechanism, to determine the cost of sending a message to the destination chain: ```solidity // LayerZero/V2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol struct MessagingParams { uint32 dstEid; bytes32 receiver; bytes message; bytes options; bool payInLzToken; } struct MessagingFee { uint256 nativeFee; uint256 lzTokenFee; } // LayerZero Endpoint's quote mechanism function quote(MessagingParams calldata _params, address _sender) external view returns (MessagingFee memory); ``` Both the OApp and OFT have implemented a [quote mechanism](../configuration/gas-fees.md) using this Endpoint method. If a user wants to send a message from Chain A to Chain B, the gas quote returned on Chain A is: - `the execution cost on Chain A` + `fees for the Security Stack and Executor` + `a quote for the gas to be executed on Chain B` For example, if a user wants `200000` gas units on Chain B, then a quote for that gas token is obtained by multiplying the gas by the gas price on the destination chain. It also takes into account dollar prices of the source and destination native tokens. The source chain's native token quote is calculated using following formula: $$ \text{GAS} \times \text{DESTINATION\_GAS\_PRICE} \times \frac{\text{DESTINATION\_NATIVE\_TOKEN\_PRICE}}{\text{SOURCE\_NATIVE\_TOKEN\_PRICE}} $$

:::info For example, assume Chain A is **Astar** and Chain B is **Astar zkEVM**, **Astar** uses token `ASTR` as a native token, and **Astar zkEVM** uses `ETH` as its native token. Other assumptions are: 1. ASTR = ~$0.15 2. ETH = ~$3500 3. Destination gas price = 4 Gwei The quote returned will be: ``` 200000 * (4000000000 / 10**18) * $3500 / $0.15 = 18.7 ASTR quote ``` ::: ### Profiling These sample gas profiles were based on 15 OFT transfers across 3 EVM networks (`Sepolia`, `Fuji`, `Mumbai`): | Metric | Value | | ------------------------------------------ | ------------------- | | Deployment gas | `2,903,879` | | Send gas on source chain (average) | `226,541` | | Send gas on source chain (range) | `221,261 - 241,095` | | Receive gas on destination chain (average) | `62,000` | | Receive gas on destination chain (range) | `56,970 - 78,882` | Please note that the values provided above were measured for standard OFT transactions with minimal custom logic applied. You can explore the source code at the following links: - [Sepolia Etherscan](https://sepolia.etherscan.io/address/0x67457db11bcf2d79be032d8cda7c696eb8142d98) - [Snowtrace Testnet](https://testnet.snowtrace.io/address/0x205d4d615c73965467eeb9a113cef702095d9d05) - [Mumbai PolygonScan](https://mumbai.polygonscan.com/address/0x82c404bdffcc7da1a3d5ee8aee5f0932a0f68a26) Feel free to input these contract addresses into [LayerZero Scan](https://layerzeroscan.com/) to discover all the transfers used for profiling, including both the source and destination transactions. ### Handling Errors Transactions in LayerZero may occasionally encounter delays in transit from the source chain to the destination chain. Common causes for these delays include: - Failure to initiate a valid transaction from the source chain into the LayerZero protocol. - Insufficient gas payments made by the user. - Transaction reverts on the destination chain, either due to in-contract or configuration issues. [LayerZero Scan](../tooling/layerzeroscan.md) offers a comprehensive tool for users to track their transactions. It provides detailed insights into where transactions may encounter delays, serving as a starting point for debugging. Users can find detailed guidelines and support for debugging and recovering stalled transactions in [Debugging Messages](../troubleshooting/debugging-messages.md). --- --- title: LayerZero EVM Chain Compatibility sidebar_label: EVM Chain Compatibility description: 'Understanding how LayerZero operates across chains, with a focus on fee delivery, gas estimation, and on-chain state reliability.' --- 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`, `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 cross-chain message delivery. Each chain may have a unique fee model, which impacts how gas estimates should be calculated for [`lzReceive`](../../../concepts/glossary.md#lzreceive) and [`lzCompose`](../../../concepts/glossary.md#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 cross-chain 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. - **On-chain 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. :::tip 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. - **On-chain 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). - **On-chain 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. :::info 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.md#oft-alt) if the Subnet requires a custom ERC20 token for fees. - **On-chain 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. - **On-chain 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 [OFT Alt](../oft/oft-patterns-extensions.md#oft-alt). - **On-chain 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, Citrea, and Corn **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. - **On-chain 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/) | [Corn Documentation](https://docs.usecorn.com/) ### **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. - **On-chain 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. - **On-chain 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. - **On-chain 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. - **On-chain 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 cross-chain messaging and token integrations to work reliably across all supported networks. --- --- title: LayerZero V2 Integration Checklist sidebar_label: Integration Checklist --- The checklist below is designed to help prepare a project that integrates LayerZero V2 for an external audit or Mainnet deployment. ### 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. ### Token Bridging Guidelines For new tokens, inherit from `OFT` or `ONFT`. For existing tokens, use `OFTAdapter` or `ONFTAdapter`. For non-EVM tokens, select the correct VM from the navbar and see the equivalent sections. :::warning **There can only be one 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. ::: ### Avoid Hardcoding LayerZero Endpoint IDs Use admin-restricted setters to configure endpoint IDs instead of hardcoding them. ### Call `setPeer` on every OApp Deployment 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 cross-chain communication. ```solidity // 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 // 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; } ``` ### Set Security and Executor Configurations 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 EndpointV2.setSendLibrary(aOApp, bEid, newLib) EndpointV2.setReceiveLibrary(aOApp, bEid, newLib, gracePeriod) EndpointV2.setReceiveLibraryTimeout(aOApp, bEid, lib, gracePeriod) EndpointV2.setConfig(aOApp, sendLibrary, sendConfig) EndpointV2.setConfig(aOApp, receiveLibrary, receiveConfig) EndpointV2.setDelegate(delegate) ``` Follow the [Protocol Configuration](../configuration/dvn-executor-config.md) documentation to configure DVNs for each chain pathway. :::caution **If no configuration is set, the OApp will fallback to the default settings set by LayerZero Labs.** ```solidity /// @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(); } } ``` ::: ### Implement Enforced Options Implement and set `enforcedOptions` to ensure users pay a predetermined amount of gas for delivery on the destination transaction. This setup guarantees that messages sent from a source have sufficient gas to be executed on the destination chain. Test the gas required for execution on the destination chain to prevent failures due to insufficient gas. ```solidity // 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 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); ``` ### Avoid Redundant `require` Statements Do not add `require` statements that repeat checks in parent contracts, such as those in `OAppReceiver.lzReceive`. ```solidity /** * @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. // highlight-next-line if (address(endpoint) != msg.sender) revert OnlyEndpoint(msg.sender); // Ensure that the sender matches the expected peer for the source endpoint. // highlight-next-line 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); } ``` ### Add `require` Statements in `lzCompose` Unlike child contracts with the `OAppReceiver.lzReceive` method, the `ILayerZeroComposer.lzCompose` does not have built-in checks. Add these checks for the source `oApp` and `endpoint` before any custom state change logic: ```solidity // 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 // 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. ### Implement Instant Finality Guarantee (IFG) Design your OApp with IFG to ensure that transactions accepted at the source will be accepted at the destination, minimizing state damage in case of message failure. ### Perform One Action Per Message Minimize the impact of potential message failure by performing only one action per message. ### Message Encoding Use type-safe bytes codec for message encoding. Use custom codecs only if necessary and if your app requires deep optimization. For example, see the `OFTMsgCodec.sol`: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; library OFTMsgCodec { // Offset constants for encoding and decoding OFT messages uint8 private constant SEND_TO_OFFSET = 32; uint8 private constant SEND_AMOUNT_SD_OFFSET = 40; /** * @dev Encodes an OFT LayerZero message. * @param _sendTo The recipient address. * @param _amountShared The amount in shared decimals. * @param _composeMsg The composed message. * @return _msg The encoded message. * @return hasCompose A boolean indicating whether the message has a composed payload. */ function encode( bytes32 _sendTo, uint64 _amountShared, bytes memory _composeMsg ) internal view returns (bytes memory _msg, bool hasCompose) { hasCompose = _composeMsg.length > 0; // @dev Remote chains will want to know the composed function caller ie. msg.sender on the src. _msg = hasCompose ? abi.encodePacked(_sendTo, _amountShared, addressToBytes32(msg.sender), _composeMsg) : abi.encodePacked(_sendTo, _amountShared); } /** * @dev Checks if the OFT message is composed. * @param _msg The OFT message. * @return A boolean indicating whether the message is composed. */ function isComposed(bytes calldata _msg) internal pure returns (bool) { return _msg.length > SEND_AMOUNT_SD_OFFSET; } /** * @dev Retrieves the recipient address from the OFT message. * @param _msg The OFT message. * @return The recipient address. */ function sendTo(bytes calldata _msg) internal pure returns (bytes32) { return bytes32(_msg[:SEND_TO_OFFSET]); } /** * @dev Retrieves the amount in shared decimals from the OFT message. * @param _msg The OFT message. * @return The amount in shared decimals. */ function amountSD(bytes calldata _msg) internal pure returns (uint64) { return uint64(bytes8(_msg[SEND_TO_OFFSET:SEND_AMOUNT_SD_OFFSET])); } /** * @dev Retrieves the composed message from the OFT message. * @param _msg The OFT message. * @return The composed message. */ function composeMsg(bytes calldata _msg) internal pure returns (bytes memory) { return _msg[SEND_AMOUNT_SD_OFFSET:]; } /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } /** * @dev Converts bytes32 to an address. * @param _b The bytes32 value to convert. * @return The address representation of bytes32. */ function bytes32ToAddress(bytes32 _b) internal pure returns (address) { return address(uint160(uint256(_b))); } } ``` --- --- title: LayerZero Experimental Simple Config Generator sidebar_label: Experimental Simple Config description: Learn how to use the experimental LayerZero simple config generator --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; We have developed a new simple config generator which makes use of the `@layerzerolabs/metadata-tools` package. It is currently in the experimental stage. It allows for a more simplified Layerzero config file. Here's how to use it: 1. Install metadata-tools: `pnpm add -D @layerzerolabs/metadata-tools` 2. Create a new [LZ config](/docs/concepts/glossary.md#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 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 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, }; } ``` - 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. 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 npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` The wire command would process 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. ``` npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` --- --- title: EVM DVN and Executor Configuration sidebar_label: DVN & Executor Configuration toc_min_heading_level: 2 toc_max_heading_level: 5 --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Before setting your DVN and Executor Configuration, you should review the [Security Stack Core Concepts](../../../concepts/modular-security/security-stack-dvns.md). 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.md#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. :::tip Use the LayerZero CLI The LayerZero CLI has abstracted these calls for every supported chain. See the [**CLI Setup Guide**](../create-lz-oapp/start.md) to easily deploy, configure, and send messages using LayerZero. ::: ### 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 // 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 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 executorAddress)']; 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 // 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 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(); ``` ### Setting Custom Send Config (DVN & Executor) In this example, we configure both the ULN (DVN settings) and Executor settings on the sending chain. ```solidity // 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 cross-chain 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 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 executorAddress: '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 executorAddress)'; 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). :::warning 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. ::: :::info 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**](../technical-reference/integration-checklist.md#enforce-msgvalue-in-_lzreceive-and-lzcompose) for more details. :::

```solidity // 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 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) | :::tip 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) | :::warning 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)** | :::warning 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 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)** | :::warning 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. --- --- title: Message Execution Options sidebar_label: Execution Gas Options description: Learn how to generate message execution options and how to use them in your LayerZero contracts. --- What are message `_options`? Because the source chain has no concept of the destination chain's state, you must specify the amount of gas you anticipate will be necessary for executing your `lzReceive` or `lzCompose` method on the destination smart contract. LayerZero provides robust **Message Execution Options**, which allow you to specify arbitrary logic as part of the message transaction, such as the gas amount and `msg.value` the [Executor](../../../concepts/permissionless-execution/executors.md) pays for message delivery, the order of message execution, or dropping an amount of gas to a destination address. The most common options you will use when building are [`lzReceiveOption`](#lzreceive-option), [`lzComposeOption`](#lzcompose-option), and [`lzNativeDropOption`](#lznativedrop-option). :::info It's important to remember that gas values may vary depending on the destination chain. For example, all new Ethereum transactions cost `21000` wei, but other chains may have lower or higher opcode costs, or entirely different gas mechanisms. ::: ## Options Builders A Solidity library and off-chain SDK have been provided to build specific Message Options for your application. - `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). ## Generating Options You can generate options depending on your OApp's development environment: - **Remix**: for quick testing in Remix, you can deploy locally to the Remix VM a contract using the `OptionsBuilder.sol` library. See the example provided below.



- **Foundry**: `_options` can be generated directly in your Foundry unit tests using the `OptionsBuilder.sol` library. See the [OmniCounter Test](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/oapp/test/OmniCounter.t.sol#L80) file as an example for how to properly invoke options. - **Hardhat**: you can also locally declare options in Hardhat via the `options.ts` file. All tools use the same method for packing the `_options` bytes array to simplify your experience when switching between environments. ### Import Options All `_options` tools must be imported into your environment to be used. #### Options Library Import the `OptionsBuilder` from `@layerzerolabs/oapp-evm` into either your Foundry test or smart contract to be deployed locally. ```solidity import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; ``` #### Options SDK Start by importing `Options` from `@layerzerolabs/lz-v2-utilities`. ```javascript import {Options} from '@layerzerolabs/lz-v2-utilities'; ``` ### Initialize Options The `newOptions` method is used to initialize a new bytes array. ```javascript const _options = Options.newOptions(); ``` It's a starting point to which you can add specific option configurations. The command below in Solidity allows you to conveniently extend the bytes type with LayerZero's `OptionsBuilder` library methods, simplifying the creation and manipulation of message execution options. ```solidity using OptionsBuilder for bytes; ``` ### Add Options Types When generating `_options`, you will want to allocate specific gas amounts for handling different message types used in your smart contract. For instance, `addExecutorLzReceiveOption` is a method that can be used to specify how much gas limit and `msg.value` the Executor uses when calling `lzReceive` on the receiving chain. ```javascript const GAS_LIMIT = 1000000; // 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); ``` You can continue appending new `Options` methods to add more Executor message handling; all packed into a single call. See below for all [Option Types](#option-types). :::caution For each chain pathway, your OApp's configured Executor has a **native cap**: an upper bound for how much gas can be sent to the destination chain. In general, the sum of your message execution options must be **LESS THAN** the native cap. To check the native gas cap, you can query the Executor contract's `DstConfig` using the [**Executor address**](../../../deployments/deployed-contracts.md) and the [**`IExecutor.sol` interface**](https://github.com/LayerZero-Labs/LayerZero-v2/blob/bf4318b5e88e46400931bb4c1f6aa0343c035a79/messagelib/contracts/interfaces/IExecutor.sol): ```solidity function dstConfig(uint32 _dstEid) external view returns (uint64, uint16, uint128, uint128); struct DstConfig { uint64 baseGas; // for verifying / fixed calldata overhead uint16 multiplierBps; uint128 floorMarginUSD; // uses priceFeed PRICE_RATIO_DENOMINATOR uint128 nativeCap; // the MAX amount of native gas an OApp can use in execution options } ``` ::: :::tip The [**create-lz-oapp**](../create-lz-oapp/project-config.md) npx package includes a script by default for checking all the Executor `DstConfigs` for your project: ```bash npx hardhat lz:oapp:config:get:executor ``` ::: ### Pass Options in Send Call After generating `_options`, you will want to test them in a `send` call. #### Options SDK Using the Options SDK, this can be passed directly into a Hardhat task or unit test depending on your use case. ```javascript // Other parameters for the send function const _dstEid = 'someEndpointId'; // Destination endpoint ID const message = 'Your message here'; // The message you want to send // Call the send function on the smart contract // Convert your options array toHex() const tx = await yourOAppContract.send(destEndpointId, message, _options.toHex()); await tx.wait(); ``` In this Typescript snippet, the `send` function is being called on the `YourOAppContract` contract instance, passing in the destination endpoint ID, the message, and the `_options` that were constructed: ```solidity contract YourOAppContract { // ... other functions and declarations function send(uint32 _dstEid, string memory message, bytes memory _options) public payable { // Logic to handle the sending of the message with the provided options // This might involve interacting with other contracts or internal logic bytes memory _payload = abi.encode(message); _lzSend( _dstEid, // Destination chain's endpoint ID. _payload, // Encoded message payload being sent. _options, // Message execution options (e.g., gas to use on destination). MessagingFee(msg.value, 0), // Fee struct containing native gas and ZRO token. payable(msg.sender) // The refund address in case the send call reverts. ); } // ... other functions and declarations } ``` In this Solidity snippet, the `send` function takes three parameters: `_dstEid`, `message`, and `_options`. The function's logic would then use those parameters to to send a message cross-chain. #### Options Library Using the `OptionsBuilder.sol` library, these `_options` can be directly referenced in your Foundry tests for quick local testing. ```solidity import { MyOApp } from "../contracts/oapp/examples/MyOApp.sol"; import { TestHelper } from "../contracts/tests/TestHelper.sol"; import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; contract MyOAppTest is TestHelper { using OptionsBuilder for bytes; // ... other test setup functions function test_increment() public { bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(50000, 0); (uint256 nativeFee, ) = aCounter.quote(bEid, MsgCodec.VANILLA_TYPE, options); aCounter.increment{ value: nativeFee }(bEid, MsgCodec.VANILLA_TYPE, options); // ... other test logic } } ``` The above example is taken from the [OmniCounter Foundry Test](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/oapp/test/OmniCounter.t.sol#L76) in the LayerZero V2 repo. :::info See [**`TestHelper.sol`**](https://github.com/LayerZero-Labs/LayerZero-v2/blob/432db51666878f147ceaf1c46d230f344430c78e/oapp/test/TestHelper.sol#L4) for full Foundry testing support. ::: ## Option Types There are multiple option types to take advantage of, each controlling specific handling of LayerZero messages. ### `lzReceive` Option The `lzReceive` option specifies the gas values the Executor uses when calling `lzReceive` on the destination chain. ```javascript Options.newOptions().addExecutorLzReceiveOption(50000, 0); ``` It defines the amount of `_gas` and `msg.value` to be used in the `lzReceive` call by the Executor on the destination chain: `OPTION_TYPE_LZRECEIVE` contains `(uint128 _gas, uint128 _value)` `_gas`: The amount of gas you'd provide for the `lzReceive` call in source chain native tokens. `50000` should be enough for most transactions, but this value should be profiled based on your function's specific opcode cost on each chain. `_value`: The `msg.value` for the call. This value is often included to fund any operations that need native gas on the destination chain, including sending another nested message. ### `lzCompose` Option This option allows you to allocate some gas and value to your **Composed Message** on the destination chain. [`lzCompose`](../oapp/message-design-patterns.md) is used when you want to call external contracts from your `lzReceive` function. ```javascript Options.newOptions().addExecutorLzComposeOption(0, 30000, 0); ``` `OPTION_TYPE_LZCOMPOSE` contains `(uint16 _index, uint128 _gas, uint128 _value)` `_index`: The index of the `lzCompose()` function call. When multiples of this option are added, they are summed PER index by the Executor on the remote chain. This can be useful for defining multiple composed message steps that happen sequentially. `_gas`: The gas amount for the lzCompose call varies based on the destination's compose logic and the destination chain's characteristics (e.g., opcode pricing). It's important to perform tailored testing to determine the optimal gas requirement for your specific transaction needs. `_value`: The `msg.value` for the call. ### `lzNativeDrop` Option This option contains how much native gas you want to drop to the `_receiver`, this is often done to allow users or a contract to have some gas on a new chain. ```javascript Options.newOptions().addExecutorNativeDropOption(100000, receiverAddressInBytes32); ``` `OPTION_TYPE_LZNATIVEDROP` contains `(uint128 _amount, bytes32 _receiver)` `_amount`: The amount of gas in wei to drop for the receiver. `_receiver`: The `bytes32` representation of the receiver address. ### `OrderedExecution` Option By adding this option, the Executor will utilize [**Ordered Message Delivery**](../oapp/message-design-patterns.md#unordered-delivery). This overrides the default behavior of [**Unordered Message Delivery**](../oapp/message-design-patterns.md#unordered-delivery). ```javascript Options.newOptions().addExecutorOrderedExecutionOption(bytes('')); ``` For example, if nonce `2` transaction fails, all subsequent transactions with this option will not be executed until the previous message has been resolved with. `bytes`: The argument should always be initialized as an empty bytes array (`""`). :::caution These message `_options` must be combined with in-app contract changes, listed under [**Ordered Message Delivery**](../oapp/message-design-patterns.md#enabling-ordered-delivery). ::: ### Duplicate Option Types Multiple options of the same type can be passed and appended into the same options array. The logic on how multiple options of the same type are summed differs per option type: ```solidity bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(50000, 0).addExecutorLzComposeOption(0, 30000, 0).addExecutorLzComposeOption(1, 30000, 0); ``` - **`lzReceive`**: Both the `_gas` and `_value` parameters are summed. - **`lzCompose`**: Both the `_gas` and `_value` parameters are **summed by index**. - **`lzNativeDrop`**: The `_amount` parameter is summed by unique `_receiver` address. Make sure that appending multiple options is the intended behavior of your unique OApp. ## Determining Gas Costs Gas profiling and optimization is outside the scope of LayerZero's documentation, however, the following resources may be useful for determining what `_options` should be used for your `_lzReceive` and `lzCompose` calls. ### Tenderly For supported chains, the [Tenderly Gas Profiler](https://dashboard.tenderly.co/explorer) can be extremely useful for determining how much to reduce your execution options by: ![Tenderly Gas](/img/tenderly.png) In the above image, you can see that the \_lzReceive call for this OFT token transfer with composed call used `45,358` wei for gas. - Provided `lzReceive` Option: `50000` wei - Actual `lzReceive` Cost: `45,358` wei In general, this opcode cost may fluctuate depending on the destination chain and how your contract logic executes, so you should take care in defining `_options` based on the message types for your application. --- --- title: Estimating Gas Fees sidebar_label: Estimating Source Gas Fees description: See how to return the native gas fee for an omnichain message. --- Both [`OApp`](../oapp/overview.md) and [`OFT`](../oft/quickstart.md) come packaged with methods you can implement or call directly in order to receive a quote for how much native gas your message will cost to send to the destination chain. :::info Both the `OApp` and `OFT` implementations for estimating fees require some knowledge of how `_options` work. We recommend reviewing the [**OApp**](../oapp/overview.md) or [**OFT Quickstart**](../oft/quickstart.md) and [**Message Options**](../configuration/options.md) guides first to better understand `_options` usage. ::: [](../oapp/message-design-patterns.md#unordered-delivery) ## OApp To estimate how much gas a message will cost to be sent and received, you will need to implement a `quote` function to return an estimate from the Endpoint contract to use as a recommended `msg.value`. ```solidity function quote( uint32 _dstEid, // destination endpoint id bytes memory payload, // message payload being sent bytes memory _options, // your message execution options bool memory _payInLzToken // boolean for which token to return fee in ) public view returns (uint256 nativeFee, uint256 zroFee) { return _quote(_dstEid, payload, _options, _payInLzToken); } ``` The `_quote` can be returned in either the native gas token or in `LzToken`, supporting both payment methods. In general, this quote will be accurate as the same function is used by the Endpoint when pricing an `_lzSend` call: ```solidity // How the _quote function works. // This function is already defined in your OApp contract. /// @dev the generic quote interface to interact with the LayerZero EndpointV2.quote() function _quote( uint32 _dstEid, bytes memory _message, bytes memory _options, bool _payInLzToken ) internal view virtual returns (MessagingFee memory fee) { return endpoint.quote( MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _payInLzToken), address(this) ); } ``` :::tip Make sure that the arguments passed into the `_quote` function identically match the parameters used in the `lzSend` function. If parameters mismatch, you may run into errors as your `msg.value` will not match the actual gas quote. :::

:::note Remember that to send a message, a `msg.sender` will be paying the source chain, the selected DVNs to deliver the message, and the destination chain to execute the transaction. ::: ## OFT To estimate how much gas an OFT transfer will cost, call the `quoteSend` function to return an estimate from the Endpoint contract. ```solidity // @dev Requests a nativeFee/lzTokenFee quote for sending the corresponding msg cross-chain through the layerZero Endpoint function quoteSend( SendParam calldata _sendParam, // send parameters struct bytes calldata _extraOptions, // extra message options bool _payInLzToken, // bool for payment in native gas or LzToken bytes calldata _composeMsg, // data for composed message bytes calldata _oftCmd // data for custom OFT behaviours ) public view virtual returns ( MessagingFee memory msgFee, // fee struct for native or LzToken OFTLimit memory oftLimit, OFTReceipt memory oftReceipt, OFTFeeDetail[] memory oftFeeDetails // @dev unused in the default implementation, future proofs complex fees inside of an oft send ) { (oftLimit, oftReceipt) = quoteOFT(_sendParam); (bytes memory message, bytes memory options) = _buildMsgAndOptions( _sendParam, _extraOptions, _composeMsg, oftReceipt.amountCreditLD ); msgFee = _quote(_sendParam.dstEid, message, options, _payInLzToken); } ``` --- --- title: Deploy Deterministic Addresses --- 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). --- --- title: TestHelper --- # TestHelper (Foundry) # Overview The TestHelper contract is designed to facilitate the testing of Omnichain Applications (OApps) developed using LayerZero V2, specifically within the Foundry test framework. This contract provides a suite of functions to simulate cross-chain transactions and validate the behavior of OApps locally in your Foundry unit tests. The full code to this contract can be found in [Monorepo](https://github.com/LayerZero-Labs/monorepo/blob/main/packages/layerzero-v2/evm/oapp/test/TestHelper.sol). :::info For developers new to Foundry or those looking to deepen their understanding of its capabilities in Solidity testing, the following resources can be helpful: 1. **Getting Started with Foundry**: To begin your journey with Foundry, the [**Foundry Book**](https://book.getfoundry.sh) offers a detailed guide on installation, setup, and basic usage. It's an excellent starting point for understanding the fundamentals of Foundry and its role in smart contract development. 2. **Solidity Testing with Foundry**: For a deeper dive into testing Solidity contracts using Foundry, the [**Foundry GitHub**](https://github.com/foundry-rs/foundry) provides comprehensive documentation, examples, and community contributions. This resource is invaluable for learning best practices and advanced techniques in contract testing. ::: ### Installation To install the TestHelper package in Foundry, run the following command: ``` forge install LayerZero-Labs/devtools ``` And then add the following remapping to your `remappings.txt` file: ``` @layerzerolabs/test-devtools-evm-foundry/=lib/devtools/packages/test-devtools-evm-foundry ``` #### NPM If you have a hybrid Foundry and NPM setup you can use following command to install the tool: ```bash npm install @layerzerolabs/test-devtools-evm-foundry ``` ### Sample Implementation ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { Packet } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.sol"; import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; import { MessagingFee } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import { MessagingReceipt } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppSender.sol"; // The unique path location of your OApp import { MyOApp } from "../src/MyOApp.sol"; import { TestHelperOz5 } from "@layerzerolabs/test-devtools-evm-foundry/contracts/TestHelperOz5.sol"; import "forge-std/console.sol"; /// @notice Unit test for MyOApp using the TestHelper. /// @dev Inherits from TestHelper to utilize its setup and utility functions. contract MyOAppTest is TestHelperOz5 { using OptionsBuilder for bytes; // Declaration of mock endpoint IDs. uint16 aEid = 1; uint16 bEid = 2; // Declaration of mock contracts. MyOApp aMyOApp; // OApp A MyOApp bMyOApp; // OApp B /// @notice Calls setUp from TestHelper and initializes contract instances for testing. function setUp() public virtual override { super.setUp(); // Setup function to initialize 2 Mock Endpoints with Mock MessageLib. setUpEndpoints(2, LibraryType.UltraLightNode); // Initializes 2 MyOApps; one on chain A, one on chain B. address[] memory sender = setupOApps(type(MyOApp).creationCode, 1, 2); aMyOApp = MyOApp(payable(sender[0])); bMyOApp = MyOApp(payable(sender[1])); } /// @notice Tests the send and multi-compose functionality of MyOApp. /// @dev Simulates message passing from A -> B and checks for data integrity. function test_send() public { // Setup variable for data values before calling send(). string memory dataBefore = aMyOApp.data(); // Generates 1 lzReceive execution option via the OptionsBuilder library. // STEP 0: Estimating message gas fees via the quote function. bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(150000, 0); MessagingFee memory fee = aMyOApp.quote(bEid, "test message", options, false); // STEP 1: Sending a message via the _lzSend() method. MessagingReceipt memory receipt = aMyOApp.send{ value: fee.nativeFee }(bEid, "test message", options); // Asserting that the receiving OApps have NOT had data manipulated. assertEq(bMyOApp.data(), dataBefore, "shouldn't be changed until lzReceive packet is verified"); // STEP 2 & 3: Deliver packet to bMyOApp manually. verifyPackets(bEid, addressToBytes32(address(bMyOApp))); // Asserting that the data variable has updated in the receiving OApp. assertEq(bMyOApp.data(), "test message", "lzReceive data assertion failure"); } } ``` # Key Functions The TestHelper contract, integral to testing Omnichain Applications (OApps) with Foundry, is equipped with a variety of functions. While some of these functions are geared towards internal mechanics of the contract and may not be directly utilized by developers, others are crucial for effectively testing cross-chain functionalities in OApps. Below, we delve into those key functions that are particularly important for external use in testing scenarios. ## Initializers ### `setUp()` The `setUp()` function initializes the test environment. This function can be overridden in derived contracts to set up specific test conditions. ```solidity function setUp() public virtual {} ``` It is called at the beginning of each test to prepare the test environment. ### `setUpEndpoints` The `setUpEndpoints` function is designed to initialize a specified number of mock endpoints. This function allows for the creation of multiple endpoints, each potentially representing different blockchains or networks, and configures them with a chosen library type (e.g., Ultra Light Node, Simple Message Lib). ```solidity /** * @dev setup the endpoints * @param _endpointNum num of endpoints */ function setUpEndpoints(uint8 _endpointNum, LibraryType _libraryType) public { // Full code implementation here: https://github.com/LayerZero-Labs/devtools/blob/30e92c638876c5aa435ae0a5c856f15cfed2a706/packages/test-devtools-evm-foundry/contracts/TestHelperOz5.sol } ``` - `_endpointNum`: The number of endpoints to set up. - `_libraryType`: The type of library to use (Ultra Light Node or Simple Message Lib). - **UltraLightNode**: A messaging library featuring Mock Decentralized Verifier Networks (DVNs) and Executors for complex cross-chain message verification and execution. - **SimpleMessageLib**: A streamlined library for basic cross-chain message passing, lacking additional functionalities like Mock DVNs and Executors found in more complex libraries. ### `setupOApps` `setupOApps` automates the deployment and wiring of OApp instances. It enables developers to simulate multiple instances of their OApps on different mock chains, providing a comprehensive testing landscape. ```solidity /** * @dev setup UAs, only if the UA has `endpoint` address as the unique parameter */ function setupOApps( bytes memory _oappCreationCode, uint8 _startEid, uint8 _oappNum ) public returns (address[] memory oapps) { // Full code implementation here: https://github.com/LayerZero-Labs/monorepo/blob/main/packages/layerzero-v2/evm/oapp/test/TestHelper.sol } ``` - `bytes _oappCreationCode`: Represents the bytecode (creation code) of the Omnichain Application (OApp) to be deployed. It is essentially the compiled code of the OApp contract. - `uint8 _startEid`: Specifies the starting mock Endpoint ID (Eid) for the OApps being set up. In the context of LayerZero and cross-chain applications, an Endpoint ID uniquely identifies a specific blockchain or network endpoint. - `uint8 _oappNum`: Indicates the number of OApp instances to deploy. This function returns an array of mock OApp addresses that are deployed and wired together (via `setPeer`) in the test environment. ### Sample Implementation The example implementation below demonstrates how to utilize initializers in TestHelper like `setUpEndpoints` and `setupOApps` to create a testing environment. ```solidity function setUp() public virtual override { super.setUp(); // Setup function to initialize 2 Mock Endpoints with Mock MessageLib. setUpEndpoints(2, LibraryType.UltraLightNode); // Initializes 2 Sample OApps; one on chain A, one on chain B. address[] memory sender = setupOApps(type(SampleOApp).creationCode, 1, 2); aSampleOApp = SampleOApp(payable(sender[0])); bSampleOApp = SampleOApp(payable(sender[1])); } ``` ## Simulate Transactions ### `verifyPackets` `verifyPackets` simulates the receipt and processing of packets on the destination chain. ```solidity /** * @dev dst UA receive/execute packets * @dev will NOT work calling this directly with composer IF the composed payload is different from the lzReceive msg payload */ function verifyPackets(uint32 _dstEid, bytes32 _dstAddress, uint256 _packetAmount, address _composer) public { // Full code implementation here: https://github.com/LayerZero-Labs/monorepo/blob/main/packages/layerzero-v2/evm/oapp/test/TestHelper.sol } ``` - `_dstEid`: The destination endpoint Id - `_dstAddress`: The destination address (as `bytes32`) - `_packetAmount`: Specifies the number of packets to verify. Used to limit the number of packets that will be processed during the simulation. This can be useful for testing scenarios where you need to control the volume of packets being verified in a single function call. - `_composer`: The address of the composer. Used when the verification process involves composed messaging. - **Overloads**: 1. `verifyPackets(uint32 _dstEid, bytes32 _dstAddress)` 2. `verifyPackets(uint32 _dstEid, address _dstAddress)` ### Sample Implementation ```solidity /// @notice Tests the send and receive functionality. /// @dev Simulates message passing from A -> B and checks for data integrity. function test_send_and_compose() public { // Setup variables for data values before calling send(). string memory dataBefore = bSampleOApp.data(); // STEP 0: Estimating message gas fees via the quote function. bytes memory _payload = abi.encode("test message"); // Generates 1 lzReceive execution option via the OptionsBuilder library. bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(150000, 0); MessagingFee memory fee = aSampleApp.quote(bEid, "test message", options, false); // STEP 1: Sending a message via the _lzSend() method. MessagingReceipt memory receipt = aSampleOApp.send{ value: fee.nativeFee }(bEid, "test message", options); // Asserting that the receiving OApps have NOT had data manipulated. assertEq(bSampleOApp.data(), dataBefore, "shouldn't be changed until lzReceive packet is verified"); // STEP 2 & 3: Deliver packet to bSampleOApp. verifyPackets(bEid, addressToBytes32(address(bSampleOApp))); // Asserting that the data variable has updated in both receiving OApps. assertEq(bSampleOApp.data(), "test message", "lzReceive data assertion failure"); } ``` ## Helper Functions In addition to its main testing functions, the **TestHelper.sol** contract includes helper functions that enhance its capability to handle various scenarios in the testing of Omnichain Applications (OApps). These functions are critical for ensuring a thorough and versatile testing environment, particularly when dealing with the complexities of cross-chain communication. ### `addressToBytes32` `addressToBytes32` converts an Ethereum address to a `bytes32` format. This is useful in scenarios where addresses need to be handled in a fixed-size byte format, which is common in many blockchain protocols and LayerZero operations. ```solidity function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } ``` - `_addr`: The Ethereum address to convert. - **Returns**: `bytes32` representation of the address. ### `getNextInflightPacket` `getNextInflightPacket` Retrieves the next packet in line for delivery to a specified destination. This is crucial for testing the order and integrity of packet delivery in cross-chain communications. Use it to inspect and verify the sequence and content of packets destined for a particular chain or address. ```solidity function getNextInflightPacket(uint16 _dstEid, bytes32 _dstAddress) public view returns (bytes memory packetBytes) { DoubleEndedQueue.Bytes32Deque storage queue = packetsQueue[_dstEid][_dstAddress]; if (queue.length() > 0) { bytes32 guid = queue.back(); packetBytes = packets[guid]; } } ``` - `_dstEid`: The destination Endpoint ID. - `_dstAddress`: The destination address (as `bytes32`). - **Returns**: The next packet (as bytes) scheduled for delivery. ### `hasPendingPackets` `hasPendingPackets` checks if there are any pending packets for a given destination. Useful for verifying if packets are scheduled for delivery. ```solidity function hasPendingPackets(uint16 _dstEid, bytes32 _dstAddress) public view returns (bool flag) { DoubleEndedQueue.Bytes32Deque storage queue = packetsQueue[_dstEid][_dstAddress]; return queue.length() > 0; } ``` - `_dstEid`: Destination endpoint ID - `_dstAddress`: Destination Address (as `bytes32`). - **Returns**: Boolean indicating the presence of pending packets. ### `assertGuid` `assertGuid` validates that a given packet has the correct Global Unique Identifier (GUID) which is defined in the messageLib. ```solidity function assertGuid(bytes calldata packetBytes, bytes32 guid) external pure { bytes32 packetGuid = packetBytes.guid(); require(packetGuid == guid, "guid not match"); } ``` - `_packetBytes`: The packet content. - `guid`: The Global Unique Identifier of the specific packet being checked. - **Usage**: 1. Testing Packet Integrity: This function is instrumental in testing scenarios to ensure that packets being sent and received in a cross-chain setup are correctly identified and match their intended GUIDs. 2. Debugging and Validation: It's a useful tool for debugging and validating that the packet creation, modification, or routing processes are functioning correctly, as any discrepancy in GUIDs would be a clear indicator of an issue. --- --- title: LayerZero Scan --- [LayerZero Scan](https://layerzeroscan.com/) is a comprehensive search, API, and analytics platform designed to streamline the experience for developers and users dealing with omnichain transactions. ![Scan-Light](/img/learn/scan.png#gh-light-mode-only) ![Scan-Dark](/img/learn/scan.png#gh-dark-mode-only) ## Overview Scan offers an enhanced developer experience for working with omnichain transactions in the LayerZero protocol by providing: - **Unified Message Explorer**: Track LayerZero transactions across multiple chains within a single interface. - **Protocol Analytics**: Monitor marketwide trends and the state of the ecosystem through detailed analytics. - **Scan Client**: Interface your frontend applications with omnichain transaction logs seamlessly. Developers can monitor transactions on both the [Mainnet Explorer](https://layerzeroscan.com/) and [Testnet Explorer](https://testnet.layerzeroscan.com/). ## Transaction Statuses ![Scan-Light](/img/learn/tx-statuses.png#gh-light-mode-only) ![Scan-Dark](/img/learn/tx-statuses.png#gh-dark-mode-only) - **Delivered**: The message has been successfully sent and received by the destination chain. - **Inflight**: The message is currently being transmitted between chains and has not yet reached its destination. - **Payload Stored**: The message arrived at the destination, but reverted or ran out of gas during execution and needs to be retried. - **Failed**: The transaction encountered an error and did not complete. - **Blocked**: A previous message nonce has a stored payload, halting the current transaction. - **Confirming**: The system is validating the finality of a transaction amidst potential high gas replacements or block reorgs. ## Protocol Analytics Users can also monitor protocol and chain analytics, making it easy to observe market wide trends and understand the current state of the ecosystem. ![Scan-Analytics-Light](/img/learn/scan-analytics.png#gh-light-mode-only) ![Scan-Analytics-Dark](/img/learn/scan-analytics.png#gh-dark-mode-only) --- --- title: Debugging Messages --- 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.md) and the message nonce has been committed to the [Endpoint](../../../concepts/protocol/layerzero-endpoint.md)'s messaging channel. **`Delivered`**: the message has been successfully executed by the [Executor](../../../concepts/permissionless-execution/executors.md). Because verification and execution are separate, LayerZero can provide specific error handling for each message state. ## 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. - **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. ```solidity 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); } ``` ### Skipping Nonce Occasionally, an [OApp delegate](../oapp/overview.md#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. :::caution 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. ::: :::warning 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: ```solidity /// @dev the caller must provide _nonce to prevent skipping the unintended nonce /// @dev it could happen in some race conditions, e.g. intent to skip nonce 3, but nonce 3 was consumed before this transaction was included in the block /// @dev NOTE: only allows skipping the next of the effective inbound nonce (from the inboundNonce() function). if the Oapp wants to skips a delivered message, it should call the clear() function and ignore the payload instead /// @dev after skipping, the lazyInboundNonce is set to the provided nonce, which makes the inboundNonce also the provided nonce function skip ( address _oapp, //the Oapp address uint32 _srcEid, //source chain endpoint id bytes32 _sender, //the byte32 format of sender address uint64 _nonce // the message nonce you wish to skip to ) external { _assertAuthorized(_oapp); if (_nonce != inboundNonce(_oapp, _srcEid, _sender) + 1) revert Errors.InvalidNonce(_nonce); //Skipping ahead of this nonce. lazyInboundNonce[_oapp][_srcEid][_sender] = _nonce; emit InboundNonceSkipped(_srcEid, _sender, _oapp, _nonce); } ``` **Example for calling `skip`** 1. **Set up Dependencies and Define the ABI** ```js // 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 // 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 // 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 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. :::warning It is crucial to use this function with caution because once a payload is cleared, it cannot be recovered. ::: ```solidity /// @dev Oapp uses this interface to clear a message. /// @dev this is a PULL mode versus the PUSH mode of lzReceive /// @dev the cleared message can be ignored by the app (effectively burnt) /// @dev authenticated by oapp /// @param _origin the origin of the message /// @param _guid the guid of the message /// @param _message the message function clear( address _oapp, //the Oapp address Origin calldata _origin, // The `Origin` struct of the message. bytes32 _guid, // The unique identifier of the message. This can be fetched from the arguments of `LzReceive`. bytes calldata _message // The bytes message you sent on the source chain. This can be fetched from the arguments of `LzReceive`. ) external { _assertAuthorized(_oapp); bytes memory payload = abi.encodePacked(_guid, _message); _clearPayload(_oapp, _origin.srcEid, _origin.sender, _origin.nonce, payload); emit PacketDelivered(_origin, _oapp); } ``` **Example for calling `clear`** 1. **Set up Dependencies and Define the ABI** ```js // 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 // 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 // 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 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. :::tip `nilify` and `burn` are called similarly to `clear` and `skip`, refer to those examples if needed. ::: #### **`nilify`** ```solidity /// @dev Marks a packet as verified, but disallows execution until it is re-verified. /// @dev Reverts if the provided _payloadHash does not match the currently verified payload hash. /// @dev A non-verified nonce can be nilified by passing EMPTY_PAYLOAD_HASH for _payloadHash. /// @dev Assumes the computational intractability of finding a payload that hashes to bytes32.max. /// @dev Authenticated by the caller function nilify( address _oapp, // The Oapp address uint32 _srcEid, // The source Endpoint Id bytes32 _sender, // The bytes32 representation of the source chain's Oapp address uint64 _nonce, // The nonce you want to nilify bytes32 _payloadHash // The targeted payload hash ) external ``` 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`** ```solidity /// @dev Marks a nonce as unexecutable and un-verifiable. The nonce can never be re-verified or executed. /// @dev Reverts if the provided _payloadHash does not match the currently verified payload hash. /// @dev Only packets with nonces less than or equal to the lazy inbound nonce can be burned. /// @dev Reverts if the nonce has already been executed. /// @dev Authenticated by the caller function burn( address _oapp, // The Oapp address uint32 _srcEid, // The source Endpoint Id bytes32 _sender, // The bytes32 representation of the source chain's Oapp address uint64 _nonce, // The nonce you want to nilify bytes32 _payloadHash // The targeted payload hash ) external ``` 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. --- --- title: Error Codes --- # Error Codes & Handling :::note 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. ::: :::tip You can decode LayerZero error codes that are not in human readable format using [**create-lz-oapp**](./error-messages.md). ::: ### 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 :::info 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 :::info 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 :::info 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 :::info 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 :::info 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 :::info 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. --- --- sidebar_label: Overview --- # LayerZero V2 Solana Programs 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 #### Solana Protocol Configurations

:::info 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](http://localhost:3000/v2/developers/evm/tooling/layerzeroscan): a comprehensive block 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://layerzero.network/community). --- --- title: Getting Started with LayerZero V2 on Solana sidebar_label: Getting Started with Solana --- Any data, whether it's a fungible token transfer, an NFT, or some other smart contract input can be encoded on-chain 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**. :::tip 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.md), provide message handling, interfaces for protocol configurations, and other utilities for interoperability: - [`Omnichain Fungible Token (OFT)`](/v2/developers/solana/oft/program): an extension of `OApp` built for handling and supporting omnichain fungible token transfers. - [`Omnichain Application (OApp)`](/v2/developers/solana/oapp/overview): 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 tutorial (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)), however we will cover some major considerations. :::info 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. ![Solana Token Program](/img/solana/spl-token-light.svg#gh-light-mode-only) ![Solana Token Program](/img/solana/spl-token-dark.svg#gh-dark-mode-only) 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). ![Solana Token Program](/img/solana/solana-token-program-light.svg#gh-light-mode-only) ![Solana Token Program](/img/solana/solana-token-program-dark.svg#gh-dark-mode-only) An account on Solana either is an executable program (i.e. a smart contract) or holds data (e.g. how many tokens you have). :::info 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 on-chain. 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 on-chain 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). ![OFT Program](/img/solana/pdas-light.svg#gh-light-mode-only) ![OFT Program](/img/solana/pda-dark.svg#gh-dark-mode-only) 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. ![OFT Program](/img/solana/oft-program-model-light.svg#gh-light-mode-only) ![OFT Program](/img/solana/oft-program-model-dark.svg#gh-dark-mode-only) 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 cross-chain tokens on Solana will need to deploy their own instance of the **OFT Program** that will have their own **OFT Store ** Account. ![Solana Token Program](/img/solana/oft-program-to-store-light.svg#gh-light-mode-only) ![Solana Token Program](/img/solana/oft-program-to-store-dark.svg#gh-dark-mode-only) 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. :::info 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 On-chain Programs](https://solana.com/docs/programs/overview) to learn more about Solana. :::caution 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. ::: --- --- title: LayerZero V2 Solana Technical Overview sidebar_label: Technical Overview description: Technical documentation for LayerZero V2 on Solana, covering send, verification, and receive workflows. toc_min_heading_level: 2 toc_max_heading_level: 5 --- LayerZero V2 on Solana mirrors the design of the EVM version in that it coordinates cross-chain 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 cross-chain 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 cross-chain 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.md) (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 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 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 on-chain. ```rust // 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 // 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 // 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 // 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.md#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 // 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 // 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 on-chain. ```rust // 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 on-chain in the Endpoint's messaging channel, thereby maintaining the integrity and security of the cross-chain 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 // 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 // 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 // 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`. --- --- title: Transaction Pricing sidebar_label: Transaction Pricing --- Every transaction using LayerZero has four main cost elements, one for each component that enables cross-chain messaging: 1. an initial source blockchain transaction. 2. the fee paid to the OApp's configured [Security Stack](../../../concepts/modular-security/security-stack-dvns.md). 3. the configured [Executor](../../../concepts/permissionless-execution/executors.md) fee for executing the message on the destination chain. 4. the cost of purchasing the specified amount of destination gas token(s) for the Executor's destination transaction. The source chain's native gas token quote for the messaging fee is calculated using following formula: $$ \text{GAS} \times \text{DESTINATION\_GAS\_PRICE} \times \frac{\text{DESTINATION\_NATIVE\_TOKEN\_PRICE}}{\text{SOURCE\_NATIVE\_TOKEN\_PRICE}} $$ ### Gas Amount Because the source chain has no concept of the destination chain's state, you must specify the amount of gas in `wei` you anticipate will be necessary for executing your `_lzReceive` or `lzCompose` method on the destination smart contract. LayerZero provides robust [Message Execution Options](../gas-settings/options.md), which allow users to provide detailed instructions regarding the gas limit and `msg.value` the Executor uses for message delivery on the destination chain per function call: ```solidity // addExecutorLzReceiveOption(GAS_LIMIT, MSG_VALUE) // addExecutorLzComposeOption(INDEX, GAS_LIMIT, MSG_VALUE) bytes memory options = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(60000, 0) .addExecutorComposeOption(0, 30000, 0) ``` :::caution The amount of gas units (`wei`) that your contract's `_lzReceive` or `lzCompose` methods consume can be dynamic depending on the destination chain. Different blockchains have different opcode costs and gas mechanisms that can fluctuate (e.g., sequencer fees, proof fees, etc). To mitigate the risk of transactions stalling due to `OUT-OF-GAS` issues on the destination, it is advisable to test gas costs for your `_lzReceive` or `lzCompose` contract logic, and incorporate a gas buffer by allocating additional gas upfront depending on the chain. ::: ### Quote Mechanism The LayerZero Endpoint provides an on-chain quote mechanism, to determine the cost of sending a message to the destination chain: Both the OApp and OFT have implemented a [quote mechanism](../oft/account.md#estimating-gas-fees) using this Endpoint method. If a user wants to send a message from Chain A to Chain B, the gas quote returned on Chain A is: - `the execution cost on Chain A` + `fees for the Security Stack and Executor` + `a quote for the gas to be executed on Chain B` For example, if a user wants `200000` gas units on Chain B, then a quote for that gas token is obtained by multiplying the gas by the gas price on the destination chain. It also takes into account dollar prices of the source and destination native tokens. The source chain's native token quote is calculated using following formula: $$ \text{GAS} \times \text{DESTINATION\_GAS\_PRICE} \times \frac{\text{DESTINATION\_NATIVE\_TOKEN\_PRICE}}{\text{SOURCE\_NATIVE\_TOKEN\_PRICE}} $$

:::info For example, assume Chain A is **Astar** and Chain B is **Astar zkEVM**, **Astar** uses token `ASTR` as a native token, and **Astar zkEVM** uses `ETH` as its native token. Other assumptions are: 1. ASTR = ~$0.15 2. ETH = ~$3500 3. Destination gas price = 4 Gwei The quote returned will be: ``` 200000 * (4000000000 / 10**18) * $3500 / $0.15 = 18.7 ASTR quote ``` ::: ### Profiling These sample gas profiles were based on 15 OFT transfers across 3 EVM networks (`Sepolia`, `Fuji`, `Mumbai`): | Metric | Value | | ------------------------------------------ | ------------------- | | Deployment gas | `2,903,879` | | Send gas on source chain (average) | `226,541` | | Send gas on source chain (range) | `221,261 - 241,095` | | Receive gas on destination chain (average) | `62,000` | | Receive gas on destination chain (range) | `56,970 - 78,882` | Please note that the values provided above were measured for standard OFT transactions with minimal custom logic applied. You can explore the source code at the following links: - [Sepolia Etherscan](https://sepolia.etherscan.io/address/0x67457db11bcf2d79be032d8cda7c696eb8142d98) - [Snowtrace Testnet](https://testnet.snowtrace.io/address/0x205d4d615c73965467eeb9a113cef702095d9d05) - [Mumbai PolygonScan](https://mumbai.polygonscan.com/address/0x82c404bdffcc7da1a3d5ee8aee5f0932a0f68a26) Feel free to input these contract addresses into [LayerZero Scan](https://layerzeroscan.com/) to discover all the transfers used for profiling, including both the source and destination transactions. ### Handling Errors Transactions in LayerZero may occasionally encounter delays in transit from the source chain to the destination chain. Common causes for these delays include: - Failure to initiate a valid transaction from the source chain into the LayerZero protocol. - Insufficient gas payments made by the user. - Transaction reverts on the destination chain, either due to in-contract or configuration issues. [LayerZero Scan](../tooling/layerzeroscan.md) offers a comprehensive tool for users to track their transactions. It provides detailed insights into where transactions may encounter delays, serving as a starting point for debugging. --- --- title: Solana Guidance --- ## 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 solana program deploy --program-id target/deploy/oft-keypair.json target/verifiable/oft.so -u devnet --with-compute-unit-price ``` You can run 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. ## Deciding 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.md#shared-decimals) and [local decimals](../../../concepts/glossary.md#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/account#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 ) | --- --- title: LayerZero V2 Solana OApp Reference sidebar_label: Solana OApp Reference --- 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. ![OApp Example](/img/learn/ABLight.svg#gh-light-mode-only) ![OApp Example](/img/learn/ABDark.svg#gh-dark-mode-only) 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 blockchain use case. :::info While LayerZero provides an OApp Standard to inherit when working in the Ethereum Virtual Machine (EVM), Solana programs do not use contract inheritance. Instead, LayerZero provides an **Endpoint Cross Program Invocation (CPI) Helper** which you can reuse within your programs to call the LayerZero Endpoint. :::

In the Ethereum Virtual Machine, the object-oriented nature of Solidity allows for a generic OApp Standard which developers can inherit and override to utilize common cross-chain messaging functions and utilities. In contrast, Solana’s [Cross Program Invocation (CPI)](https://solana.com/docs/core/cpi) model requires that all interacting accounts be provided when a program is called. Since an OApp could be used for a cross-chain fungible / non-fungible token transfers, issuance of a name service, or any solution that moves information between blockchains, a generic OApp Program could potentially interact with an unpredictable range of programs and accounts. This makes a single "**OApp Program**" unfeasible due to it being impossible to anticipate all unique developer use cases at program initialization. This **OApp Reference** instead will provide an overview for the core instructions necessary for a Solana Program to interact with the LayerZero Endpoint and Message Library programs.

```rust use anchor_lang::{ prelude::*, solana_program::{keccak::hash, system_program::ID as SYSTEM_ID}, }; use endpoint::{ self, cpi::accounts::{Clear, ClearCompose, Quote, RegisterOApp, Send, SendCompose, SetDelegate}, instructions::{ ClearComposeParams, ClearParams, QuoteParams, RegisterOAppParams, SendComposeParams, SendParams, SetDelegateParams, }, ConstructCPIContext, MessagingFee, MessagingReceipt, COMPOSED_MESSAGE_HASH_SEED, ENDPOINT_SEED, NONCE_SEED, OAPP_SEED, PAYLOAD_HASH_SEED, }; pub const EVENT_SEED: &[u8] = b"__event_authority"; pub fn register_oapp( endpoint_program: Pubkey, accounts: &[AccountInfo], seeds: &[&[u8]], params: RegisterOAppParams, ) -> Result<()> { let cpi_ctx = RegisterOApp::construct_context(endpoint_program, accounts)?; endpoint::cpi::register_oapp(cpi_ctx.with_signer(&[&seeds]), params) } pub fn set_delegate( endpoint_program: Pubkey, accounts: &[AccountInfo], seeds: &[&[u8]], params: SetDelegateParams, ) -> Result<()> { let cpi_ctx = SetDelegate::construct_context(endpoint_program, accounts)?; endpoint::cpi::set_delegate(cpi_ctx.with_signer(&[&seeds]), params) } pub fn send( endpoint_program: Pubkey, accounts: &[AccountInfo], seeds: &[&[u8]], params: SendParams, ) -> Result { // construct cpi context let cpi_ctx = Send::construct_context(endpoint_program, accounts)?; let rtn = endpoint::cpi::send(cpi_ctx.with_signer(&[&seeds]), params)?; Ok(rtn.get()) } pub fn quote( endpoint_program: Pubkey, accounts: &[AccountInfo], params: QuoteParams, ) -> Result { let cpi_ctx = Quote::construct_context(endpoint_program, accounts)?; let result = endpoint::cpi::quote(cpi_ctx, params)?; Ok(result.get()) } pub fn clear( endpoint_program: Pubkey, accounts: &[AccountInfo], seeds: &[&[u8]], params: ClearParams, ) -> Result<[u8; 32]> { let cpi_ctx = Clear::construct_context(endpoint_program, accounts)?; let result = endpoint::cpi::clear(cpi_ctx.with_signer(&[&seeds]), params)?; Ok(result.get()) } pub fn send_compose( endpoint_program: Pubkey, accounts: &[AccountInfo], seeds: &[&[u8]], params: SendComposeParams, ) -> Result<()> { let cpi_ctx = SendCompose::construct_context(endpoint_program, accounts)?; endpoint::cpi::send_compose(cpi_ctx.with_signer(&[&seeds]), params) } pub fn clear_compose( endpoint_program: Pubkey, accounts: &[AccountInfo], seeds: &[&[u8]], params: ClearComposeParams, ) -> Result<()> { let cpi_ctx = ClearCompose::construct_context(endpoint_program, accounts)?; endpoint::cpi::clear_compose(cpi_ctx.with_signer(&[&seeds]), params) } pub fn get_accounts_for_clear( endpoint_program: Pubkey, receiver: &Pubkey, src_eid: u32, sender: &[u8; 32], nonce: u64, ) -> Vec { let (nonce_account, _) = Pubkey::find_program_address( &[NONCE_SEED, &receiver.to_bytes(), &src_eid.to_be_bytes(), sender], &endpoint_program, ); let (payload_hash_account, _) = Pubkey::find_program_address( &[ PAYLOAD_HASH_SEED, &receiver.to_bytes(), &src_eid.to_be_bytes(), sender, &nonce.to_be_bytes(), ], &endpoint_program, ); let (oapp_registry_account, _) = Pubkey::find_program_address(&[OAPP_SEED, &receiver.to_bytes()], &endpoint_program); let (event_authority_account, _) = Pubkey::find_program_address(&[EVENT_SEED], &endpoint_program); let (endpoint_settings_account, _) = Pubkey::find_program_address(&[ENDPOINT_SEED], &endpoint_program); vec![ LzAccount { pubkey: endpoint_program, is_signer: false, is_writable: false }, LzAccount { pubkey: *receiver, is_signer: false, is_writable: false }, LzAccount { pubkey: oapp_registry_account, is_signer: false, is_writable: false }, LzAccount { pubkey: nonce_account, is_signer: false, is_writable: true }, LzAccount { pubkey: payload_hash_account, is_signer: false, is_writable: true }, LzAccount { pubkey: endpoint_settings_account, is_signer: false, is_writable: true }, LzAccount { pubkey: event_authority_account, is_signer: false, is_writable: false }, LzAccount { pubkey: endpoint_program, is_signer: false, is_writable: false }, ] } pub fn get_accounts_for_send_compose( endpoint_program: Pubkey, from: &Pubkey, to: &Pubkey, guid: &[u8; 32], index: u16, composed_message: &[u8], ) -> Vec { let (composed_message_account, _) = Pubkey::find_program_address( &[ COMPOSED_MESSAGE_HASH_SEED, &from.to_bytes(), &to.to_bytes(), &guid[..], &index.to_be_bytes(), &hash(composed_message).to_bytes(), ], &endpoint_program, ); let (event_authority_account, _) = Pubkey::find_program_address(&[EVENT_SEED], &endpoint_program); vec![ LzAccount { pubkey: endpoint_program, is_signer: false, is_writable: false }, LzAccount { pubkey: *from, is_signer: false, is_writable: false }, LzAccount { pubkey: Pubkey::default(), is_signer: true, is_writable: true }, LzAccount { pubkey: composed_message_account, is_signer: false, is_writable: true }, LzAccount { pubkey: SYSTEM_ID, is_signer: false, is_writable: false }, LzAccount { pubkey: event_authority_account, is_signer: false, is_writable: false }, LzAccount { pubkey: endpoint_program, is_signer: false, is_writable: false }, ] } pub fn get_accounts_for_clear_compose( endpoint_program: Pubkey, from: &Pubkey, to: &Pubkey, guid: &[u8; 32], index: u16, composed_message: &[u8], ) -> Vec { let (composed_message_account, _) = Pubkey::find_program_address( &[ COMPOSED_MESSAGE_HASH_SEED, &from.to_bytes(), &to.to_bytes(), &guid[..], &index.to_be_bytes(), &hash(composed_message).to_bytes(), ], &endpoint_program, ); let (event_authority_account, _) = Pubkey::find_program_address(&[EVENT_SEED], &endpoint_program); vec![ LzAccount { pubkey: endpoint_program, is_signer: false, is_writable: false }, LzAccount { pubkey: *to, is_signer: false, is_writable: false }, LzAccount { pubkey: composed_message_account, is_signer: false, is_writable: true }, LzAccount { pubkey: event_authority_account, is_signer: false, is_writable: false }, LzAccount { pubkey: endpoint_program, is_signer: false, is_writable: false }, ] } /// same to anchor_lang::prelude::AccountMeta #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct LzAccount { pub pubkey: Pubkey, pub is_signer: bool, pub is_writable: bool, } ``` :::tip Review the rest of the code in the [**LayerZero V2 Solana Repo**](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/solana/programs)! ::: --- --- title: LayerZero V2 Solana OFT Program sidebar_label: Solana OFT Program Deploy --- 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.md). 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), 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 cross-chain tokens on Solana should deploy their own instance of the OFT Program to create new OFT Store accounts, so that they own their OFT's Upgrade Authority. :::note 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. ::: ### Requirements To ensure compatibility and smooth operation, please use the following versions: - Rust: `v1.75.0` - Solana CLI: `1.17.31` - Anchor: `0.29.0` - Docker You can find the sample codebase in the [LayerZero Developer Tools Repo](https://github.com/LayerZero-Labs/devtools/tree/main/examples/oft-solana). ### Setup project Setup your project using the LayerZero CLI. ```bash LZ_ENABLE_SOLANA_OFT_EXAMPLE=1 npx create-lz-oapp@latest ``` The CLI will also automatically run `pnpm install` for you. ### Generate Program Keypairs Ensure program keypairs exist: ```bash anchor keys sync -p oft ``` The above command will generate a keypair for the OFT program in your workspace if it doesn't yet exist, and also automatically update `Anchor.toml` to use the generated keypair's public key. The default path for the program's keypair will be `target/deploy/oft-keypair.json`. View the program ID based on the generated keypair: ```bash anchor keys list ``` You will see an output such as: ```bash endpoint: H3SKp4cL5rpzJDntDa2umKE9AHkGiyss1W8BNDndhHWp oft: DLZdefiak8Ur82eWp3Fii59RiCRZn3SjNCmweCdhf1DD ``` Copy the `oft` program ID value for use in the build step later. ### Build ``` anchor build -p oft --verifiable -e OFT_ID= ``` :::info Building in verifiable mode requires Docker. We recommend you to build in verifiable mode, so that you can carry out [program verification](#optional-verify-the-oft-program). If using Docker is not possible, you can [build in regular mode](#building-without-docker). ::: ### Test Run tests to ensure everything is set up correctly: ```bash pnpm test ``` Note that the above would run the foundry and hardhat tasks for the Solidity contract alone. ### Deploy Navigate to the contract directory to prepare for deployment: ```bash solana program deploy --program-id target/deploy/oft-keypair.json target/verifiable/oft.so -u devnet ``` To learn more about deploying Solana programs, refer to [Deploy a Solana Program with the CLI](https://docs.solanalabs.com/cli/examples/deploy-a-program). :::info If you encounter issues during compilation and testing, it might be due to the versions of Solana and Anchor. You can switch to Solana version 1.17.31 and Anchor version 0.29.0, as these are the versions we have tested and verified to be working. ::: ### (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). :::info 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 solana-verify get-executable-hash ./target/verifiable/oft.so ``` Compare with the on-chain 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 solana-verify verify-from-repo -ud --program-id --mount-path examples/oft-solana https://github.com/LayerZero-Labs/devtools --library-name oft -- --config env.OFT_ID=\'\' ``` The above instruction runs against the Solana Devnet as it uses the `-ud` flag. To run it against Solana Mainnet, replace `-ud` with `-um`. 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. #### (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 solana-verify verify-from-repo --remote -um --program-id --mount-path examples/oft-solana https://github.com/LayerZero-Labs/devtools --library-name oft -- --config env.OFT_ID=\'\' ``` :::note 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). ::: ### Creating OFT Store accounts After successful program deployment, you can now use the program to create either a vanilla [Solana OFT](./account.md#creating-a-solana-oft) or [Solana OFT Adapter](./account#creating-a-solana-oft-adapter-adapt-an-existing-spl-token). ### 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. :::caution 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 [programs.localnet] oft = "9obQfBnWMhxwYLtmarPWdjJgTc2mAYGRCoWbdvs9Wdm5" ``` the output of running `anchor keys list`: ```bash 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 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 nett 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 commmand to upgrade the program. ```bash solana program deploy --program-id target/deploy/oft-keypair.json target/deploy/oft.so -u devnet --with-compute-unit-price 300000 ``` :::info 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 ``` :::note For more troubleshooting help, refer to the Solana OFT [README](https://github.com/LayerZero-Labs/devtools/tree/main/examples/oft-solana). ::: ### 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 While it is possible to create a Solana OFT using the Token2022 (Token Extensions) there is limited compatibility with token extensions. It is advised for you to conduct an end-to-end test if you require token extensions for your Solana OFT. #### 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. :::caution 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 cross-chain, 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 :::caution 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. ::: #### 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 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 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. --- --- title: LayerZero V2 Solana OFT Create sidebar_label: Solana OFT Create --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; You should have already completed the steps in the [Solana OFT Program Deploy page](./program.md) before continuing with this page. The Omnichain Fungible Token (OFT) Standard allows **fungible tokens** to be transferred across multiple blockchains without asset wrapping or middlechains. The Solana equivalent of this standard is the **OFT Program**. :::tip You should be familiar with the [**Solana Program Library**](https://spl.solana.com/token) and the [**Token-2022**](https://spl.solana.com/token-2022) program before continuing. ::: ## 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. :::info 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 cross-chain transfers for Solana tokens. ![Solana Token Program](/img/solana/oft-program-to-store-light.svg#gh-light-mode-only) ![Solana Token Program](/img/solana/oft-program-to-store-dark.svg#gh-dark-mode-only) 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). :::caution You will need to [**deploy your own OFT Program**](./program.md) to start bridging your SPL Tokens. ::: You can use the same **OFT Program** to create multiple Solana OFTs. :::info 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 5 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. | :::info 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. ::: ### Creating a Solana OFT #### Mint Authority and Freeze Authority This section applies to regular OFTs and does not apply to OFT Adapters as the authorities are not changed in the case of an OFT Adapter. Before proceeding to create your OFT, you should understand the concepts of [Mint Authority and Freeze Authority](https://solana.com/docs/core/tokens#mint-account) with regards to SPL tokens. If you require the ability to mint additional tokens (outside the context of cross-chain transfers), you will want to include the `--additional-minters` flag when you run the create script in the next section. If you don't require the ability to mint additional tokens, you will want to go with the `--only-oft-store true` flag. #### Running the Create OFT script :::info The examples below show how to integrate these scripts as [**Hardhat tasks**](https://hardhat.org/hardhat-runner/docs/advanced/create-task), which is a common setup for many developers working with both EVM and non-EVM smart contracts in the same project. ::: SOL is the 'native gas token' of Solana. All other tokens (fungible and non-fungible tokens (NFTs)), are called **Solana Program Library (SPL) Tokens**. You should also understand how to add your own [Token Metadata](https://solana.com/developers/guides/token-extensions/metadata-pointer#token-metadata-interface-overview) before creating an OFT. The LayerZero CLI includes a hardhat script for creating OFTs and also OFT Adapters. For creation of regular OFTs, the create script will take care of creating a new SPL token. For creation of OFT Adapters, the create script will take in the existing SPL Token Mint Address as a parameter. As mentioned above, you have two options when creating your OFT, from the perspective of minting rights: **Only OFT Store** and **Additional Minters**. You will need to supply flags depending on your choice. :::info The eid `40168` below corresponds to Solana Devnet. To create on Solana Mainnet, change the eid value to `30168`. ::: ```bash pnpm hardhat lz:oft:solana:create --eid 40168 --program-id --only-oft-store true ``` :::caution If you choose to go with `--only-oft-store true`, you will not be able to add in other signers/minters or update the Mint Authority, and the Freeze Authority will be immediately renounced. The token Mint Authority will be fixed Mint Authority Multisig address while the Freeze Authority will be set to None. ::: ```bash pnpm hardhat lz:oft:solana:create --eid 40168 --program-id --additional-minters ``` :::info If you choose to go with Additional Minters, it will also be possible to later on renounce the Freeze Authority and also update the Mint Authority to another multisig that has only the OFT Store as a signer. ::: ```bash pnpm hardhat lz:oft:solana:create --eid 40168 --program-id --mint --token-program ``` :::info You can use OFT Mint-And-Burn Adapter if you want to use an existing token on Solana. For OFT Mint-And-Burn Adapter, tokens will be burned when sending to other chains and minted when receiving from other chains. Note that before attempting any cross-chain transfers, you must transfer the Mint Authority to the OFT Store address for `lz_receive` to work, as that is not handled in the script. ::: :::caution You cannot use Mint-And-Burn Adapter if your token's Mint Authority has been renounced. ::: Flags for the create script: - `--amount`: The initial supply to mint on Solana (optional) - `--eid`: Solana mainnet or testnet - `--localDecimals`: Token local decimals (default = 9, optional) - `--sharedDecimals`: OFT shared decimals (default = 6, optional) - `--name`: Token Name (default = `MockOFT`) - `--mint`: The Token mint public key (used for MABA only) - `--programId`: The OFT Program ID - `--sellerFeeBasisPoints`: Seller fee basis points (default = 0) - `--symbol`: Token Symbol (default = `MOFT`) - `--tokenMetadataIsMutable`: Token metadata is mutable (default = true) - `--additionalMinters`: Comma-separated list of additional minters (optional) - `--onlyOftStore`: If you plan to have only the OFTStore and no additional minters. This is not reversible and will result in losing the ability to mint new tokens by everything but the OFTStore (default = false, optional) - `--tokenProgram`: The Token Program public key (used for MABA only, default = `TOKEN_PROGRAM_ID.toBase58()`) - `--uri`: URI for token metadata (default = empty string) - `--computeUnitPriceScaleFactor`: The compute unit price scale factor (default = 4, optional) :::caution You should only mint additional tokens if this is your first canonical OFT supply (i.e., the first source of tokens across every chain). While `OFT <-> OFT` connections will handle additional token supplies without an issue, `OFT <-> OFT Adapter` connections will need to ensure that the total supply globally is equal to the lockbox supply in OFT Adapter. ::: After the script successfully completes, you will get a file at `deployments/solana-testnet/OFT.json` that lists the addresses of all the accounts mentioned in [OFT Account Model](#oft-account-model). You can now skip to the section [Updating layerzero.config.ts](#updating-layerzeroconfigts). ### Creating a Solana OFT Adapter (adapt an existing SPL Token) This section applies if you have an existing SPL Token that you want to adapt into an OFT. For OFT Adapter, tokens will be locked when sending to other chains and unlocked when receiving from other chains. #### Running the Create OFT Adapter script :::info The eid `40168` below corresponds to Solana Devnet. To create on Solana Mainnet, change the eid value to `30168`. ::: ```bash pnpm hardhat lz:oft-adapter:solana:create --eid 40168 --program-id --mint --token-program ``` After the script successfully completes, you will get a file at `deployments/solana-testnet/OFT.json` that lists the addresses of all the accounts mentioned in [OFT Account Model](#oft-account-model). :::warning **There can only be one 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. ::: ### Updating `layerzero.config.ts` #### Setting `enforcedOptions` The LayerZero CLI makes use of the [Simple Config Generator](/docs/developers/evm/technical-reference/simple-config.md) and includes `enforcedOptions` that will be applied when you run the wire command. For a send to work, it is required to have either `enforcedOptions` or `extraOptions` (provided in a `send` call). Since the example `layerzero.config.ts` applies `enforcedOptions` by default, you will not need to supply `extraOptions` when calling `send`. Before going to production, it is recommended to refer to the [LayerZero Config Defaults](https://layerzeroscan.com/tools/defaults) to understand the right config parameters for specific pathways. For testing purposes, you may use the existing values in the `layerzero.config.ts` provided. Learn more about options at [Message Execution Options](#message-execution-options) #### Optional: Setting New Delegate During OFT Initialization, you have the opportunity to set a **delegate**. This address will have the ability to implement custom configurations such as setting DVNs, Executors, and message debugging functions such as skipping inbound packets. You will also be able to set a delegate address after the OFT has been initialized and configured. To set a delegate, modify the `config` object in `layerzero.config.ts` as follows: ```typescript const config: OAppOmniGraphHardhat = { contracts: [ { contract: sepoliaContract, }, { contract: solanaContract, config: { delegate: '', } }, ], ... // the rest of the config object ``` The change in `delegate` will take effect when you run the wiring step. ### Initializing the Solana OFT :::caution Do this only when initializing the OFT for the first time. The only exception is if a new pathway is added later. If so, run this again to properly initialize the pathway. ::: This script inits the config on the Solana side, which is necessary given the self-ownership model for Solana OFTs. ``` pnpm hardhat lz:oft:solana:init-config --oapp-config layerzero.config.ts ``` ### Configuring LayerZero Contracts/Program LayerZero contracts have unique configurations on a per pathway basis (i.e., from A to B has different properties than from B to A). :::tip This guide assumes you already have deployed other OFT Instances on your desired EVM or other non-EVM chains. If you have not deployed any other OFT contracts yet, see the [**OFT Quickstart**](../../evm/oft/quickstart.md) in the EVM section. ::: Now that you have deployed your Solana OFT, you will need to connect the OFT Instance to your other chains. While LayerZero provides default configuration settings for most pathways, you should only connect your OFT Instances on different chains after viewing your [DVN and Executor Configuration Settings](../configuration/dvn-executor-config.md). Your configurations are set via the `layerzero.config.ts` file. Once you've finished selecting and preparing your configurations, you can activate them by running the wiring script. ``` pnpm 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. Under the hood, the wiring script takes care of calling methods such as `setPeer`, which handles the allowlisting of the messaging of OApps cross-chain, which enables OFTs. The wiring script will also create and execute transactions to set the on-chain configs to match the `layerzero.config.ts`. For example, changing owners and delegates can also be done by updating the `layerzero.config.ts` file, and then running the wiring script. :::caution `setPeer` opens your OFT to start receiving messages from the address set, meaning you should configure any application settings you intend on changing prior to calling `setPeer`. :::

:::warning OFTs need `setPeer` to be called correctly on both Chain A and Chain B to send and receive 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 burn source funds without a corresponding mint on destination. You can confirm the peer address is the expected destination OFT address by using the `isPeer` function. :::

### Message Execution Options `_options` are a generated bytes array with specific instructions for the [DVNs](../../../concepts/modular-security/security-stack-dvns.md) and [Executor](../../../concepts/permissionless-execution/executors.md) to when handling cross-chain 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. In a [previous section](#recommended-setting-enforcedoptions), we already went through how to set `enforcedOptions`, so in this section we'll show you how to generate `_options` to pass through as `extraOptions`. If you had already set `enforcedOptions`, then you can pass an empty bytes array (`0x` if sending from EVM, `Buffer.from('')` if sending from Solana) and skip forward to [Estimating Fees and Calling Send](#estimating-fees-and-calling-send). 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 [Solana Execution Gas Options](../gas-settings/options.md), 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. :::info 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. ::: :::caution 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 Enforced 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 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 [Estimating Fees and Calling Send](#estimating-fees-and-calling-send). :::tip `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 Enforced Options Inbound to Solana For sends to Solana, **you must recommend that you set at minimum 0.0025 SOL (2_500_000 lamports) in your `lzReceiveOption` when sending to Solana**. If setting `enforcedOptions` via `layerzero.config.ts`, this parameter is referred to as `value`. When using the OptionsBuilder in Typescript, this is the second parameter to the `addExecutorLzReceiveOption` call. The absolute minimum `value` is `1_500_000` ,but at this figure, transactions may still fail, so we recommend `2_500_000` instead to have some buffer on top. :::info 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. ::: To pass in `extraOptions` for the send from EVM (Sepolia, in our example) to Solana, modify `tasks/evm/send.ts` ```javascript import {Options} from '@layerzerolabs/lz-v2-utilities'; // ... // add the following 3 lines anywhere before the sendParam declaration const GAS_LIMIT = 200_000; // Gas (Compute Units in Solana) limit for the executor const MSG_VALUE = 2_500_000; // msg.value for the lzReceive() function on destination in lamports const _options = Options.newOptions().addExecutorLzReceiveOption(GAS_LIMIT, MSG_VALUE); // ... // replace the extraOptions value in sendParam const sendParam = { dstEid, to: makeBytes32(bs58.decode(to)), amountLD: amountLD.toString(), minAmountLD: amountLD.mul(9_000).div(10_000).toString(), extraOptions: _options.toHex(), // <-- here composeMsg: '0x', oftCmd: '0x', }; ``` We will call this script in the next section. ### Estimating Fees and Calling Send Both send scripts take care of fees estimation, which is done via `quote()`/`quoteSend()` calls on the OApp. For reference, in `tasks/solana/sendOFT.ts`: ```typescript const { nativeFee } = await oft.quote( umi.rpc, { payer: umiWalletSigner.publicKey, tokenMint: mint, tokenEscrow: umiEscrowPublicKey, }, ``` In `tasks/evm/send.ts`: ``` const [msgFee] = await token.functions.quoteSend(sendParam, false) ``` Now, we can proceed to sending our OFT across chains. From Solana Devnet to Sepolia: ```bash pnpm hardhat lz:oft:solana:send --amount --from-eid 40168 --to --to-eid 40161 ``` From Sepolia to Solana Devnet: ```bash pnpm hardhat --network sepolia-testnet send --dst-eid 40168 --amount --to ``` **Congratulations**! You've now unlocked the power of cross-chain transfers (without asset-wrapping or middlechains) through OFTs. ## Additional Information ### 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`. :::info 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 // @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. :::caution Shared decimals also control how token transfer precision is calculated. ::: ### Token Transfer Precision The OFT Standard also handles differences in decimal precision before every cross-chain 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 ``` :::tip 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. ### Adding Send and Receive Logic In Solana, the concept of function overrides as commonly understood in object-oriented languages like Solidity does not directly apply. Because of this, to change or add any custom business logic to the token, you will need to deploy your own variant of the OFT Program. For more information, visit the [OFT Program Library](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/solana/programs). --- --- title: LayerZero V2 Solana OFT SDK sidebar_label: Solana OFT SDK --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; 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/solana/sendOFT.ts](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/solana/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 // 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.71` - `@layerzerolabs/lz-v2-utilities@^3.0.71` - `@layerzerolabs/lz-definitions@^3.0.71` - `@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: ``` "overrides": { "@solana/web3.js": "~1.95.8" } ``` ``` "pnpm": { "overrides": { "@solana/web3.js": "~1.95.8" } } ``` ``` "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. --- --- title: Solana Execution Gas Options description: Learn how to generate message execution options and how to use them in your LayerZero contracts. --- Because the source chain has no concept of the destination chain's state, you must specify the amount of gas you anticipate will be necessary for executing your `lzReceive` or `lzCompose` transaction on the destination chain. LayerZero provides robust **Message Execution Options**, which allow you to specify the `gas_limit` and `msg.value` used in the Executor's transaction for message delivery on EVM chains, an amount of native gas token to airdrop to any destination address, or whether messages should be executed in a specific order. The most common options you will use when building are `lzReceiveOption`, `lzComposeOption`, and `lzNativeDropOption`. ### Options Builders An off-chain SDK has been provided to build specific Message Execution Options for your application. - `options.ts`: Can be imported from [`@layerzerolabs/lz-v2-utilities`](https://www.npmjs.com/package/@layerzerolabs/lz-v2-utilities). ### Generating Options Since both the EVM and Solana versions use the same **Options SDK**, review the EVM section for all of the [Available Options Types](/v2/developers/evm/configuration/options#option-types). ### Sending Outbound to EVM Chains When sending messages from Solana to an EVM chain, you will supply: - **Gas Limit** and **Message Value** necessary to execute the destination transaction in wei. ```javascript Options.newOptions().addExecutorLzReceiveOption(gas_limit, msg.value); ``` These execution options will be charged to the payer when quoting a cross-chain send transfer on Solana. ### Sending Outbound to Solana When sending to Solana, instead of supplying the `gas_limit` and `msg.value`, you will supply: - **Compute Units**: a unit of compute, per Solana-BPF instruction, intended to approximate the cost to execute the instruction. Similar to gas units on Ethereum. - **Lamports**: the smallest atomic unit of SOL. 1 SOL is equal to one billion (10⁹) lamports. ```javascript Options.newOptions().addExecutorLzReceiveOption(compute_units, lamports); ``` ```javascript Options.newOptions().addExecutorNativeDropOption(lamports, receiver); ``` Because Solana programs pull the necessary SOL from the sender’s account rather than pushing it with the transaction like `msg.value`, the **lzReceiveOption** will drop the amount of `lamports` specified into the destination OApp's account before executing the transaction logic. :::caution You must send at least **0.0015 SOL (1500000 lamports)** in your `lamports` field of execution options when sending to Solana. Unlike EVM addresses, every Solana Account requires a minimum balance of the native gas token to exist rent free. To send gas, therefore, you will need a minimum amount of lamports to execute and initialize the account within the transaction. ::: ### Further Reading - [Fees on Solana - Solana Docs](https://solana.com/docs/core/fees) - [Introduction to Solana Compute Units and Transaction Fees - RareSkills](https://www.rareskills.io/post/solana-compute-unit-price) - [What is the Gas Limit? - Ethereum Docs](https://ethereum.org/en/developers/docs/gas/#what-is-gas-limit) - [What is msg.value? - Ethereum Stack Exchange](https://ethereum.stackexchange.com/questions/43362/what-is-msg-value) --- --- title: Solana DVN and Executor Configuration sidebar_label: DVN and Executor Configuration toc_min_heading_level: 2 toc_max_heading_level: 5 --- Before setting your DVN and Executor Configuration, you should review the [Security Stack Core Concepts](../../../concepts/modular-security/security-stack-dvns.md). 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.md#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. :::tip Use the LayerZero CLI The LayerZero CLI has abstracted these calls for every supported chain. See the [**CLI Setup Guide**](../../evm/create-lz-oapp/start.md) 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 npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` Alternatively, you can also retrieve it via the following script. ```typescript 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 { 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: [] } } } ``` :::info 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 :::tip The [**create-lz-oapp**](../../evm/create-lz-oapp/start.md#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](/docs/concepts/glossary.md#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) | :::tip 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) | :::warning 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)** | :::warning 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 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)** | :::warning 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. ::: --- --- title: LayerZero Scan --- [LayerZero Scan](https://layerzeroscan.com/) is a comprehensive search, API, and analytics platform designed to streamline the experience for developers and users dealing with omnichain transactions. ![Scan-Light](/img/learn/scan.png#gh-light-mode-only) ![Scan-Dark](/img/learn/scan.png#gh-dark-mode-only) ## Overview Scan offers an enhanced developer experience for working with omnichain transactions in the LayerZero protocol by providing: - **Unified Message Explorer**: Track LayerZero transactions across multiple chains within a single interface. - **Protocol Analytics**: Monitor marketwide trends and the state of the ecosystem through detailed analytics. - **Scan Client**: Interface your frontend applications with omnichain transaction logs seamlessly. Developers can monitor transactions on both the [Mainnet Explorer](https://layerzeroscan.com/) and [Testnet Explorer](https://testnet.layerzeroscan.com/). ## Transaction Statuses ![Scan-Light](/img/learn/tx-statuses.png#gh-light-mode-only) ![Scan-Dark](/img/learn/tx-statuses.png#gh-dark-mode-only) - **Delivered**: The message has been successfully sent and received by the destination chain. - **Inflight**: The message is currently being transmitted between chains and has not yet reached its destination. - **Payload Stored**: The message arrived at the destination, but reverted or ran out of gas during execution and needs to be retried. - **Failed**: The transaction encountered an error and did not complete. - **Blocked**: A previous message nonce has a stored payload, halting the current transaction. - **Confirming**: The system is validating the finality of a transaction amidst potential high gas replacements or block reorgs. ## Protocol Analytics Users can also monitor protocol and chain analytics, making it easy to observe market wide trends and understand the current state of the ecosystem. ![Scan-Analytics-Light](/img/learn/scan-analytics.png#gh-light-mode-only) ![Scan-Analytics-Dark](/img/learn/scan-analytics.png#gh-dark-mode-only) --- --- title: Common Errors --- 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 (or 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 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](https://docs.layerzero.network/v2/developers/evm/technical-reference/deployed-contracts). Instead, this address is the Executor Config PDA. It can be derived using the following: ```typescript const executorProgramId = '6doghB248px58JSSwG4qejQ46kFMW4AMj7vzJnWZHNZn'; console.log(new ExecutorPDADeriver('executorProgramId').config()); ``` The result is: ```text AwrbHeCyniXaQhiJZkLhgWdUCteeWSGaSN1sTfLiY7xK ``` The full error message looks similar to below: ```text 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 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 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 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). --- --- title: Frequently Asked Questions (FAQ) --- ### How do I renounce my Solana OFT's Freeze Authority? 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. --- --- sidebar_label: Start Here --- # LayerZero V2 Aptos Move Standards **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**. :::info 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 ## Configuration

:::tip 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: A comprehensive block 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. --- --- title: Quickstart - Create Your First Omnichain App sidebar_label: CLI Setup Guide --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; This guide will walk you through the process of sending a simple cross-chain 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. :::info 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. ::: ![OApp Example](/img/learn/ABLight.svg#gh-light-mode-only) ![OApp Example](/img/learn/ABDark.svg#gh-dark-mode-only) ## 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. :::caution Disclaimer The Aptos CLI is currently in **alpha**. While progress is being made toward a full build compatible with all create-lz-oapp examples, the CLI is not yet production-ready. For now, you can follow its progress in the LayerZero devtools repo 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.md) or wait for the [**official create-lz-oapp Aptos release**](https://github.com/LayerZero-Labs/devtools/pull/1080). ::: --- --- title: LayerZero V2 Aptos Move OApp sidebar_label: Omnichain Application (OApp) --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; 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. ![OApp Example](/img/learn/ABLight.svg#gh-light-mode-only) ![OApp Example](/img/learn/ABDark.svg#gh-dark-mode-only) 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.md) and/or [Solana](../../solana/oapp/overview.md): 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 cross-chain messages - Manage **Admin** and **Delegate** roles for secure cross-chain 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** cross-chain 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 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 cross-chain 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 cross-chain 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 cross-chain. - **`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](../configuration/options.md). ```rust module oapp::oapp_core { use endpoint_v2::endpoint; use endpoint_v2_common::bytes32::Bytes32; use std::option::{self, Option}; friend oapp::oapp; /// Sends a cross-chain 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 cross-chain 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 cross-chain 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 cross-chain 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 module oapp::oapp_receive { use endpoint_v2::endpoint; /// Main entry for receiving a cross-chain 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.md#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 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 cross-chain 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 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 cross-chain 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 cross-chain 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 cross-chain 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. --- --- title: LayerZero V2 Aptos Move OFT sidebar_label: Omnichain Fungible Token (OFT) --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; 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.md) or [Solana](../../solana/oft/program.md). 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. ![OFT Example](/img/learn/oft_mechanism_light.jpg#gh-light-mode-only) ![OFT Example](/img/learn/oft_mechanism.jpg#gh-dark-mode-only) - **Lock + Unlock** (OFT Adapter): Move supply into an escrow on the source, release it on the destination. ![OFT Example](/img/learn/oft-adapter-light.svg#gh-light-mode-only) ![OFT Example](/img/learn/oft-adapter-dark.svg#gh-dark-mode-only) 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 cross-chain, 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** cross-chain messages (`lz_send`) - **Receive** cross-chain messages (via `lz_receive`) - **Quote** cross-chain 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 cross-chain 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 cross-chain token operations. ## OFT: `oft::oft_fa` When tokens are sent cross-chain, 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`:
Code Snippet: debit_fungible_asset (Burn on Send) ```rust 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) } ```
Code Snippet: credit (Mint on Receive) ```rust public(friend) fun credit( to: address, amount_ld: u64, src_eid: u32, lz_receive_value: Option, ): u64 acquires OftImpl { // 1. (Optional) deposit cross-chain 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 cross-chain 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 cross-chain. The approach is the same on Aptos:
Code Snippet: debit_fungible_asset (Lock on Send) ```rust 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) } ```
Code Snippet: credit (Unlock on Receive) ```rust public(friend) fun credit( to: address, amount_ld: u64, src_eid: u32, lz_receive_value: Option, ): u64 acquires OftImpl { // 1. (Optional) deposit cross-chain “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. :::warning 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 cross-chain messages from `oft_core` eventually call your `credit(...)`. Example from `oft.move`: ```rust 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 cross-chain 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 cross-chain 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 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 cross-chain 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 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 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 cross-chain 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 cross-chain 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 cross-chain 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 cross-chain messaging on Aptos, providing a robust, modular framework for omnichain fungible tokens. --- --- title: Aptos Execution Options description: Learn how to generate message execution options and how to use them in your LayerZero contracts. --- When sending cross-chain messages, the source chain has no knowledge of the destination chain's state or the resources required to execute a transaction on it. To bridge this gap, **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. ### Why Execution Options? Because the execution environment differs between chains (e.g., `gas` units consumed, `native` tokens sent, the `receiver` application called), you need a way to communicate the execution requirements of your messages per destination chain to the off-chain infrastructure that will process the request. - **Specifying Gas Limit**: You anticipate that your `EndpointV2.lzReceive()` function on the destination chain will require a certain amount of gas to execute. You specify this gas limit in your options so the Executor can allocate the appropriate resources. - **Transferring Native Tokens**: You want to send a certain amount of the destination chain's `native` token (e.g., APT on Aptos) along with your message. By specifying `msg.value`, you inform the Executor to include this amount in the transaction. ### How Do Execution Options Work? When sending a LayerZero message, every send call requires `options` sent to the Endpoint, which are interpreted by the Send Library and forwarded to the configured Executor and DVN(s). ```rust // oapp_core.move options: vector endpoint::send( &oapp_store::call_ref(), dst_eid, get_peer_bytes32(dst_eid), message, // highlight-next-line options, native_fee, zro_fee, ) ``` #### 1. Building Options - You use the correct library to serialize your request for gas units and native tokens. - The SDK serializes your requests (e.g., `gas limit`, `msg.value`) into a bytes array. ```typescript import {Options} from '@layerzerolabs/lz-v2-utilities'; let options = Options.newOptions() .addExecutorLzReceiveOption(gas_limit_wei, msg_value_wei) .toBytes(); ``` #### 2. Sending the Message You include the serialized `options` when sending your cross-chain message via the LayerZero Endpoint. The caller pays `fees` returned from the configured DVNs and Executor. ```rust endpoint::send( &oapp_store::call_ref(), dst_eid, get_peer_bytes32(dst_eid), message, // highlight-next-line options, native_fee, zro_fee, ) ``` #### 3. Executor Processing - The Executor receives the message and deserializes the options to see the number of gas units and native tokens requested, and calculate the `fees` for sending the message. - Once the fees have been paid, the Executor uses these parameters to execute the transaction on the destination chain by calling `EndpointV2.lzReceive()` after the message has been successfully verified. Each Executor may support different pathways, maximum gas amounts, etc. ### Options Builders An off-chain SDK has been provided to build specific Message Execution Options for your application. - `options.ts`: Can be imported from [`@layerzerolabs/lz-v2-utilities`](https://www.npmjs.com/package/@layerzerolabs/lz-v2-utilities). Since both the EVM and Aptos versions use the same **Options SDK**, review the EVM section for all of the [Available Options Types](/v2/developers/evm/configuration/options#option-types). At a high level, you can specify the gas settings for `EndpointV2.lzReceive()` and `EndpointV2.lzCompose()` if necessary, as well as options for sending native tokens to specific EOA. ### Sending Outbound to EVM Chains When sending messages from Aptos to an EVM chain, you will supply: - **Gas Limit** and **Message Value** necessary to execute the destination transaction in wei. ```javascript Options.newOptions().addExecutorLzReceiveOption(gas_limit, msg_value); ``` These execution options will be charged to the payer when quoting a cross-chain send transfer on Aptos. ### Sending Outbound to Aptos Similar to the EVM, Aptos users can set a gas limit for their transactions, specifying the maximum amount of gas they are willing to consume. When sending messages from an EVM chain to Aptos, you will supply: - **Gas Limit** and **Message Value** necessary to execute the destination transaction on Aptos. ```javascript Options.newOptions().addExecutorLzReceiveOption(gas_limit, msg_value); ``` **Gas Limit**: Specifies the maximum amount of gas units that can be consumed during the execution of the `EndpointV2.lzReceive()` function on Aptos. Aptos gas units are similar to EVM gas units but may have different costs and limits. **Message Value**: Represents the amount of the native Aptos token (APT) to be sent along with the message to the destination contract. This value functions similarly to `msg.value` on EVM chains. :::info The required Aptos gas limit amount will generally be lower than those on EVM chains. It's recommended to start with a gas limit of around 1,500 units for the `lzReceive` function on Aptos V2. ::: #### Further Reading - [Aptos Gas Fees](https://aptos.dev/en/network/blockchain/gas-txn-fee#estimating-gas-consumption-via-simulation) --- --- title: Aptos DVN and Executor Configuration sidebar_label: DVN & Executor Configuration toc_min_heading_level: 2 toc_max_heading_level: 5 --- Before setting your DVN and Executor Configuration, you should review the [Security Stack Core Concepts](../../../concepts/modular-security/security-stack-dvns.md). 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.md#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. :::tip Use the LayerZero CLI The LayerZero CLI has abstracted these calls for every supported chain. See the [**CLI Setup Guide**](../../aptos-move/create-lz-oapp/start.md) 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 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 public entry fun set_send_library( account: &signer, remote_eid: u32, msglib: address, ) { ... } ``` Which can be invoked like: ```typescript 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 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 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 struct ExecutorConfig has copy, drop { max_message_size: u32, executor_address: address, } ``` - `max_message_size`: max size of cross-chain 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 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 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) | :::tip 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) | :::warning 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)** | :::warning 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 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)** | :::warning 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. --- --- sidebar_label: Hyperliquid - Core Concepts --- # Hyperliquid & LayerZero Composer - Core Concepts 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 uses a custom consensus algorithm called HyperBFT. Hyperliquid state execution is split into two broad components: `HyperCore` and the `HyperEVM`. `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. ![Hyperliquid Stack](/img/hyperliquid/hyperliquid-stack.png) #### HyperCore **HyperCore**, or Core, is a high-performance Layer 1 that manages the exchange’s on-chain 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 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 { "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 ```bash # add ethers-v6 to your project as an alias for ethers@^6.13.5 pnpm add ethers-v6@npm:ethers@^6.13.5 ``` ```ts 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 > 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.md) 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, 2-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 {"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 npx @layerzerolabs/hyperliquid-composer set-block --size big --network mainnet --private-key $PRIVATE_KEY ``` :::note 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 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). **L1ActionPrecompiles**: - `0x0000000000000000000000000000000000000000`: One of many `L1Read` precompiles. - `0x3333333333333333333333333333333333333333`: The `L1WritePrecompile` for sending transactions to HyperCore. {" "} - `L1Read` reads from the last produced `HyperCore` block at EVM transaction execution. - `L1Write` writes to the first produced `HyperCore` block after the production of the EVM block. :::note `L1Write` is currently available on testnet only. ::: ### 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. :::warning Critical Note on Hyperliquidity Using the Hyperliquid UI for spot deployment (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.md) 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). :::info `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]` :::warning 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. ::: :::warning 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.md) 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 `L1WritePrecompile` 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 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; } ``` :::info 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 because we don't want developers to change their OFT contracts. ```solidity contract HyperLiquidComposer is IHyperLiquidComposer { constructor( address _endpoint, address _oft, uint64 _coreIndexId, // Core Spot token's index ID uint64 _weiDiff // Decimal difference: HIP1.decimals - ERC20.decimals ) {...} function lzCompose(address _oApp, bytes32 _guid, bytes calldata _message, address _executor, bytes calldata _extraData) external payable override { // ... logic to transfer to asset bridge and L1WritePrecompile ... } } ``` ### 12. LayerZero Transaction on HyperEVM Since this is a compose call, the `toAddress` is the `HyperLiquidComposer` contract address. The token receiver is encoded as an `abi.encode/Packed()` of the `receiver` address into `SendParam.composeMsg`. This is later used in the `lzCompose` phase to transfer the tokens to the L1 spot address on behalf of the `token receiver` address. ```solidity _credit(toAddress, _toLD(_message.amountSD()), _origin.srcEid) ``` This `mints` the amount in local decimals to the token receiver (`HyperLiquidComposer` contract address). We now need to create a `Transfer` event to send the tokens from HyperEVM to HyperCore, the composer computes the amount receivable on `HyerCore` based on the number of tokens in HyperCore's asset bridge, the max transferable tokens (`u64.max * scale`) and sends the tokens to itself on HyperCore (this scales the tokens based on `HyperAsset.decimalDiff`). It also sends to the `receivers` address on HyperEVM any leftover tokens from the above transformation from HyperEVM amount to HyperCore. ```solidity IHyperAssetAmount amounts = quoteHyperCoreAmount(_amount, isOft); oft::transfer(0x2000...abcd, amounts.evm); // <- gets the user amounts.core on HyperCore oft::transfer(_receiver_, amounts.dust); ``` As a result the invariant of `amounts.dust + amounts.evm = _amount` and `amounts.evm = 10.pow(decimalDiff) * amounts.core` are always satisfied. **Composer's Internal Logic (`_sendAssetToHyperCore`):** 1. Calculates `amounts.evm` (amount to send to EVM asset bridge) and `amounts.core` (equivalent amount on HyperCore), considering bridge capacity and decimal scaling. 2. Calculates `amounts.dust` (any leftover EVM amount that cannot be bridged). 3. `token.safeTransfer(oftAsset.assetBridgeAddress, amounts.evm);` → This moves tokens to HyperCore side. 4. `IHyperLiquidWritePrecompile(HLP_PRECOMPILE_WRITE).sendSpot(_receiver, oftAsset.coreIndexId, amounts.core);` → This moves tokens on HyperCore from composer to receiver. 5. `token.safeTransfer(_receiver, amounts.dust);` → Refunds dust to receiver on HyperEVM. ```solidity function _sendAssetToHyperCore(address _receiver, uint256 _amountLD) internal virtual { IHyperAssetAmount memory amounts = quoteHyperCoreAmount(_amountLD, true); if (amounts.evm > 0) { token.safeTransfer(oftAsset.assetBridgeAddress, amounts.evm); IHyperLiquidWritePrecompile(HLP_PRECOMPILE_WRITE).sendSpot(_receiver, oftAsset.coreIndexId, amounts.core); } if (amounts.dust > 0) { token.safeTransfer(_receiver, amounts.dust); } } ``` ### 10. OFTWrapper for Hyperliquid Bridge Using `Hyperliquid Bridge` will incur a fee (currently `0bp`) that may be enabled once write precompiles are enabled. We use Stargate's [OFT Wrapper](https://github.com/stargate-protocol/stargate-v2/blob/main/packages/stg-evm-v2/src/peripheral/oft-wrapper/OFTWrapper.sol) on ALL networks that we support on `Hyperliquid Bridge`. The repository that we use to deploy the bridge on various networks: [LayerZero-Labs/hyperliquid-oft-wrapper](https://github.com/LayerZero-Labs/hyperliquid-oft-wrapper) --- --- sidebar_label: Hyperliquid OFT Deployment Guide --- # Deployment Guide - OFT on Hyperliquid with LayerZero Composer 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.md). - **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 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 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 npx @layerzerolabs/hyperliquid-composer set-block \ --size small \ --network \ --log-level verbose \ --private-key $PRIVATE_KEY ``` :::info 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 | `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 #### 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 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 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 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 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 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 (for mainnet) - 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:** :::warning 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 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 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 "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 // Change this: "existingTokenAndWei": [ { "token": 0, "wei": "" } ] // To this: "existingTokenAndWei": [] ``` - **Command:** ```bash 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 Warning: This action is irreversible. ::: - **Action:** Confirm User Genesis. - **Command:** ```bash 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 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 curl -X POST "https://api.hyperliquid-testnet.xyz/info" \ # or mainnet URL -H "Content-Type: application/json" \ -d '{"type": "tokenDetails", "tokenId": ""}' ``` - `` is the on-chain 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 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. :::info 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. :::warning 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 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. :::warning 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 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 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] ``` :::warning 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 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 needs to perform `L1WritePrecompile` actions on HyperCore to transfer tokens to the final recipient. - **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 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.encodePacked(actualReceiverAddressOnHyperliquid)`. - `SendParam.extraOptions` might be used to specify gas for the `lzCompose` call and the subsequent L1 action. ### Modifying OFT/Composer Behavior & Error Handling The `HyperLiquidComposer` contract has built-in checks and error handling because Hyperliquid's native bridge mechanics do not prevent certain fund-locking scenarios. - **Transfer Exceeding `u64.max` on HyperCore:** HyperCore's `spotSend` (L1 action) supports a max of `u64` tokens. If an EVM amount translates to more than `u64.max` HIP-1 tokens, the Composer will bridge `u64.max` equivalent and refund the excess (dust) to the `receiver` on HyperEVM. - **Transfer Exceeding HyperCore Asset Bridge Capacity:** If the HyperCore side of the asset bridge doesn't have enough tokens to fulfill the requested bridge amount (e.g., `X` tokens requested, but only `Y < X` available on Core bridge), the Composer will: 1. Bridge the maximum possible amount (`Y` tokens). 2. Convert the unbridged EVM portion back to EVM tokens. 3. Refund this "dust" amount to the `receiver` on HyperEVM. - **Malformed `composeMsg` - Unable to decode `receiver` address:** If `SendParam.composeMsg` cannot be decoded into a valid HyperEVM-style address, the Composer cannot determine the final recipient on HyperCore. - **EVM Sender:** If the original LayerZero transaction `msg.sender` (on the source chain) was an EVM address, the Composer attempts to refund the tokens to this `msg.sender` on HyperEVM. - **Non-EVM Sender (e.g., Solana, Aptos):** > ⚠️ **This is a potential token lock scenario.** If the `composeMsg` is malformed AND the original `msg.sender` is from a non-EVM chain, the Composer cannot easily refund to a compatible HyperEVM address. The tokens may become locked in the Composer contract. Ideally, a cross-chain refund mechanism would be used, but this adds complexity and gas costs. You can customize the `HyperLiquidComposer.sol` contract if you need different error-handling behaviors, but be extremely cautious due to the risks involved with the Hyperliquid asset bridge. --- --- sidebar_label: LayerZero Hyperliquid SDK --- # LayerZero Hyperliquid SDK - Command Reference 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.md). To view all commands and their options, run: ```bash npx @layerzerolabs/hyperliquid-composer -h ``` ### 1. Type Conversions #### Compute the asset bridge address for a Core Spot token ```bash npx @layerzerolabs/hyperliquid-composer to-bridge --token-index ``` ### 2. Reading Core Spot State #### List Core Spot metadata ```bash 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 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 npx @layerzerolabs/hyperliquid-composer hip-token \ --token-index \ --network {testnet | mainnet} \ [--log-level {info | verbose}] ``` #### View a deployment state ```bash npx @layerzerolabs/hyperliquid-composer spot-deploy-state \ --token-index \ --network {testnet | mainnet} \ --deployer-address <0x> \ [--log-level {info | verbose}] ``` ### 3. Switching Blocks (`evmUserModify`) ```bash 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 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 npx @layerzerolabs/hyperliquid-composer set-genesis \ --token-index \ --network {testnet | mainnet} \ --private-key $PRIVATE_KEY_HYPERLIQUID \ [--log-level {info | verbose}] ``` #### 4.3 `registerSpot` ```bash npx @layerzerolabs/hyperliquid-composer register-spot \ --token-index \ --network {testnet | mainnet} \ --private-key $PRIVATE_KEY_HYPERLIQUID \ [--log-level {info | verbose}] ``` #### 4.4 `createSpotDeployment` ```bash 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 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 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 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] ``` --- --- sidebar_label: V2 Protocol Contracts and Executor title: Deployed Endpoints, Message Libraries, and Executors description: See a full list of all the blockchains LayerZero currently supports. hide_table_of_contents: true displayed_sidebar: null --- Below you can find a description of the main LayerZero V2 contracts and find the corresponding deployment information for each blockchain network LayerZero supports. :::info **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 cross-chain 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 cross-chain messages. It combines functionalities from `SendUlnBase` and `SendLibBaseE2` to ensure secure message dispatch. | | **ReceiveUln302** | A message library for receiving and verifying cross-chain 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 cross-chain messages automatically with a specified `gas limit` and `msg.value` for a fee. | | **LZ Dead DVN** | Represents a **Dead Decentralized Verifier Network (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. | ## 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). ![Checker Example](/img/defaultchecker.png) --- --- title: DVN Providers sidebar_label: DVN Providers hide_table_of_contents: true displayed_sidebar: null className: component-page !important --- 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! :::tip For example, if you want to add **LayerZero Lab's DVN** to a pathway from Ethereum to Base, first you should select: - **DVNs**: LayerZero Labs - **Chains**: Ethereum, Base Only if LayerZero Labs is on both chains, can I add that DVN to my Security Stack. :::

--- --- sidebar_label: LayerZero Read Data Channels title: Read Data Channels description: See a full list of all the blockchains LayerZero currently supports. hide_table_of_contents: true displayed_sidebar: null --- All of the **LayerZero Read** specific contract addresses and supported chains. :::tip 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. ::: --- --- title: Abstract Mainnet OFT Quickstart sidebar_label: Abstract Mainnet OFT Quickstart description: How to get started building on Abstract Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Abstract Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash LZ_ENABLE_ZKSOLC_EXAMPLE=1 npx create-lz-oapp@latest # select onft721-zksync, even if you are not using onft ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // your zkSync chain 'abstract-mainnet': { eid: EndpointId.ABSTRACT_V2_MAINNET, url: process.env.RPC_URL_ABSTRACT || 'https://api.mainnet.abs.xyz', accounts, zksync: true, // crucial for zkSync networks ethNetwork: 'mainnet', verifyURL: 'https://explorer.era.zksync.io/contract_verification', }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const abstractContract: OmniPointHardhat = { eid: EndpointId.ABSTRACT_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> abstract // abstract <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. abstract) abstractContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → abstract, confirmations for abstract → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → abstract, options for abstract → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: abstractContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose abstract ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Abstract Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=abstract&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=abstract) and [Executor](../deployed-contracts.md?chains=abstract) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Animechain Mainnet OFT Quickstart sidebar_label: Animechain Mainnet OFT Quickstart description: How to get started building on Animechain Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Animechain Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Animechain Mainnet (EID=30372) 'animechain-mainnet': { eid: EndpointId.ANIMECHAIN_V2_MAINNET, url: process.env.RPC_URL_ANIMECHAIN || 'https://INSERT-RPC', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const animechainContract: OmniPointHardhat = { eid: EndpointId.ANIMECHAIN_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> animechain // animechain <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. animechain) animechainContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → animechain, confirmations for animechain → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → animechain, options for animechain → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: animechainContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose animechain ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Animechain Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=animechain&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=animechain) and [Executor](../deployed-contracts.md?chains=animechain) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Ape Mainnet OFT Quickstart sidebar_label: Ape Mainnet OFT Quickstart description: How to get started building on Ape Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Ape Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Ape Mainnet (EID=30312) 'ape-mainnet': { eid: EndpointId.APE_V2_MAINNET, url: process.env.RPC_URL_APE || 'https://rpc.apechain.com', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const apeContract: OmniPointHardhat = { eid: EndpointId.APE_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> ape // ape <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. ape) apeContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → ape, confirmations for ape → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → ape, options for ape → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: apeContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose ape ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Ape Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=ape&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=ape) and [Executor](../deployed-contracts.md?chains=ape) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Arbitrum Mainnet OFT Quickstart sidebar_label: Arbitrum Mainnet OFT Quickstart description: How to get started building on Arbitrum Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Arbitrum Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Arbitrum Mainnet (EID=30110) 'arbitrum-mainnet': { eid: EndpointId.ARBITRUM_V2_MAINNET, url: process.env.RPC_URL_ARBITRUM || 'https://arb1.arbitrum.io/rpc', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const arbitrumContract: OmniPointHardhat = { eid: EndpointId.ARBITRUM_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> arbitrum // arbitrum <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. arbitrum) arbitrumContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → arbitrum, confirmations for arbitrum → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → arbitrum, options for arbitrum → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: arbitrumContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose arbitrum ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Arbitrum Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=arbitrum&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=arbitrum) and [Executor](../deployed-contracts.md?chains=arbitrum) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Arbitrum Nova Mainnet OFT Quickstart sidebar_label: Arbitrum Nova Mainnet OFT Quickstart description: How to get started building on Arbitrum Nova Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Arbitrum Nova Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Arbitrum Nova Mainnet (EID=30175) 'nova-mainnet': { eid: EndpointId.NOVA_V2_MAINNET, url: process.env.RPC_URL_NOVA || 'https://nova.arbitrum.io/rpc', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const novaContract: OmniPointHardhat = { eid: EndpointId.NOVA_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> nova // nova <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. nova) novaContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → nova, confirmations for nova → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → nova, options for nova → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: novaContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose nova ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Arbitrum Nova Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=nova&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=nova) and [Executor](../deployed-contracts.md?chains=nova) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Arbitrum Sepolia Testnet OFT Quickstart sidebar_label: Arbitrum Sepolia Testnet OFT Quickstart description: How to get started building on Arbitrum Sepolia Testnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Arbitrum Sepolia Testnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Arbitrum Sepolia Testnet (EID=40231) 'arbitrum-sepolia-testnet': { eid: EndpointId.ARBSEP_V2_TESTNET, url: process.env.RPC_URL_ARBITRUM_SEPOLIA || 'https://sepolia-rollup.arbitrum.io/rpc', accounts, }, // another network you want to connect to 'optimism-testnet': { eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import { EndpointId } from '@layerzerolabs/lz-definitions' import type { OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat' import { OAppEnforcedOption } from '@layerzerolabs/toolbox-hardhat' import { ExecutorOptionType } from '@layerzerolabs/lz-v2-utilities' import { TwoWayConfig, generateConnectionsConfig } from '@layerzerolabs/metadata-tools' const arbitrum-sepoliaContract: OmniPointHardhat = { eid: EndpointId.ARBSEP_V2_TESTNET, contractName: 'MyOFT', } const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', } // To connect all the above chains to each other, we need the following pathways: // Optimism <-> arbitrum-sepolia // arbitrum-sepolia <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ] const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. arbitrum-sepolia) arbitrum-sepoliaContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → arbitrum-sepolia, confirmations for arbitrum-sepolia → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → arbitrum-sepolia, options for arbitrum-sepolia → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ] export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways) return { contracts: [{ contract: optimismContract }, { contract: arbitrum-sepoliaContract }], connections, } } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose arbitrum-sepolia ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Arbitrum Sepolia Testnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://testnet.layerzeroscan.com/tools/defaults?srcChainKey[0]=arbitrum-sepolia&version=V2&dstChainKey[0]=optimism-sepolia) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=arbitrum-sepolia) and [Executor](../deployed-contracts.md?chains=arbitrum-sepolia) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Astar Mainnet OFT Quickstart sidebar_label: Astar Mainnet OFT Quickstart description: How to get started building on Astar Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Astar Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Astar Mainnet (EID=30210) 'astar-mainnet': { eid: EndpointId.ASTAR_V2_MAINNET, url: process.env.RPC_URL_ASTAR || 'https://astar.public.blastapi.io', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const astarContract: OmniPointHardhat = { eid: EndpointId.ASTAR_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> astar // astar <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. astar) astarContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → astar, confirmations for astar → Optimism] [20, 32], // 5) Enforced execution options: // [options for Optimism → astar, options for astar → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: astarContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose astar ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Astar Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=astar&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=astar) and [Executor](../deployed-contracts.md?chains=astar) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Astar zkEVM Mainnet OFT Quickstart sidebar_label: Astar zkEVM Mainnet OFT Quickstart description: How to get started building on Astar zkEVM Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Astar zkEVM Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Astar zkEVM Mainnet (EID=30257) 'zkatana-mainnet': { eid: EndpointId.ZKATANA_V2_MAINNET, url: process.env.RPC_URL_ZKATANA || 'https://rpc.startale.com/astar-zkevm', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const zkatanaContract: OmniPointHardhat = { eid: EndpointId.ZKATANA_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> zkatana // zkatana <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. zkatana) zkatanaContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → zkatana, confirmations for zkatana → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → zkatana, options for zkatana → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: zkatanaContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose zkatana ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Astar zkEVM Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=zkatana&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=zkatana) and [Executor](../deployed-contracts.md?chains=zkatana) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Avalanche Fuji Testnet OFT Quickstart sidebar_label: Avalanche Fuji Testnet OFT Quickstart description: How to get started building on Avalanche Fuji Testnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Avalanche Fuji Testnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Avalanche Fuji Testnet (EID=40106) 'fuji-testnet': { eid: EndpointId.AVALANCHE_V2_TESTNET, url: process.env.RPC_URL_FUJI || 'https://api.avax-test.network/ext/bc/C/rpc', accounts, }, // another network you want to connect to 'optimism-testnet': { eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const fujiContract: OmniPointHardhat = { eid: EndpointId.AVALANCHE_V2_TESTNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> fuji // fuji <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. fuji) fujiContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → fuji, confirmations for fuji → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → fuji, options for fuji → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: fujiContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose fuji ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Avalanche Fuji Testnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://testnet.layerzeroscan.com/tools/defaults?srcChainKey[0]=fuji&version=V2&dstChainKey[0]=optimism-sepolia) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=fuji) and [Executor](../deployed-contracts.md?chains=fuji) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Avalanche Mainnet OFT Quickstart sidebar_label: Avalanche Mainnet OFT Quickstart description: How to get started building on Avalanche Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Avalanche Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Avalanche Mainnet (EID=30106) 'avalanche-mainnet': { eid: EndpointId.AVALANCHE_V2_MAINNET, url: process.env.RPC_URL_AVALANCHE || 'https://api.avax.network/ext/bc/C/rpc', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const avalancheContract: OmniPointHardhat = { eid: EndpointId.AVALANCHE_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> avalanche // avalanche <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. avalanche) avalancheContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → avalanche, confirmations for avalanche → Optimism] [20, 12], // 5) Enforced execution options: // [options for Optimism → avalanche, options for avalanche → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: avalancheContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose avalanche ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Avalanche Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=avalanche&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=avalanche) and [Executor](../deployed-contracts.md?chains=avalanche) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Bahamut Mainnet OFT Quickstart sidebar_label: Bahamut Mainnet OFT Quickstart description: How to get started building on Bahamut Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Bahamut Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Bahamut Mainnet (EID=30363) 'bahamut-mainnet': { eid: EndpointId.BAHAMUT_V2_MAINNET, url: process.env.RPC_URL_BAHAMUT || 'https://rpc1.bahamut.io', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const bahamutContract: OmniPointHardhat = { eid: EndpointId.BAHAMUT_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> bahamut // bahamut <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. bahamut) bahamutContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → bahamut, confirmations for bahamut → Optimism] [20, 15], // 5) Enforced execution options: // [options for Optimism → bahamut, options for bahamut → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: bahamutContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose bahamut ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Bahamut Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=bahamut&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=bahamut) and [Executor](../deployed-contracts.md?chains=bahamut) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Base Mainnet OFT Quickstart sidebar_label: Base Mainnet OFT Quickstart description: How to get started building on Base Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Base Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Base Mainnet (EID=30184) 'base-mainnet': { eid: EndpointId.BASE_V2_MAINNET, url: process.env.RPC_URL_BASE || 'https://mainnet.base.org', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const baseContract: OmniPointHardhat = { eid: EndpointId.BASE_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> base // base <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. base) baseContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → base, confirmations for base → Optimism] [20, 10], // 5) Enforced execution options: // [options for Optimism → base, options for base → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: baseContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose base ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Base Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=base&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=base) and [Executor](../deployed-contracts.md?chains=base) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Base Sepolia Testnet OFT Quickstart sidebar_label: Base Sepolia Testnet OFT Quickstart description: How to get started building on Base Sepolia Testnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Base Sepolia Testnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Base Sepolia Testnet (EID=40245) 'base-sepolia-testnet': { eid: EndpointId.BASESEP_V2_TESTNET, url: process.env.RPC_URL_BASE_SEPOLIA || 'https://sepolia.base.org', accounts, }, // another network you want to connect to 'optimism-testnet': { eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import { EndpointId } from '@layerzerolabs/lz-definitions' import type { OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat' import { OAppEnforcedOption } from '@layerzerolabs/toolbox-hardhat' import { ExecutorOptionType } from '@layerzerolabs/lz-v2-utilities' import { TwoWayConfig, generateConnectionsConfig } from '@layerzerolabs/metadata-tools' const base-sepoliaContract: OmniPointHardhat = { eid: EndpointId.BASESEP_V2_TESTNET, contractName: 'MyOFT', } const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', } // To connect all the above chains to each other, we need the following pathways: // Optimism <-> base-sepolia // base-sepolia <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ] const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. base-sepolia) base-sepoliaContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → base-sepolia, confirmations for base-sepolia → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → base-sepolia, options for base-sepolia → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ] export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways) return { contracts: [{ contract: optimismContract }, { contract: base-sepoliaContract }], connections, } } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose base-sepolia ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Base Sepolia Testnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://testnet.layerzeroscan.com/tools/defaults?srcChainKey[0]=base-sepolia&version=V2&dstChainKey[0]=optimism-sepolia) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=base-sepolia) and [Executor](../deployed-contracts.md?chains=base-sepolia) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Beam Mainnet OFT Quickstart sidebar_label: Beam Mainnet OFT Quickstart description: How to get started building on Beam Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Beam Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Beam Mainnet (EID=30198) 'beam-mainnet': { eid: EndpointId.MERITCIRCLE_V2_MAINNET, url: process.env.RPC_URL_BEAM || 'https://build.onbeam.com/rpc', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const beamContract: OmniPointHardhat = { eid: EndpointId.MERITCIRCLE_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> beam // beam <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. beam) beamContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → beam, confirmations for beam → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → beam, options for beam → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: beamContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose beam ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Beam Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=beam&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=beam) and [Executor](../deployed-contracts.md?chains=beam) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Berachain Mainnet OFT Quickstart sidebar_label: Berachain Mainnet OFT Quickstart description: How to get started building on Berachain Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Berachain Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Berachain Mainnet (EID=30362) 'bera-mainnet': { eid: EndpointId.BERA_V2_MAINNET, url: process.env.RPC_URL_BERA || 'https://rpc.berachain.com', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const beraContract: OmniPointHardhat = { eid: EndpointId.BERA_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> bera // bera <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. bera) beraContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → bera, confirmations for bera → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → bera, options for bera → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: beraContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose bera ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Berachain Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=bera&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=bera) and [Executor](../deployed-contracts.md?chains=bera) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Bevm Mainnet OFT Quickstart sidebar_label: Bevm Mainnet OFT Quickstart description: How to get started building on Bevm Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Bevm Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Bevm Mainnet (EID=30317) 'bevm-mainnet': { eid: EndpointId.BEVM_V2_MAINNET, url: process.env.RPC_URL_BEVM || 'https://rpc-mainnet-1.bevm.io/', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const bevmContract: OmniPointHardhat = { eid: EndpointId.BEVM_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> bevm // bevm <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. bevm) bevmContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → bevm, confirmations for bevm → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → bevm, options for bevm → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: bevmContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose bevm ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Bevm Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=bevm&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=bevm) and [Executor](../deployed-contracts.md?chains=bevm) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Bitlayer Mainnet OFT Quickstart sidebar_label: Bitlayer Mainnet OFT Quickstart description: How to get started building on Bitlayer Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Bitlayer Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Bitlayer Mainnet (EID=30314) 'bitlayer-mainnet': { eid: EndpointId.BITLAYER_V2_MAINNET, url: process.env.RPC_URL_BITLAYER || 'https://rpc.bitlayer.org', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const bitlayerContract: OmniPointHardhat = { eid: EndpointId.BITLAYER_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> bitlayer // bitlayer <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. bitlayer) bitlayerContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → bitlayer, confirmations for bitlayer → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → bitlayer, options for bitlayer → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: bitlayerContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose bitlayer ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Bitlayer Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=bitlayer&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=bitlayer) and [Executor](../deployed-contracts.md?chains=bitlayer) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Blast Mainnet OFT Quickstart sidebar_label: Blast Mainnet OFT Quickstart description: How to get started building on Blast Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Blast Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Blast Mainnet (EID=30243) 'blast-mainnet': { eid: EndpointId.BLAST_V2_MAINNET, url: process.env.RPC_URL_BLAST || 'https://rpc.blast.io', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const blastContract: OmniPointHardhat = { eid: EndpointId.BLAST_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> blast // blast <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. blast) blastContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → blast, confirmations for blast → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → blast, options for blast → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: blastContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose blast ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Blast Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=blast&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=blast) and [Executor](../deployed-contracts.md?chains=blast) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: BNB Smart Chain (BSC) Mainnet OFT Quickstart sidebar_label: BNB Smart Chain (BSC) Mainnet OFT Quickstart description: How to get started building on BNB Smart Chain (BSC) Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **BNB Smart Chain (BSC) Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // BNB Smart Chain (BSC) Mainnet (EID=30102) 'bsc-mainnet': { eid: EndpointId.BSC_V2_MAINNET, url: process.env.RPC_URL_BSC || 'https://bsc.drpc.org', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const bscContract: OmniPointHardhat = { eid: EndpointId.BSC_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> bsc // bsc <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. bsc) bscContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → bsc, confirmations for bsc → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → bsc, options for bsc → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: bscContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose bsc ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **BNB Smart Chain (BSC) Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=bsc&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=bsc) and [Executor](../deployed-contracts.md?chains=bsc) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: BNB Smart Chain (BSC) Testnet OFT Quickstart sidebar_label: BNB Smart Chain (BSC) Testnet OFT Quickstart description: How to get started building on BNB Smart Chain (BSC) Testnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **BNB Smart Chain (BSC) Testnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // BNB Smart Chain (BSC) Testnet (EID=40102) 'bsc-testnet': { eid: EndpointId.BSC_V2_TESTNET, url: process.env.RPC_URL_BSC || 'https://bsc-testnet.public.blastapi.io', accounts, }, // another network you want to connect to 'optimism-testnet': { eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const bscContract: OmniPointHardhat = { eid: EndpointId.BSC_V2_TESTNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> bsc // bsc <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. bsc) bscContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → bsc, confirmations for bsc → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → bsc, options for bsc → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: bscContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose bsc-testnet ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **BNB Smart Chain (BSC) Testnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://testnet.layerzeroscan.com/tools/defaults?srcChainKey[0]=bsc-testnet&version=V2&dstChainKey[0]=optimism-sepolia) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=bsc-testnet) and [Executor](../deployed-contracts.md?chains=bsc-testnet) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: BOB Mainnet OFT Quickstart sidebar_label: BOB Mainnet OFT Quickstart description: How to get started building on BOB Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **BOB Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // BOB Mainnet (EID=30279) 'bob-mainnet': { eid: EndpointId.BOB_V2_MAINNET, url: process.env.RPC_URL_BOB || 'https://rpc.gobob.xyz', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const bobContract: OmniPointHardhat = { eid: EndpointId.BOB_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> bob // bob <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. bob) bobContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → bob, confirmations for bob → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → bob, options for bob → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: bobContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose bob ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **BOB Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=bob&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=bob) and [Executor](../deployed-contracts.md?chains=bob) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Bouncebit Mainnet OFT Quickstart sidebar_label: Bouncebit Mainnet OFT Quickstart description: How to get started building on Bouncebit Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Bouncebit Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Bouncebit Mainnet (EID=30293) 'bouncebit-mainnet': { eid: EndpointId.BOUNCEBIT_V2_MAINNET, url: process.env.RPC_URL_BOUNCEBIT || 'https://fullnode-mainnet.bouncebitapi.com', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const bouncebitContract: OmniPointHardhat = { eid: EndpointId.BOUNCEBIT_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> bouncebit // bouncebit <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. bouncebit) bouncebitContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → bouncebit, confirmations for bouncebit → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → bouncebit, options for bouncebit → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: bouncebitContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose bouncebit ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Bouncebit Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=bouncebit&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=bouncebit) and [Executor](../deployed-contracts.md?chains=bouncebit) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Canto Mainnet OFT Quickstart sidebar_label: Canto Mainnet OFT Quickstart description: How to get started building on Canto Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Canto Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Canto Mainnet (EID=30159) 'canto-mainnet': { eid: EndpointId.CANTO_V2_MAINNET, url: process.env.RPC_URL_CANTO || 'https://canto.gravitychain.io', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const cantoContract: OmniPointHardhat = { eid: EndpointId.CANTO_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> canto // canto <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. canto) cantoContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → canto, confirmations for canto → Optimism] [20, 2], // 5) Enforced execution options: // [options for Optimism → canto, options for canto → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: cantoContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose canto ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Canto Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=canto&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=canto) and [Executor](../deployed-contracts.md?chains=canto) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Celo Mainnet OFT Quickstart sidebar_label: Celo Mainnet OFT Quickstart description: How to get started building on Celo Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Celo Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Celo Mainnet (EID=30125) 'celo-mainnet': { eid: EndpointId.CELO_V2_MAINNET, url: process.env.RPC_URL_CELO || 'https://forno.celo.org', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const celoContract: OmniPointHardhat = { eid: EndpointId.CELO_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> celo // celo <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. celo) celoContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → celo, confirmations for celo → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → celo, options for celo → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: celoContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose celo ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Celo Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=celo&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=celo) and [Executor](../deployed-contracts.md?chains=celo) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Codex Mainnet OFT Quickstart sidebar_label: Codex Mainnet OFT Quickstart description: How to get started building on Codex Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Codex Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Codex Mainnet (EID=30323) 'codex-mainnet': { eid: EndpointId.CODEX_V2_MAINNET, url: process.env.RPC_URL_CODEX || 'https://INSERT-RPC', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const codexContract: OmniPointHardhat = { eid: EndpointId.CODEX_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> codex // codex <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. codex) codexContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → codex, confirmations for codex → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → codex, options for codex → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: codexContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose codex ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Codex Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=codex&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=codex) and [Executor](../deployed-contracts.md?chains=codex) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Concrete OFT Quickstart sidebar_label: Concrete OFT Quickstart description: How to get started building on Concrete and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Concrete** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Concrete (EID=30366) 'concrete-mainnet': { eid: EndpointId.CONCRETE_V2_MAINNET, url: process.env.RPC_URL_CONCRETE || 'https://INSERT-RPC', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const concreteContract: OmniPointHardhat = { eid: EndpointId.CONCRETE_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> concrete // concrete <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. concrete) concreteContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → concrete, confirmations for concrete → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → concrete, options for concrete → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: concreteContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose concrete ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Concrete** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=concrete&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=concrete) and [Executor](../deployed-contracts.md?chains=concrete) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Conflux eSpace Mainnet OFT Quickstart sidebar_label: Conflux eSpace Mainnet OFT Quickstart description: How to get started building on Conflux eSpace Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Conflux eSpace Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Conflux eSpace Mainnet (EID=30212) 'conflux-mainnet': { eid: EndpointId.CONFLUX_V2_MAINNET, url: process.env.RPC_URL_CONFLUX || 'https://evm.confluxrpc.com', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const confluxContract: OmniPointHardhat = { eid: EndpointId.CONFLUX_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> conflux // conflux <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. conflux) confluxContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → conflux, confirmations for conflux → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → conflux, options for conflux → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: confluxContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose conflux ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Conflux eSpace Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=conflux&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=conflux) and [Executor](../deployed-contracts.md?chains=conflux) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: CoreDAO Mainnet OFT Quickstart sidebar_label: CoreDAO Mainnet OFT Quickstart description: How to get started building on CoreDAO Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **CoreDAO Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // CoreDAO Mainnet (EID=30153) 'coredao-mainnet': { eid: EndpointId.COREDAO_V2_MAINNET, url: process.env.RPC_URL_COREDAO || 'https://rpc.coredao.org', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const coredaoContract: OmniPointHardhat = { eid: EndpointId.COREDAO_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> coredao // coredao <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. coredao) coredaoContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → coredao, confirmations for coredao → Optimism] [20, 21], // 5) Enforced execution options: // [options for Optimism → coredao, options for coredao → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: coredaoContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose coredao ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **CoreDAO Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=coredao&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=coredao) and [Executor](../deployed-contracts.md?chains=coredao) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Corn Mainnet OFT Quickstart sidebar_label: Corn Mainnet OFT Quickstart description: How to get started building on Corn Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Corn Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Corn Mainnet (EID=30331) 'mp1-mainnet': { eid: EndpointId.MP1_V2_MAINNET, url: process.env.RPC_URL_MP1 || 'https://mainnet.corn-rpc.com', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const mp1Contract: OmniPointHardhat = { eid: EndpointId.MP1_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> mp1 // mp1 <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. mp1) mp1Contract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → mp1, confirmations for mp1 → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → mp1, options for mp1 → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: mp1Contract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose mp1 ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Corn Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=mp1&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=mp1) and [Executor](../deployed-contracts.md?chains=mp1) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Cronos EVM Mainnet OFT Quickstart sidebar_label: Cronos EVM Mainnet OFT Quickstart description: How to get started building on Cronos EVM Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Cronos EVM Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Cronos EVM Mainnet (EID=30359) 'cronosevm-mainnet': { eid: EndpointId.CRONOSEVM_V2_MAINNET, url: process.env.RPC_URL_CRONOSEVM || 'https://evm.cronos.org', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const cronosevmContract: OmniPointHardhat = { eid: EndpointId.CRONOSEVM_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> cronosevm // cronosevm <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. cronosevm) cronosevmContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → cronosevm, confirmations for cronosevm → Optimism] [20, 2], // 5) Enforced execution options: // [options for Optimism → cronosevm, options for cronosevm → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: cronosevmContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose cronosevm ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Cronos EVM Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=cronosevm&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=cronosevm) and [Executor](../deployed-contracts.md?chains=cronosevm) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Cronos zkEVM Mainnet OFT Quickstart sidebar_label: Cronos zkEVM Mainnet OFT Quickstart description: How to get started building on Cronos zkEVM Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Cronos zkEVM Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash LZ_ENABLE_ZKSOLC_EXAMPLE=1 npx create-lz-oapp@latest # select onft721-zksync, even if you are not using onft ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // your zkSync chain 'cronoszkevm-mainnet': { eid: EndpointId.CRONOSZKEVM_V2_MAINNET, url: process.env.RPC_URL_CRONOSZKEVM || 'https://mainnet.zkevm.cronos.org', accounts, zksync: true, // crucial for zkSync networks ethNetwork: 'mainnet', verifyURL: 'https://explorer.era.zksync.io/contract_verification', }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const cronoszkevmContract: OmniPointHardhat = { eid: EndpointId.CRONOSZKEVM_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> cronoszkevm // cronoszkevm <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. cronoszkevm) cronoszkevmContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → cronoszkevm, confirmations for cronoszkevm → Optimism] [20, 2], // 5) Enforced execution options: // [options for Optimism → cronoszkevm, options for cronoszkevm → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: cronoszkevmContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose cronoszkevm ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Cronos zkEVM Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=cronoszkevm&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=cronoszkevm) and [Executor](../deployed-contracts.md?chains=cronoszkevm) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Cyber Mainnet OFT Quickstart sidebar_label: Cyber Mainnet OFT Quickstart description: How to get started building on Cyber Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Cyber Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Cyber Mainnet (EID=30283) 'cyber-mainnet': { eid: EndpointId.CYBER_V2_MAINNET, url: process.env.RPC_URL_CYBER || 'https://rpc.cyber.co', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const cyberContract: OmniPointHardhat = { eid: EndpointId.CYBER_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> cyber // cyber <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. cyber) cyberContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → cyber, confirmations for cyber → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → cyber, options for cyber → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: cyberContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose cyber ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Cyber Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=cyber&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=cyber) and [Executor](../deployed-contracts.md?chains=cyber) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Degen Mainnet OFT Quickstart sidebar_label: Degen Mainnet OFT Quickstart description: How to get started building on Degen Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Degen Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Degen Mainnet (EID=30267) 'degen-mainnet': { eid: EndpointId.DEGEN_V2_MAINNET, url: process.env.RPC_URL_DEGEN || 'https://rpc.degen.tips', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const degenContract: OmniPointHardhat = { eid: EndpointId.DEGEN_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> degen // degen <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. degen) degenContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → degen, confirmations for degen → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → degen, options for degen → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: degenContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose degen ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Degen Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=degen&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=degen) and [Executor](../deployed-contracts.md?chains=degen) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Dexalot Subnet Mainnet OFT Quickstart sidebar_label: Dexalot Subnet Mainnet OFT Quickstart description: How to get started building on Dexalot Subnet Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Dexalot Subnet Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Dexalot Subnet Mainnet (EID=30118) 'dexalot-mainnet': { eid: EndpointId.DEXALOT_V2_MAINNET, url: process.env.RPC_URL_DEXALOT || 'https://subnets.avax.network/dexalot/mainnet/rpc', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const dexalotContract: OmniPointHardhat = { eid: EndpointId.DEXALOT_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> dexalot // dexalot <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. dexalot) dexalotContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → dexalot, confirmations for dexalot → Optimism] [20, 10], // 5) Enforced execution options: // [options for Optimism → dexalot, options for dexalot → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: dexalotContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose dexalot ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Dexalot Subnet Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=dexalot&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=dexalot) and [Executor](../deployed-contracts.md?chains=dexalot) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: DFK Chain OFT Quickstart sidebar_label: DFK Chain OFT Quickstart description: How to get started building on DFK Chain and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **DFK Chain** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // DFK Chain (EID=30115) 'dfk-mainnet': { eid: EndpointId.DFK_V2_MAINNET, url: process.env.RPC_URL_DFK || 'https://subnets.avax.network/defi-kingdoms/dfk-chain/rpc', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const dfkContract: OmniPointHardhat = { eid: EndpointId.DFK_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> dfk // dfk <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. dfk) dfkContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → dfk, confirmations for dfk → Optimism] [20, 10], // 5) Enforced execution options: // [options for Optimism → dfk, options for dfk → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: dfkContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose dfk ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **DFK Chain** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=dfk&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=dfk) and [Executor](../deployed-contracts.md?chains=dfk) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: DM2 Verse Mainnet OFT Quickstart sidebar_label: DM2 Verse Mainnet OFT Quickstart description: How to get started building on DM2 Verse Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **DM2 Verse Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // DM2 Verse Mainnet (EID=30315) 'dm2verse-mainnet': { eid: EndpointId.DM2VERSE_V2_MAINNET, url: process.env.RPC_URL_DM2VERSE || 'https://rpc.dm2verse.dmm.com', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const dm2verseContract: OmniPointHardhat = { eid: EndpointId.DM2VERSE_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> dm2verse // dm2verse <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. dm2verse) dm2verseContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → dm2verse, confirmations for dm2verse → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → dm2verse, options for dm2verse → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: dm2verseContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose dm2verse ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **DM2 Verse Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=dm2verse&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=dm2verse) and [Executor](../deployed-contracts.md?chains=dm2verse) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: DOS Chain Mainnet OFT Quickstart sidebar_label: DOS Chain Mainnet OFT Quickstart description: How to get started building on DOS Chain Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **DOS Chain Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // DOS Chain Mainnet (EID=30149) 'dos-mainnet': { eid: EndpointId.DOS_V2_MAINNET, url: process.env.RPC_URL_DOS || 'https://main.doschain.com', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const dosContract: OmniPointHardhat = { eid: EndpointId.DOS_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> dos // dos <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. dos) dosContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → dos, confirmations for dos → Optimism] [20, 2], // 5) Enforced execution options: // [options for Optimism → dos, options for dos → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: dosContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose dos ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **DOS Chain Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=dos&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=dos) and [Executor](../deployed-contracts.md?chains=dos) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: EDU Chain Mainnet OFT Quickstart sidebar_label: EDU Chain Mainnet OFT Quickstart description: How to get started building on EDU Chain Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **EDU Chain Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // EDU Chain Mainnet (EID=30328) 'edu-mainnet': { eid: EndpointId.EDU_V2_MAINNET, url: process.env.RPC_URL_EDU || 'https://rpc.edu-chain.raas.gelato.cloud', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const eduContract: OmniPointHardhat = { eid: EndpointId.EDU_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> edu // edu <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. edu) eduContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → edu, confirmations for edu → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → edu, options for edu → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: eduContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose edu ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **EDU Chain Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=edu&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=edu) and [Executor](../deployed-contracts.md?chains=edu) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Ethereum Holesky Testnet OFT Quickstart sidebar_label: Ethereum Holesky Testnet OFT Quickstart description: How to get started building on Ethereum Holesky Testnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Ethereum Holesky Testnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Ethereum Holesky Testnet (EID=40217) 'holesky-testnet': { eid: EndpointId.HOLESKY_V2_TESTNET, url: process.env.RPC_URL_HOLESKY || 'https://ethereum-holesky.publicnode.com', accounts, }, // another network you want to connect to 'optimism-testnet': { eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const holeskyContract: OmniPointHardhat = { eid: EndpointId.HOLESKY_V2_TESTNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> holesky // holesky <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. holesky) holeskyContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → holesky, confirmations for holesky → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → holesky, options for holesky → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: holeskyContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose holesky-testnet ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Ethereum Holesky Testnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://testnet.layerzeroscan.com/tools/defaults?srcChainKey[0]=holesky-testnet&version=V2&dstChainKey[0]=optimism-sepolia) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=holesky-testnet) and [Executor](../deployed-contracts.md?chains=holesky-testnet) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Ethereum Mainnet OFT Quickstart sidebar_label: Ethereum Mainnet OFT Quickstart description: How to get started building on Ethereum Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Ethereum Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Ethereum Mainnet (EID=30101) 'ethereum-mainnet': { eid: EndpointId.ETHEREUM_V2_MAINNET, url: process.env.RPC_URL_ETHEREUM || 'https://INSERT-RPC', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const ethereumContract: OmniPointHardhat = { eid: EndpointId.ETHEREUM_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> ethereum // ethereum <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. ethereum) ethereumContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → ethereum, confirmations for ethereum → Optimism] [20, 15], // 5) Enforced execution options: // [options for Optimism → ethereum, options for ethereum → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: ethereumContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose ethereum ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Ethereum Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=ethereum&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=ethereum) and [Executor](../deployed-contracts.md?chains=ethereum) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Ethereum Sepolia Testnet OFT Quickstart sidebar_label: Ethereum Sepolia Testnet OFT Quickstart description: How to get started building on Ethereum Sepolia Testnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Ethereum Sepolia Testnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Ethereum Sepolia Testnet (EID=40161) 'sepolia-testnet': { eid: EndpointId.SEPOLIA_V2_TESTNET, url: process.env.RPC_URL_SEPOLIA || 'https://sepolia.drpc.org', accounts, }, // another network you want to connect to 'optimism-testnet': { eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const sepoliaContract: OmniPointHardhat = { eid: EndpointId.SEPOLIA_V2_TESTNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> sepolia // sepolia <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. sepolia) sepoliaContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → sepolia, confirmations for sepolia → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → sepolia, options for sepolia → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: sepoliaContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose sepolia ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Ethereum Sepolia Testnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://testnet.layerzeroscan.com/tools/defaults?srcChainKey[0]=sepolia&version=V2&dstChainKey[0]=optimism-sepolia) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=sepolia) and [Executor](../deployed-contracts.md?chains=sepolia) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Etherlink Mainnet OFT Quickstart sidebar_label: Etherlink Mainnet OFT Quickstart description: How to get started building on Etherlink Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Etherlink Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Etherlink Mainnet (EID=30292) 'etherlink-mainnet': { eid: EndpointId.ETHERLINK_V2_MAINNET, url: process.env.RPC_URL_ETHERLINK || 'https://node.mainnet.etherlink.com', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const etherlinkContract: OmniPointHardhat = { eid: EndpointId.ETHERLINK_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> etherlink // etherlink <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. etherlink) etherlinkContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → etherlink, confirmations for etherlink → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → etherlink, options for etherlink → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: etherlinkContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose etherlink ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Etherlink Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=etherlink&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=etherlink) and [Executor](../deployed-contracts.md?chains=etherlink) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: EVM on Flow Mainnet OFT Quickstart sidebar_label: EVM on Flow Mainnet OFT Quickstart description: How to get started building on EVM on Flow Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **EVM on Flow Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // EVM on Flow Mainnet (EID=30336) 'flow-mainnet': { eid: EndpointId.FLOW_V2_MAINNET, url: process.env.RPC_URL_FLOW || 'https://mainnet.evm.nodes.onflow.org', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const flowContract: OmniPointHardhat = { eid: EndpointId.FLOW_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> flow // flow <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. flow) flowContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → flow, confirmations for flow → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → flow, options for flow → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: flowContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose flow ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **EVM on Flow Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=flow&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=flow) and [Executor](../deployed-contracts.md?chains=flow) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Fantom Mainnet OFT Quickstart sidebar_label: Fantom Mainnet OFT Quickstart description: How to get started building on Fantom Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Fantom Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Fantom Mainnet (EID=30112) 'fantom-mainnet': { eid: EndpointId.FANTOM_V2_MAINNET, url: process.env.RPC_URL_FANTOM || 'https://rpcapi.fantom.network', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const fantomContract: OmniPointHardhat = { eid: EndpointId.FANTOM_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> fantom // fantom <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. fantom) fantomContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → fantom, confirmations for fantom → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → fantom, options for fantom → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: fantomContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose fantom ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Fantom Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=fantom&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=fantom) and [Executor](../deployed-contracts.md?chains=fantom) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Flare Mainnet OFT Quickstart sidebar_label: Flare Mainnet OFT Quickstart description: How to get started building on Flare Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Flare Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Flare Mainnet (EID=30295) 'flare-mainnet': { eid: EndpointId.FLARE_V2_MAINNET, url: process.env.RPC_URL_FLARE || 'https://flare-api.flare.network/ext/C/rpc', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const flareContract: OmniPointHardhat = { eid: EndpointId.FLARE_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> flare // flare <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. flare) flareContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → flare, confirmations for flare → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → flare, options for flare → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: flareContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose flare ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Flare Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=flare&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=flare) and [Executor](../deployed-contracts.md?chains=flare) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Fraxtal Mainnet OFT Quickstart sidebar_label: Fraxtal Mainnet OFT Quickstart description: How to get started building on Fraxtal Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Fraxtal Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Fraxtal Mainnet (EID=30255) 'fraxtal-mainnet': { eid: EndpointId.FRAXTAL_V2_MAINNET, url: process.env.RPC_URL_FRAXTAL || 'https://rpc.frax.com', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const fraxtalContract: OmniPointHardhat = { eid: EndpointId.FRAXTAL_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> fraxtal // fraxtal <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. fraxtal) fraxtalContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → fraxtal, confirmations for fraxtal → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → fraxtal, options for fraxtal → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: fraxtalContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose fraxtal ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Fraxtal Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=fraxtal&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=fraxtal) and [Executor](../deployed-contracts.md?chains=fraxtal) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Fuse Mainnet OFT Quickstart sidebar_label: Fuse Mainnet OFT Quickstart description: How to get started building on Fuse Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Fuse Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Fuse Mainnet (EID=30138) 'fuse-mainnet': { eid: EndpointId.FUSE_V2_MAINNET, url: process.env.RPC_URL_FUSE || 'https://rpc.fuse.io', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const fuseContract: OmniPointHardhat = { eid: EndpointId.FUSE_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> fuse // fuse <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. fuse) fuseContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → fuse, confirmations for fuse → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → fuse, options for fuse → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: fuseContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose fuse ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Fuse Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=fuse&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=fuse) and [Executor](../deployed-contracts.md?chains=fuse) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Glue Mainnet OFT Quickstart sidebar_label: Glue Mainnet OFT Quickstart description: How to get started building on Glue Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Glue Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Glue Mainnet (EID=30342) 'glue-mainnet': { eid: EndpointId.GLUE_V2_MAINNET, url: process.env.RPC_URL_GLUE || 'https://testnet-node-1.server-1.glue.net', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const glueContract: OmniPointHardhat = { eid: EndpointId.GLUE_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> glue // glue <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. glue) glueContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → glue, confirmations for glue → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → glue, options for glue → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: glueContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose glue ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Glue Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=glue&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=glue) and [Executor](../deployed-contracts.md?chains=glue) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Gnosis Mainnet OFT Quickstart sidebar_label: Gnosis Mainnet OFT Quickstart description: How to get started building on Gnosis Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Gnosis Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Gnosis Mainnet (EID=30145) 'gnosis-mainnet': { eid: EndpointId.GNOSIS_V2_MAINNET, url: process.env.RPC_URL_GNOSIS || 'https://rpc.gnosischain.com', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const gnosisContract: OmniPointHardhat = { eid: EndpointId.GNOSIS_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> gnosis // gnosis <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. gnosis) gnosisContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → gnosis, confirmations for gnosis → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → gnosis, options for gnosis → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: gnosisContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose gnosis ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Gnosis Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=gnosis&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=gnosis) and [Executor](../deployed-contracts.md?chains=gnosis) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Goat Mainnet OFT Quickstart sidebar_label: Goat Mainnet OFT Quickstart description: How to get started building on Goat Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Goat Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Goat Mainnet (EID=30361) 'goat-mainnet': { eid: EndpointId.GOAT_V2_MAINNET, url: process.env.RPC_URL_GOAT || 'https://rpc.goat.network', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const goatContract: OmniPointHardhat = { eid: EndpointId.GOAT_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> goat // goat <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. goat) goatContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → goat, confirmations for goat → Optimism] [20, 4], // 5) Enforced execution options: // [options for Optimism → goat, options for goat → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: goatContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose goat ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Goat Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=goat&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=goat) and [Executor](../deployed-contracts.md?chains=goat) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Gravity Mainnet OFT Quickstart sidebar_label: Gravity Mainnet OFT Quickstart description: How to get started building on Gravity Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Gravity Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Gravity Mainnet (EID=30294) 'gravity-mainnet': { eid: EndpointId.GRAVITY_V2_MAINNET, url: process.env.RPC_URL_GRAVITY || 'https://rpc.gravity.xyz', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const gravityContract: OmniPointHardhat = { eid: EndpointId.GRAVITY_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> gravity // gravity <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. gravity) gravityContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → gravity, confirmations for gravity → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → gravity, options for gravity → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: gravityContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose gravity ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Gravity Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=gravity&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=gravity) and [Executor](../deployed-contracts.md?chains=gravity) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Gunz Mainnet OFT Quickstart sidebar_label: Gunz Mainnet OFT Quickstart description: How to get started building on Gunz Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Gunz Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Gunz Mainnet (EID=30371) 'gunz-mainnet': { eid: EndpointId.GUNZ_V2_MAINNET, url: process.env.RPC_URL_GUNZ || 'https://rpc.gunzchain.io/ext/bc/2M47TxWHGnhNtq6pM5zPXdATBtuqubxn5EPFgFmEawCQr9WFML/rpc', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const gunzContract: OmniPointHardhat = { eid: EndpointId.GUNZ_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> gunz // gunz <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. gunz) gunzContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → gunz, confirmations for gunz → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → gunz, options for gunz → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: gunzContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose gunz ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Gunz Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=gunz&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=gunz) and [Executor](../deployed-contracts.md?chains=gunz) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Harmony Mainnet OFT Quickstart sidebar_label: Harmony Mainnet OFT Quickstart description: How to get started building on Harmony Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Harmony Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Harmony Mainnet (EID=30116) 'harmony-mainnet': { eid: EndpointId.HARMONY_V2_MAINNET, url: process.env.RPC_URL_HARMONY || 'https://api.harmony.one', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const harmonyContract: OmniPointHardhat = { eid: EndpointId.HARMONY_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> harmony // harmony <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. harmony) harmonyContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → harmony, confirmations for harmony → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → harmony, options for harmony → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: harmonyContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose harmony ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Harmony Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=harmony&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=harmony) and [Executor](../deployed-contracts.md?chains=harmony) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Hedera Mainnet OFT Quickstart sidebar_label: Hedera Mainnet OFT Quickstart description: How to get started building on Hedera Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Hedera Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Hedera Mainnet (EID=30316) 'hedera-mainnet': { eid: EndpointId.HEDERA_V2_MAINNET, url: process.env.RPC_URL_HEDERA || 'https://mainnet.hashio.io/api', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const hederaContract: OmniPointHardhat = { eid: EndpointId.HEDERA_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> hedera // hedera <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. hedera) hederaContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → hedera, confirmations for hedera → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → hedera, options for hedera → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: hederaContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose hedera ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Hedera Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=hedera&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=hedera) and [Executor](../deployed-contracts.md?chains=hedera) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Hemi Mainnet OFT Quickstart sidebar_label: Hemi Mainnet OFT Quickstart description: How to get started building on Hemi Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Hemi Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Hemi Mainnet (EID=30329) 'hemi-mainnet': { eid: EndpointId.HEMI_V2_MAINNET, url: process.env.RPC_URL_HEMI || 'https://rpc.hemi.network/rpc', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const hemiContract: OmniPointHardhat = { eid: EndpointId.HEMI_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> hemi // hemi <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. hemi) hemiContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → hemi, confirmations for hemi → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → hemi, options for hemi → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: hemiContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose hemi ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Hemi Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=hemi&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=hemi) and [Executor](../deployed-contracts.md?chains=hemi) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Homeverse Mainnet OFT Quickstart sidebar_label: Homeverse Mainnet OFT Quickstart description: How to get started building on Homeverse Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Homeverse Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Homeverse Mainnet (EID=30265) 'homeverse-mainnet': { eid: EndpointId.HOMEVERSE_V2_MAINNET, url: process.env.RPC_URL_HOMEVERSE || 'https://rpc.mainnet.oasys.homeverse.games', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const homeverseContract: OmniPointHardhat = { eid: EndpointId.HOMEVERSE_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> homeverse // homeverse <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. homeverse) homeverseContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → homeverse, confirmations for homeverse → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → homeverse, options for homeverse → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: homeverseContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose homeverse ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Homeverse Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=homeverse&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=homeverse) and [Executor](../deployed-contracts.md?chains=homeverse) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Horizen EON Mainnet OFT Quickstart sidebar_label: Horizen EON Mainnet OFT Quickstart description: How to get started building on Horizen EON Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Horizen EON Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Horizen EON Mainnet (EID=30215) 'eon-mainnet': { eid: EndpointId.EON_V2_MAINNET, url: process.env.RPC_URL_EON || 'https://eon-rpc.horizenlabs.io/ethv1', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const eonContract: OmniPointHardhat = { eid: EndpointId.EON_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> eon // eon <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. eon) eonContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → eon, confirmations for eon → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → eon, options for eon → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: eonContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose eon ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Horizen EON Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=eon&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=eon) and [Executor](../deployed-contracts.md?chains=eon) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Hubble Mainnet OFT Quickstart sidebar_label: Hubble Mainnet OFT Quickstart description: How to get started building on Hubble Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Hubble Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Hubble Mainnet (EID=30182) 'hubble-mainnet': { eid: EndpointId.HUBBLE_V2_MAINNET, url: process.env.RPC_URL_HUBBLE || 'https://sanko-arb-sepolia.rpc.caldera.xyz/http', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const hubbleContract: OmniPointHardhat = { eid: EndpointId.HUBBLE_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> hubble // hubble <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. hubble) hubbleContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → hubble, confirmations for hubble → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → hubble, options for hubble → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: hubbleContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose hubble ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Hubble Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=hubble&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=hubble) and [Executor](../deployed-contracts.md?chains=hubble) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: HyperEVM Mainnet OFT Quickstart sidebar_label: HyperEVM Mainnet OFT Quickstart description: How to get started building on HyperEVM Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **HyperEVM Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // HyperEVM Mainnet (EID=30367) 'hyperliquid-mainnet': { eid: EndpointId.HYPERLIQUID_V2_MAINNET, url: process.env.RPC_URL_HYPERLIQUID || 'https://gwan-ssl.wandevs.org:46891/', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const hyperliquidContract: OmniPointHardhat = { eid: EndpointId.HYPERLIQUID_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> hyperliquid // hyperliquid <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. hyperliquid) hyperliquidContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → hyperliquid, confirmations for hyperliquid → Optimism] [20, 1], // 5) Enforced execution options: // [options for Optimism → hyperliquid, options for hyperliquid → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: hyperliquidContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose hyperliquid ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **HyperEVM Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=hyperliquid&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=hyperliquid) and [Executor](../deployed-contracts.md?chains=hyperliquid) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: inEVM Mainnet OFT Quickstart sidebar_label: inEVM Mainnet OFT Quickstart description: How to get started building on inEVM Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **inEVM Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // inEVM Mainnet (EID=30234) 'bb1-mainnet': { eid: EndpointId.BB1_V2_MAINNET, url: process.env.RPC_URL_BB1 || 'https://mainnet.rpc.inevm.com/http', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const bb1Contract: OmniPointHardhat = { eid: EndpointId.BB1_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> bb1 // bb1 <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. bb1) bb1Contract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → bb1, confirmations for bb1 → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → bb1, options for bb1 → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: bb1Contract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose bb1 ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **inEVM Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=bb1&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=bb1) and [Executor](../deployed-contracts.md?chains=bb1) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Ink Mainnet OFT Quickstart sidebar_label: Ink Mainnet OFT Quickstart description: How to get started building on Ink Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Ink Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Ink Mainnet (EID=30339) 'ink-mainnet': { eid: EndpointId.INK_V2_MAINNET, url: process.env.RPC_URL_INK || 'https://rpc-gel.inkonchain.com', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const inkContract: OmniPointHardhat = { eid: EndpointId.INK_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> ink // ink <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. ink) inkContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → ink, confirmations for ink → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → ink, options for ink → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: inkContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose ink ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Ink Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=ink&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=ink) and [Executor](../deployed-contracts.md?chains=ink) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Iota Mainnet OFT Quickstart sidebar_label: Iota Mainnet OFT Quickstart description: How to get started building on Iota Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Iota Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Iota Mainnet (EID=30284) 'iota-mainnet': { eid: EndpointId.IOTA_V2_MAINNET, url: process.env.RPC_URL_IOTA || 'https://json-rpc.evm.iotaledger.net', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const iotaContract: OmniPointHardhat = { eid: EndpointId.IOTA_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> iota // iota <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. iota) iotaContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → iota, confirmations for iota → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → iota, options for iota → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: iotaContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose iota ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Iota Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=iota&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=iota) and [Executor](../deployed-contracts.md?chains=iota) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Japan Open Chain Mainnet OFT Quickstart sidebar_label: Japan Open Chain Mainnet OFT Quickstart description: How to get started building on Japan Open Chain Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Japan Open Chain Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Japan Open Chain Mainnet (EID=30285) 'joc-mainnet': { eid: EndpointId.JOC_V2_MAINNET, url: process.env.RPC_URL_JOC || 'https://rpc-3.japanopenchain.org', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const jocContract: OmniPointHardhat = { eid: EndpointId.JOC_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> joc // joc <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. joc) jocContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → joc, confirmations for joc → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → joc, options for joc → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: jocContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose joc ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Japan Open Chain Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=joc&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=joc) and [Executor](../deployed-contracts.md?chains=joc) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Kaia Mainnet (formerly Klaytn) OFT Quickstart sidebar_label: Kaia Mainnet (formerly Klaytn) OFT Quickstart description: How to get started building on Kaia Mainnet (formerly Klaytn) and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Kaia Mainnet (formerly Klaytn)** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Kaia Mainnet (formerly Klaytn) (EID=30150) 'klaytn-mainnet': { eid: EndpointId.KLAYTN_V2_MAINNET, url: process.env.RPC_URL_KLAYTN || 'https://public-en.node.kaia.io', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const klaytnContract: OmniPointHardhat = { eid: EndpointId.KLAYTN_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> klaytn // klaytn <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. klaytn) klaytnContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → klaytn, confirmations for klaytn → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → klaytn, options for klaytn → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: klaytnContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose klaytn ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Kaia Mainnet (formerly Klaytn)** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=klaytn&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=klaytn) and [Executor](../deployed-contracts.md?chains=klaytn) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Kava Mainnet OFT Quickstart sidebar_label: Kava Mainnet OFT Quickstart description: How to get started building on Kava Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Kava Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Kava Mainnet (EID=30177) 'kava-mainnet': { eid: EndpointId.KAVA_V2_MAINNET, url: process.env.RPC_URL_KAVA || 'https://evm.kava.io', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const kavaContract: OmniPointHardhat = { eid: EndpointId.KAVA_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> kava // kava <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. kava) kavaContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → kava, confirmations for kava → Optimism] [20, 2], // 5) Enforced execution options: // [options for Optimism → kava, options for kava → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: kavaContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose kava ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Kava Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=kava&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=kava) and [Executor](../deployed-contracts.md?chains=kava) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Lens Mainnet OFT Quickstart sidebar_label: Lens Mainnet OFT Quickstart description: How to get started building on Lens Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Lens Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash LZ_ENABLE_ZKSOLC_EXAMPLE=1 npx create-lz-oapp@latest # select onft721-zksync, even if you are not using onft ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // your zkSync chain 'lens-mainnet': { eid: EndpointId.LENS_V2_MAINNET, url: process.env.RPC_URL_LENS || 'https://lens.drpc.org', accounts, zksync: true, // crucial for zkSync networks ethNetwork: 'mainnet', verifyURL: 'https://explorer.era.zksync.io/contract_verification', }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const lensContract: OmniPointHardhat = { eid: EndpointId.LENS_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> lens // lens <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. lens) lensContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → lens, confirmations for lens → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → lens, options for lens → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: lensContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose lens ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Lens Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=lens&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=lens) and [Executor](../deployed-contracts.md?chains=lens) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Lightlink Mainnet OFT Quickstart sidebar_label: Lightlink Mainnet OFT Quickstart description: How to get started building on Lightlink Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Lightlink Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Lightlink Mainnet (EID=30309) 'lightlink-mainnet': { eid: EndpointId.LIGHTLINK_V2_MAINNET, url: process.env.RPC_URL_LIGHTLINK || 'https://replicator.phoenix.lightlink.io/rpc/v1', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const lightlinkContract: OmniPointHardhat = { eid: EndpointId.LIGHTLINK_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> lightlink // lightlink <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. lightlink) lightlinkContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → lightlink, confirmations for lightlink → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → lightlink, options for lightlink → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: lightlinkContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose lightlink ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Lightlink Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=lightlink&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=lightlink) and [Executor](../deployed-contracts.md?chains=lightlink) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Linea Mainnet OFT Quickstart sidebar_label: Linea Mainnet OFT Quickstart description: How to get started building on Linea Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Linea Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Linea Mainnet (EID=30183) 'linea-mainnet': { eid: EndpointId.ZKCONSENSYS_V2_MAINNET, url: process.env.RPC_URL_LINEA || 'https://rpc.linea.build', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const lineaContract: OmniPointHardhat = { eid: EndpointId.ZKCONSENSYS_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> linea // linea <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. linea) lineaContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → linea, confirmations for linea → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → linea, options for linea → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: lineaContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose linea ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Linea Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=linea&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=linea) and [Executor](../deployed-contracts.md?chains=linea) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Lisk Mainnet OFT Quickstart sidebar_label: Lisk Mainnet OFT Quickstart description: How to get started building on Lisk Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Lisk Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Lisk Mainnet (EID=30321) 'lisk-mainnet': { eid: EndpointId.LISK_V2_MAINNET, url: process.env.RPC_URL_LISK || 'https://rpc.api.lisk.com', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const liskContract: OmniPointHardhat = { eid: EndpointId.LISK_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> lisk // lisk <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. lisk) liskContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → lisk, confirmations for lisk → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → lisk, options for lisk → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: liskContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose lisk ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Lisk Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=lisk&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=lisk) and [Executor](../deployed-contracts.md?chains=lisk) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Loot Mainnet OFT Quickstart sidebar_label: Loot Mainnet OFT Quickstart description: How to get started building on Loot Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Loot Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Loot Mainnet (EID=30197) 'loot-mainnet': { eid: EndpointId.LOOT_V2_MAINNET, url: process.env.RPC_URL_LOOT || 'https://rpc.lootchain.com/http', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const lootContract: OmniPointHardhat = { eid: EndpointId.LOOT_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> loot // loot <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. loot) lootContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → loot, confirmations for loot → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → loot, options for loot → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: lootContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose loot ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Loot Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=loot&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=loot) and [Executor](../deployed-contracts.md?chains=loot) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Lyra Mainnet OFT Quickstart sidebar_label: Lyra Mainnet OFT Quickstart description: How to get started building on Lyra Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Lyra Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Lyra Mainnet (EID=30311) 'lyra-mainnet': { eid: EndpointId.LYRA_V2_MAINNET, url: process.env.RPC_URL_LYRA || 'https://rpc.lyra.finance', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const lyraContract: OmniPointHardhat = { eid: EndpointId.LYRA_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> lyra // lyra <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. lyra) lyraContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → lyra, confirmations for lyra → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → lyra, options for lyra → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: lyraContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose lyra ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Lyra Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=lyra&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=lyra) and [Executor](../deployed-contracts.md?chains=lyra) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Manta Pacific Mainnet OFT Quickstart sidebar_label: Manta Pacific Mainnet OFT Quickstart description: How to get started building on Manta Pacific Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Manta Pacific Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Manta Pacific Mainnet (EID=30217) 'manta-mainnet': { eid: EndpointId.MANTA_V2_MAINNET, url: process.env.RPC_URL_MANTA || 'https://pacific-rpc.manta.network/http', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const mantaContract: OmniPointHardhat = { eid: EndpointId.MANTA_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> manta // manta <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. manta) mantaContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → manta, confirmations for manta → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → manta, options for manta → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: mantaContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose manta ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Manta Pacific Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=manta&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=manta) and [Executor](../deployed-contracts.md?chains=manta) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Mantle Mainnet OFT Quickstart sidebar_label: Mantle Mainnet OFT Quickstart description: How to get started building on Mantle Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Mantle Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Mantle Mainnet (EID=30181) 'mantle-mainnet': { eid: EndpointId.MANTLE_V2_MAINNET, url: process.env.RPC_URL_MANTLE || 'https://rpc.mantle.xyz', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const mantleContract: OmniPointHardhat = { eid: EndpointId.MANTLE_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> mantle // mantle <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. mantle) mantleContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → mantle, confirmations for mantle → Optimism] [20, 2], // 5) Enforced execution options: // [options for Optimism → mantle, options for mantle → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: mantleContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose mantle ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Mantle Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=mantle&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=mantle) and [Executor](../deployed-contracts.md?chains=mantle) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Merlin Mainnet OFT Quickstart sidebar_label: Merlin Mainnet OFT Quickstart description: How to get started building on Merlin Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Merlin Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Merlin Mainnet (EID=30266) 'merlin-mainnet': { eid: EndpointId.MERLIN_V2_MAINNET, url: process.env.RPC_URL_MERLIN || 'https://rpc.merlinchain.io', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const merlinContract: OmniPointHardhat = { eid: EndpointId.MERLIN_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> merlin // merlin <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. merlin) merlinContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → merlin, confirmations for merlin → Optimism] [20, 1000000], // 5) Enforced execution options: // [options for Optimism → merlin, options for merlin → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: merlinContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose merlin ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Merlin Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=merlin&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=merlin) and [Executor](../deployed-contracts.md?chains=merlin) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Meter Mainnet OFT Quickstart sidebar_label: Meter Mainnet OFT Quickstart description: How to get started building on Meter Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Meter Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Meter Mainnet (EID=30176) 'meter-mainnet': { eid: EndpointId.METER_V2_MAINNET, url: process.env.RPC_URL_METER || 'https://rpc.meter.io', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const meterContract: OmniPointHardhat = { eid: EndpointId.METER_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> meter // meter <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. meter) meterContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → meter, confirmations for meter → Optimism] [20, 2], // 5) Enforced execution options: // [options for Optimism → meter, options for meter → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: meterContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose meter ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Meter Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=meter&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=meter) and [Executor](../deployed-contracts.md?chains=meter) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Metis Mainnet OFT Quickstart sidebar_label: Metis Mainnet OFT Quickstart description: How to get started building on Metis Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Metis Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Metis Mainnet (EID=30151) 'metis-mainnet': { eid: EndpointId.METIS_V2_MAINNET, url: process.env.RPC_URL_METIS || 'https://andromeda.metis.io/?owner=1088', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const metisContract: OmniPointHardhat = { eid: EndpointId.METIS_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> metis // metis <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. metis) metisContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → metis, confirmations for metis → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → metis, options for metis → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: metisContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose metis ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Metis Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=metis&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=metis) and [Executor](../deployed-contracts.md?chains=metis) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Mode Mainnet OFT Quickstart sidebar_label: Mode Mainnet OFT Quickstart description: How to get started building on Mode Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Mode Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Mode Mainnet (EID=30260) 'mode-mainnet': { eid: EndpointId.MODE_V2_MAINNET, url: process.env.RPC_URL_MODE || 'https://1rpc.io/mode', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const modeContract: OmniPointHardhat = { eid: EndpointId.MODE_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> mode // mode <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. mode) modeContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → mode, confirmations for mode → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → mode, options for mode → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: modeContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose mode ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Mode Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=mode&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=mode) and [Executor](../deployed-contracts.md?chains=mode) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Moonbeam Mainnet OFT Quickstart sidebar_label: Moonbeam Mainnet OFT Quickstart description: How to get started building on Moonbeam Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Moonbeam Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Moonbeam Mainnet (EID=30126) 'moonbeam-mainnet': { eid: EndpointId.MOONBEAM_V2_MAINNET, url: process.env.RPC_URL_MOONBEAM || 'https://rpc.api.moonbeam.network', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const moonbeamContract: OmniPointHardhat = { eid: EndpointId.MOONBEAM_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> moonbeam // moonbeam <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. moonbeam) moonbeamContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → moonbeam, confirmations for moonbeam → Optimism] [20, 10], // 5) Enforced execution options: // [options for Optimism → moonbeam, options for moonbeam → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: moonbeamContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose moonbeam ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Moonbeam Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=moonbeam&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=moonbeam) and [Executor](../deployed-contracts.md?chains=moonbeam) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Moonriver Mainnet OFT Quickstart sidebar_label: Moonriver Mainnet OFT Quickstart description: How to get started building on Moonriver Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Moonriver Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Moonriver Mainnet (EID=30167) 'moonriver-mainnet': { eid: EndpointId.MOONRIVER_V2_MAINNET, url: process.env.RPC_URL_MOONRIVER || 'https://rpc.api.moonriver.moonbeam.network', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const moonriverContract: OmniPointHardhat = { eid: EndpointId.MOONRIVER_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> moonriver // moonriver <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. moonriver) moonriverContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → moonriver, confirmations for moonriver → Optimism] [20, 10], // 5) Enforced execution options: // [options for Optimism → moonriver, options for moonriver → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: moonriverContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose moonriver ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Moonriver Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=moonriver&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=moonriver) and [Executor](../deployed-contracts.md?chains=moonriver) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Morph Mainnet OFT Quickstart sidebar_label: Morph Mainnet OFT Quickstart description: How to get started building on Morph Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Morph Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Morph Mainnet (EID=30322) 'morph-mainnet': { eid: EndpointId.MORPH_V2_MAINNET, url: process.env.RPC_URL_MORPH || 'https://rpc.morphl2.io', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const morphContract: OmniPointHardhat = { eid: EndpointId.MORPH_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> morph // morph <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. morph) morphContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → morph, confirmations for morph → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → morph, options for morph → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: morphContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose morph ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Morph Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=morph&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=morph) and [Executor](../deployed-contracts.md?chains=morph) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Near Aurora Mainnet OFT Quickstart sidebar_label: Near Aurora Mainnet OFT Quickstart description: How to get started building on Near Aurora Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Near Aurora Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Near Aurora Mainnet (EID=30211) 'aurora-mainnet': { eid: EndpointId.AURORA_V2_MAINNET, url: process.env.RPC_URL_AURORA || 'https://mainnet.aurora.dev', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const auroraContract: OmniPointHardhat = { eid: EndpointId.AURORA_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> aurora // aurora <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. aurora) auroraContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → aurora, confirmations for aurora → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → aurora, options for aurora → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: auroraContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose aurora ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Near Aurora Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=aurora&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=aurora) and [Executor](../deployed-contracts.md?chains=aurora) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Nibiru Mainnet OFT Quickstart sidebar_label: Nibiru Mainnet OFT Quickstart description: How to get started building on Nibiru Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Nibiru Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Nibiru Mainnet (EID=30369) 'nibiru-mainnet': { eid: EndpointId.NIBIRU_V2_MAINNET, url: process.env.RPC_URL_NIBIRU || 'https://evm-rpc.nibiru.fi', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const nibiruContract: OmniPointHardhat = { eid: EndpointId.NIBIRU_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> nibiru // nibiru <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. nibiru) nibiruContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → nibiru, confirmations for nibiru → Optimism] [20, 1], // 5) Enforced execution options: // [options for Optimism → nibiru, options for nibiru → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: nibiruContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose nibiru ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Nibiru Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=nibiru&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=nibiru) and [Executor](../deployed-contracts.md?chains=nibiru) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: OKX Mainnet OFT Quickstart sidebar_label: OKX Mainnet OFT Quickstart description: How to get started building on OKX Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **OKX Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // OKX Mainnet (EID=30155) 'okx-mainnet': { eid: EndpointId.OKX_V2_MAINNET, url: process.env.RPC_URL_OKX || 'https://exchainrpc.okex.org', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const okxContract: OmniPointHardhat = { eid: EndpointId.OKX_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> okx // okx <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. okx) okxContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → okx, confirmations for okx → Optimism] [20, 2], // 5) Enforced execution options: // [options for Optimism → okx, options for okx → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: okxContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose okx ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **OKX Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=okx&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=okx) and [Executor](../deployed-contracts.md?chains=okx) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: opBNB Mainnet OFT Quickstart sidebar_label: opBNB Mainnet OFT Quickstart description: How to get started building on opBNB Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **opBNB Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // opBNB Mainnet (EID=30202) 'opbnb-mainnet': { eid: EndpointId.OPBNB_V2_MAINNET, url: process.env.RPC_URL_OPBNB || 'https://opbnb-mainnet-rpc.bnbchain.org', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const opbnbContract: OmniPointHardhat = { eid: EndpointId.OPBNB_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> opbnb // opbnb <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. opbnb) opbnbContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → opbnb, confirmations for opbnb → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → opbnb, options for opbnb → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: opbnbContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose opbnb ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **opBNB Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=opbnb&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=opbnb) and [Executor](../deployed-contracts.md?chains=opbnb) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Optimism Mainnet OFT Quickstart sidebar_label: Optimism Mainnet OFT Quickstart description: How to get started building on Optimism Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Optimism Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Optimism Mainnet (EID=30111) 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> optimism // optimism <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. optimism) optimismContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → optimism, confirmations for optimism → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → optimism, options for optimism → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: optimismContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose optimism ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Optimism Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=optimism&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=optimism) and [Executor](../deployed-contracts.md?chains=optimism) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Optimism Sepolia Testnet OFT Quickstart sidebar_label: Optimism Sepolia Testnet OFT Quickstart description: How to get started building on Optimism Sepolia Testnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Optimism Sepolia Testnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Optimism Sepolia Testnet (EID=40232) 'optimism-sepolia-testnet': { eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OPTIMISM_SEPOLIA || 'https://sepolia.optimism.io', accounts, }, // another network you want to connect to 'optimism-testnet': { eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import { EndpointId } from '@layerzerolabs/lz-definitions' import type { OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat' import { OAppEnforcedOption } from '@layerzerolabs/toolbox-hardhat' import { ExecutorOptionType } from '@layerzerolabs/lz-v2-utilities' import { TwoWayConfig, generateConnectionsConfig } from '@layerzerolabs/metadata-tools' const optimism-sepoliaContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', } const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', } // To connect all the above chains to each other, we need the following pathways: // Optimism <-> optimism-sepolia // optimism-sepolia <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ] const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. optimism-sepolia) optimism-sepoliaContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → optimism-sepolia, confirmations for optimism-sepolia → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → optimism-sepolia, options for optimism-sepolia → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ] export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways) return { contracts: [{ contract: optimismContract }, { contract: optimism-sepoliaContract }], connections, } } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose optimism-sepolia ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Optimism Sepolia Testnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://testnet.layerzeroscan.com/tools/defaults?srcChainKey[0]=optimism-sepolia&version=V2&dstChainKey[0]=optimism-sepolia) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=optimism-sepolia) and [Executor](../deployed-contracts.md?chains=optimism-sepolia) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Orderly Mainnet OFT Quickstart sidebar_label: Orderly Mainnet OFT Quickstart description: How to get started building on Orderly Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Orderly Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Orderly Mainnet (EID=30213) 'orderly-mainnet': { eid: EndpointId.ORDERLY_V2_MAINNET, url: process.env.RPC_URL_ORDERLY || 'https://rpc.orderly.network', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const orderlyContract: OmniPointHardhat = { eid: EndpointId.ORDERLY_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> orderly // orderly <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. orderly) orderlyContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → orderly, confirmations for orderly → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → orderly, options for orderly → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: orderlyContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose orderly ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Orderly Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=orderly&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=orderly) and [Executor](../deployed-contracts.md?chains=orderly) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Otherworld Space Mainnet OFT Quickstart sidebar_label: Otherworld Space Mainnet OFT Quickstart description: How to get started building on Otherworld Space Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Otherworld Space Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash LZ_ENABLE_ALT_EXAMPLE=1 npx create-lz-oapp@latest # select OFTAlt example ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Otherworld Space Mainnet (EID=30341) 'space-mainnet': { eid: EndpointId.SPACE_V2_MAINNET, url: process.env.RPC_URL_SPACE || 'https://subnets.avax.network/space/mainnet/rpc', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const spaceContract: OmniPointHardhat = { eid: EndpointId.SPACE_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> space // space <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. space) spaceContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → space, confirmations for space → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → space, options for space → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: spaceContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFTAlt.sol" // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { OFTAlt } from "@layerzerolabs/oft-alt-evm/contracts/OFTAlt.sol"; contract MyOFTAlt is OFTAlt { constructor( string memory _name, string memory _symbol, address _lzEndpoint, address _delegate ) OFTAlt(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose space ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Otherworld Space Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=space&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=space) and [Executor](../deployed-contracts.md?chains=space) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Peaq Mainnet OFT Quickstart sidebar_label: Peaq Mainnet OFT Quickstart description: How to get started building on Peaq Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Peaq Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Peaq Mainnet (EID=30302) 'peaq-mainnet': { eid: EndpointId.PEAQ_V2_MAINNET, url: process.env.RPC_URL_PEAQ || 'https://peaq.api.onfinality.io/public', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const peaqContract: OmniPointHardhat = { eid: EndpointId.PEAQ_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> peaq // peaq <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. peaq) peaqContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → peaq, confirmations for peaq → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → peaq, options for peaq → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: peaqContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose peaq ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Peaq Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=peaq&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=peaq) and [Executor](../deployed-contracts.md?chains=peaq) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Plume Mainnet OFT Quickstart sidebar_label: Plume Mainnet OFT Quickstart description: How to get started building on Plume Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Plume Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Plume Mainnet (EID=30370) 'plumephoenix-mainnet': { eid: EndpointId.PLUMEPHOENIX_V2_MAINNET, url: process.env.RPC_URL_PLUMEPHOENIX || 'https://phoenix-rpc.plumenetwork.xyz', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const plumephoenixContract: OmniPointHardhat = { eid: EndpointId.PLUMEPHOENIX_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> plumephoenix // plumephoenix <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. plumephoenix) plumephoenixContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → plumephoenix, confirmations for plumephoenix → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → plumephoenix, options for plumephoenix → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: plumephoenixContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose plumephoenix ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Plume Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=plumephoenix&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=plumephoenix) and [Executor](../deployed-contracts.md?chains=plumephoenix) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Polygon Amoy Testnet OFT Quickstart sidebar_label: Polygon Amoy Testnet OFT Quickstart description: How to get started building on Polygon Amoy Testnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Polygon Amoy Testnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Polygon Amoy Testnet (EID=40267) 'amoy-testnet': { eid: EndpointId.AMOY_V2_TESTNET, url: process.env.RPC_URL_AMOY || 'https://rpc-amoy.polygon.technology', accounts, }, // another network you want to connect to 'optimism-testnet': { eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const amoyContract: OmniPointHardhat = { eid: EndpointId.AMOY_V2_TESTNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> amoy // amoy <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. amoy) amoyContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → amoy, confirmations for amoy → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → amoy, options for amoy → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: amoyContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose amoy-testnet ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Polygon Amoy Testnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://testnet.layerzeroscan.com/tools/defaults?srcChainKey[0]=amoy-testnet&version=V2&dstChainKey[0]=optimism-sepolia) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=amoy-testnet) and [Executor](../deployed-contracts.md?chains=amoy-testnet) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Polygon Mainnet OFT Quickstart sidebar_label: Polygon Mainnet OFT Quickstart description: How to get started building on Polygon Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Polygon Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Polygon Mainnet (EID=30109) 'polygon-mainnet': { eid: EndpointId.POLYGON_V2_MAINNET, url: process.env.RPC_URL_POLYGON || 'https://polygon.drpc.org', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const polygonContract: OmniPointHardhat = { eid: EndpointId.POLYGON_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> polygon // polygon <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. polygon) polygonContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → polygon, confirmations for polygon → Optimism] [20, 512], // 5) Enforced execution options: // [options for Optimism → polygon, options for polygon → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: polygonContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose polygon ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Polygon Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=polygon&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=polygon) and [Executor](../deployed-contracts.md?chains=polygon) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Polygon zkEVM Mainnet OFT Quickstart sidebar_label: Polygon zkEVM Mainnet OFT Quickstart description: How to get started building on Polygon zkEVM Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Polygon zkEVM Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Polygon zkEVM Mainnet (EID=30158) 'zkevm-mainnet': { eid: EndpointId.ZKPOLYGON_V2_MAINNET, url: process.env.RPC_URL_ZKEVM || 'https://rpc.ankr.com/polygon_zkevm', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const zkevmContract: OmniPointHardhat = { eid: EndpointId.ZKPOLYGON_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> zkevm // zkevm <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. zkevm) zkevmContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → zkevm, confirmations for zkevm → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → zkevm, options for zkevm → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: zkevmContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose zkevm ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Polygon zkEVM Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=zkevm&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=zkevm) and [Executor](../deployed-contracts.md?chains=zkevm) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Rari Chain Mainnet OFT Quickstart sidebar_label: Rari Chain Mainnet OFT Quickstart description: How to get started building on Rari Chain Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Rari Chain Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Rari Chain Mainnet (EID=30235) 'rarible-mainnet': { eid: EndpointId.RARIBLE_V2_MAINNET, url: process.env.RPC_URL_RARIBLE || 'https://mainnet.rpc.rarichain.org/http', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const raribleContract: OmniPointHardhat = { eid: EndpointId.RARIBLE_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> rarible // rarible <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. rarible) raribleContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → rarible, confirmations for rarible → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → rarible, options for rarible → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: raribleContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose rarible ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Rari Chain Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=rarible&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=rarible) and [Executor](../deployed-contracts.md?chains=rarible) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: re.al Mainnet OFT Quickstart sidebar_label: re.al Mainnet OFT Quickstart description: How to get started building on re.al Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **re.al Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // re.al Mainnet (EID=30237) 'real-mainnet': { eid: EndpointId.REAL_V2_MAINNET, url: process.env.RPC_URL_REAL || 'https://rpc.realforreal.gelato.digital', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const realContract: OmniPointHardhat = { eid: EndpointId.REAL_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> real // real <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. real) realContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → real, confirmations for real → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → real, options for real → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: realContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose real ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **re.al Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=real&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=real) and [Executor](../deployed-contracts.md?chains=real) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Reya Mainnet OFT Quickstart sidebar_label: Reya Mainnet OFT Quickstart description: How to get started building on Reya Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Reya Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Reya Mainnet (EID=30313) 'reya-mainnet': { eid: EndpointId.REYA_V2_MAINNET, url: process.env.RPC_URL_REYA || 'https://rpc.reya.network', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const reyaContract: OmniPointHardhat = { eid: EndpointId.REYA_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> reya // reya <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. reya) reyaContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → reya, confirmations for reya → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → reya, options for reya → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: reyaContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose reya ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Reya Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=reya&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=reya) and [Executor](../deployed-contracts.md?chains=reya) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Rootstock Mainnet OFT Quickstart sidebar_label: Rootstock Mainnet OFT Quickstart description: How to get started building on Rootstock Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Rootstock Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Rootstock Mainnet (EID=30333) 'rootstock-mainnet': { eid: EndpointId.ROOTSTOCK_V2_MAINNET, url: process.env.RPC_URL_ROOTSTOCK || 'https://mycrypto.rsk.co', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const rootstockContract: OmniPointHardhat = { eid: EndpointId.ROOTSTOCK_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> rootstock // rootstock <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. rootstock) rootstockContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → rootstock, confirmations for rootstock → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → rootstock, options for rootstock → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: rootstockContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose rootstock ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Rootstock Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=rootstock&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=rootstock) and [Executor](../deployed-contracts.md?chains=rootstock) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Sanko Mainnet OFT Quickstart sidebar_label: Sanko Mainnet OFT Quickstart description: How to get started building on Sanko Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Sanko Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Sanko Mainnet (EID=30278) 'sanko-mainnet': { eid: EndpointId.SANKO_V2_MAINNET, url: process.env.RPC_URL_SANKO || 'https://mainnet.sanko.xyz', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const sankoContract: OmniPointHardhat = { eid: EndpointId.SANKO_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> sanko // sanko <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. sanko) sankoContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → sanko, confirmations for sanko → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → sanko, options for sanko → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: sankoContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose sanko ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Sanko Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=sanko&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=sanko) and [Executor](../deployed-contracts.md?chains=sanko) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Scroll Mainnet OFT Quickstart sidebar_label: Scroll Mainnet OFT Quickstart description: How to get started building on Scroll Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Scroll Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Scroll Mainnet (EID=30214) 'scroll-mainnet': { eid: EndpointId.SCROLL_V2_MAINNET, url: process.env.RPC_URL_SCROLL || 'https://rpc.scroll.io', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const scrollContract: OmniPointHardhat = { eid: EndpointId.SCROLL_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> scroll // scroll <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. scroll) scrollContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → scroll, confirmations for scroll → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → scroll, options for scroll → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: scrollContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose scroll ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Scroll Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=scroll&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=scroll) and [Executor](../deployed-contracts.md?chains=scroll) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Sei Mainnet OFT Quickstart sidebar_label: Sei Mainnet OFT Quickstart description: How to get started building on Sei Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Sei Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Sei Mainnet (EID=30280) 'sei-mainnet': { eid: EndpointId.SEI_V2_MAINNET, url: process.env.RPC_URL_SEI || 'https://evm-rpc.sei-apis.com', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const seiContract: OmniPointHardhat = { eid: EndpointId.SEI_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> sei // sei <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. sei) seiContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → sei, confirmations for sei → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → sei, options for sei → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: seiContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose sei ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Sei Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=sei&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=sei) and [Executor](../deployed-contracts.md?chains=sei) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Shimmer Mainnet OFT Quickstart sidebar_label: Shimmer Mainnet OFT Quickstart description: How to get started building on Shimmer Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Shimmer Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Shimmer Mainnet (EID=30230) 'shimmer-mainnet': { eid: EndpointId.SHIMMER_V2_MAINNET, url: process.env.RPC_URL_SHIMMER || 'https://json-rpc.evm.shimmer.network', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const shimmerContract: OmniPointHardhat = { eid: EndpointId.SHIMMER_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> shimmer // shimmer <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. shimmer) shimmerContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → shimmer, confirmations for shimmer → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → shimmer, options for shimmer → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: shimmerContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose shimmer ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Shimmer Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=shimmer&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=shimmer) and [Executor](../deployed-contracts.md?chains=shimmer) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Skale Mainnet OFT Quickstart sidebar_label: Skale Mainnet OFT Quickstart description: How to get started building on Skale Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Skale Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash LZ_ENABLE_ALT_EXAMPLE=1 npx create-lz-oapp@latest # select OFTAlt example ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Skale Mainnet (EID=30273) 'skale-mainnet': { eid: EndpointId.SKALE_V2_MAINNET, url: process.env.RPC_URL_SKALE || 'https://mainnet.skalenodes.com/v1/elated-tan-skat', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const skaleContract: OmniPointHardhat = { eid: EndpointId.SKALE_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> skale // skale <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. skale) skaleContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → skale, confirmations for skale → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → skale, options for skale → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: skaleContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFTAlt.sol" // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { OFTAlt } from "@layerzerolabs/oft-alt-evm/contracts/OFTAlt.sol"; contract MyOFTAlt is OFTAlt { constructor( string memory _name, string memory _symbol, address _lzEndpoint, address _delegate ) OFTAlt(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose skale ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Skale Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=skale&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=skale) and [Executor](../deployed-contracts.md?chains=skale) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Soneium Mainnet OFT Quickstart sidebar_label: Soneium Mainnet OFT Quickstart description: How to get started building on Soneium Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Soneium Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Soneium Mainnet (EID=30340) 'soneium-mainnet': { eid: EndpointId.SONEIUM_V2_MAINNET, url: process.env.RPC_URL_SONEIUM || 'https://rpc.soneium.org', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const soneiumContract: OmniPointHardhat = { eid: EndpointId.SONEIUM_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> soneium // soneium <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. soneium) soneiumContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → soneium, confirmations for soneium → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → soneium, options for soneium → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: soneiumContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose soneium ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Soneium Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=soneium&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=soneium) and [Executor](../deployed-contracts.md?chains=soneium) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Sonic Mainnet OFT Quickstart sidebar_label: Sonic Mainnet OFT Quickstart description: How to get started building on Sonic Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Sonic Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Sonic Mainnet (EID=30332) 'sonic-mainnet': { eid: EndpointId.SONIC_V2_MAINNET, url: process.env.RPC_URL_SONIC || 'https://rpc.soniclabs.com', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const sonicContract: OmniPointHardhat = { eid: EndpointId.SONIC_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> sonic // sonic <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. sonic) sonicContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → sonic, confirmations for sonic → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → sonic, options for sonic → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: sonicContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose sonic ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Sonic Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=sonic&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=sonic) and [Executor](../deployed-contracts.md?chains=sonic) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Sophon Mainnet OFT Quickstart sidebar_label: Sophon Mainnet OFT Quickstart description: How to get started building on Sophon Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Sophon Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash LZ_ENABLE_ZKSOLC_EXAMPLE=1 npx create-lz-oapp@latest # select onft721-zksync, even if you are not using onft ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // your zkSync chain 'sophon-mainnet': { eid: EndpointId.SOPHON_V2_MAINNET, url: process.env.RPC_URL_SOPHON || 'https://rpc.sophon.xyz', accounts, zksync: true, // crucial for zkSync networks ethNetwork: 'mainnet', verifyURL: 'https://explorer.era.zksync.io/contract_verification', }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const sophonContract: OmniPointHardhat = { eid: EndpointId.SOPHON_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> sophon // sophon <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. sophon) sophonContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → sophon, confirmations for sophon → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → sophon, options for sophon → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: sophonContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose sophon ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Sophon Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=sophon&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=sophon) and [Executor](../deployed-contracts.md?chains=sophon) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Story Mainnet OFT Quickstart sidebar_label: Story Mainnet OFT Quickstart description: How to get started building on Story Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Story Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Story Mainnet (EID=30364) 'story-mainnet': { eid: EndpointId.STORY_V2_MAINNET, url: process.env.RPC_URL_STORY || 'https://story-evm-rpc.spidernode.net', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const storyContract: OmniPointHardhat = { eid: EndpointId.STORY_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> story // story <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. story) storyContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → story, confirmations for story → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → story, options for story → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: storyContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose story ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Story Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=story&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=story) and [Executor](../deployed-contracts.md?chains=story) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Subtensor EVM Mainnet OFT Quickstart sidebar_label: Subtensor EVM Mainnet OFT Quickstart description: How to get started building on Subtensor EVM Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Subtensor EVM Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Subtensor EVM Mainnet (EID=30374) 'subtensorevm-mainnet': { eid: EndpointId.SUBTENSOREVM_V2_MAINNET, url: process.env.RPC_URL_SUBTENSOREVM || 'https://INSERT-RPC', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const subtensorevmContract: OmniPointHardhat = { eid: EndpointId.SUBTENSOREVM_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> subtensorevm // subtensorevm <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. subtensorevm) subtensorevmContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → subtensorevm, confirmations for subtensorevm → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → subtensorevm, options for subtensorevm → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: subtensorevmContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose subtensorevm ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Subtensor EVM Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=subtensorevm&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=subtensorevm) and [Executor](../deployed-contracts.md?chains=subtensorevm) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Superposition Mainnet OFT Quickstart sidebar_label: Superposition Mainnet OFT Quickstart description: How to get started building on Superposition Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Superposition Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Superposition Mainnet (EID=30327) 'superposition-mainnet': { eid: EndpointId.SUPERPOSITION_V2_MAINNET, url: process.env.RPC_URL_SUPERPOSITION || 'https://rpc.superposition.so', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const superpositionContract: OmniPointHardhat = { eid: EndpointId.SUPERPOSITION_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> superposition // superposition <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. superposition) superpositionContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → superposition, confirmations for superposition → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → superposition, options for superposition → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: superpositionContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose superposition ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Superposition Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=superposition&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=superposition) and [Executor](../deployed-contracts.md?chains=superposition) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Swell Mainnet OFT Quickstart sidebar_label: Swell Mainnet OFT Quickstart description: How to get started building on Swell Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Swell Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Swell Mainnet (EID=30335) 'swell-mainnet': { eid: EndpointId.SWELL_V2_MAINNET, url: process.env.RPC_URL_SWELL || 'https://rpc.ankr.com/swell', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const swellContract: OmniPointHardhat = { eid: EndpointId.SWELL_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> swell // swell <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. swell) swellContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → swell, confirmations for swell → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → swell, options for swell → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: swellContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose swell ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Swell Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=swell&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=swell) and [Executor](../deployed-contracts.md?chains=swell) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Taiko Mainnet OFT Quickstart sidebar_label: Taiko Mainnet OFT Quickstart description: How to get started building on Taiko Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Taiko Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Taiko Mainnet (EID=30290) 'taiko-mainnet': { eid: EndpointId.TAIKO_V2_MAINNET, url: process.env.RPC_URL_TAIKO || 'https://rpc.taiko.xyz', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const taikoContract: OmniPointHardhat = { eid: EndpointId.TAIKO_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> taiko // taiko <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. taiko) taikoContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → taiko, confirmations for taiko → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → taiko, options for taiko → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: taikoContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose taiko ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Taiko Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=taiko&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=taiko) and [Executor](../deployed-contracts.md?chains=taiko) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: TelosEVM Mainnet OFT Quickstart sidebar_label: TelosEVM Mainnet OFT Quickstart description: How to get started building on TelosEVM Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **TelosEVM Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // TelosEVM Mainnet (EID=30199) 'telos-mainnet': { eid: EndpointId.TELOS_V2_MAINNET, url: process.env.RPC_URL_TELOS || 'https://rpc.telos.net', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const telosContract: OmniPointHardhat = { eid: EndpointId.TELOS_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> telos // telos <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. telos) telosContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → telos, confirmations for telos → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → telos, options for telos → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: telosContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose telos ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **TelosEVM Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=telos&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=telos) and [Executor](../deployed-contracts.md?chains=telos) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Tenet Mainnet OFT Quickstart sidebar_label: Tenet Mainnet OFT Quickstart description: How to get started building on Tenet Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Tenet Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Tenet Mainnet (EID=30173) 'tenet-mainnet': { eid: EndpointId.TENET_V2_MAINNET, url: process.env.RPC_URL_TENET || 'https://rpc.tenet.org', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const tenetContract: OmniPointHardhat = { eid: EndpointId.TENET_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> tenet // tenet <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. tenet) tenetContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → tenet, confirmations for tenet → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → tenet, options for tenet → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: tenetContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose tenet ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Tenet Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=tenet&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=tenet) and [Executor](../deployed-contracts.md?chains=tenet) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Tiltyard Mainnet OFT Quickstart sidebar_label: Tiltyard Mainnet OFT Quickstart description: How to get started building on Tiltyard Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Tiltyard Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Tiltyard Mainnet (EID=30238) 'tiltyard-mainnet': { eid: EndpointId.TILTYARD_V2_MAINNET, url: process.env.RPC_URL_TILTYARD || 'https://subnets.avax.network/tiltyard/mainnet/rpc', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const tiltyardContract: OmniPointHardhat = { eid: EndpointId.TILTYARD_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> tiltyard // tiltyard <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. tiltyard) tiltyardContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → tiltyard, confirmations for tiltyard → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → tiltyard, options for tiltyard → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: tiltyardContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose tiltyard ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Tiltyard Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=tiltyard&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=tiltyard) and [Executor](../deployed-contracts.md?chains=tiltyard) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Unichain Mainnet OFT Quickstart sidebar_label: Unichain Mainnet OFT Quickstart description: How to get started building on Unichain Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Unichain Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Unichain Mainnet (EID=30320) 'unichain-mainnet': { eid: EndpointId.UNICHAIN_V2_MAINNET, url: process.env.RPC_URL_UNICHAIN || 'https://unichain.api.onfinality.io/public', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const unichainContract: OmniPointHardhat = { eid: EndpointId.UNICHAIN_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> unichain // unichain <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. unichain) unichainContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → unichain, confirmations for unichain → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → unichain, options for unichain → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: unichainContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose unichain ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Unichain Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=unichain&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=unichain) and [Executor](../deployed-contracts.md?chains=unichain) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Vana Mainnet OFT Quickstart sidebar_label: Vana Mainnet OFT Quickstart description: How to get started building on Vana Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Vana Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Vana Mainnet (EID=30330) 'islander-mainnet': { eid: EndpointId.ISLANDER_V2_MAINNET, url: process.env.RPC_URL_ISLANDER || 'https://rpc.vana.org', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const islanderContract: OmniPointHardhat = { eid: EndpointId.ISLANDER_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> islander // islander <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. islander) islanderContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → islander, confirmations for islander → Optimism] [20, 20], // 5) Enforced execution options: // [options for Optimism → islander, options for islander → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: islanderContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose islander ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Vana Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=islander&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=islander) and [Executor](../deployed-contracts.md?chains=islander) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Viction Mainnet OFT Quickstart sidebar_label: Viction Mainnet OFT Quickstart description: How to get started building on Viction Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Viction Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Viction Mainnet (EID=30196) 'tomo-mainnet': { eid: EndpointId.TOMO_V2_MAINNET, url: process.env.RPC_URL_TOMO || 'https://viction.blockpi.network/v1/rpc/public', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const tomoContract: OmniPointHardhat = { eid: EndpointId.TOMO_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> tomo // tomo <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. tomo) tomoContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → tomo, confirmations for tomo → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → tomo, options for tomo → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: tomoContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose tomo ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Viction Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=tomo&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=tomo) and [Executor](../deployed-contracts.md?chains=tomo) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Worldchain Mainnet OFT Quickstart sidebar_label: Worldchain Mainnet OFT Quickstart description: How to get started building on Worldchain Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Worldchain Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Worldchain Mainnet (EID=30319) 'worldchain-mainnet': { eid: EndpointId.WORLDCHAIN_V2_MAINNET, url: process.env.RPC_URL_WORLDCHAIN || 'https://worldchain-mainnet.g.alchemy.com/public', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const worldchainContract: OmniPointHardhat = { eid: EndpointId.WORLDCHAIN_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> worldchain // worldchain <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. worldchain) worldchainContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → worldchain, confirmations for worldchain → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → worldchain, options for worldchain → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: worldchainContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose worldchain ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Worldchain Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=worldchain&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=worldchain) and [Executor](../deployed-contracts.md?chains=worldchain) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: X Layer Mainnet OFT Quickstart sidebar_label: X Layer Mainnet OFT Quickstart description: How to get started building on X Layer Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **X Layer Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // X Layer Mainnet (EID=30274) 'xlayer-mainnet': { eid: EndpointId.XLAYER_V2_MAINNET, url: process.env.RPC_URL_XLAYER || 'https://rpc.xlayer.tech', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const xlayerContract: OmniPointHardhat = { eid: EndpointId.XLAYER_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> xlayer // xlayer <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. xlayer) xlayerContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → xlayer, confirmations for xlayer → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → xlayer, options for xlayer → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: xlayerContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose xlayer ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **X Layer Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=xlayer&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=xlayer) and [Executor](../deployed-contracts.md?chains=xlayer) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Xai Mainnet OFT Quickstart sidebar_label: Xai Mainnet OFT Quickstart description: How to get started building on Xai Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Xai Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Xai Mainnet (EID=30236) 'xai-mainnet': { eid: EndpointId.XAI_V2_MAINNET, url: process.env.RPC_URL_XAI || 'https://xai-chain.net/rpc', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const xaiContract: OmniPointHardhat = { eid: EndpointId.XAI_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> xai // xai <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. xai) xaiContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → xai, confirmations for xai → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → xai, options for xai → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: xaiContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose xai ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Xai Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=xai&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=xai) and [Executor](../deployed-contracts.md?chains=xai) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: XChain Mainnet OFT Quickstart sidebar_label: XChain Mainnet OFT Quickstart description: How to get started building on XChain Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **XChain Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // XChain Mainnet (EID=30291) 'xchain-mainnet': { eid: EndpointId.XCHAIN_V2_MAINNET, url: process.env.RPC_URL_XCHAIN || 'https://xchain-rpc.idex.io', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const xchainContract: OmniPointHardhat = { eid: EndpointId.XCHAIN_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> xchain // xchain <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. xchain) xchainContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → xchain, confirmations for xchain → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → xchain, options for xchain → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: xchainContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose xchain ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **XChain Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=xchain&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=xchain) and [Executor](../deployed-contracts.md?chains=xchain) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: XDC Mainnet OFT Quickstart sidebar_label: XDC Mainnet OFT Quickstart description: How to get started building on XDC Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **XDC Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // XDC Mainnet (EID=30365) 'xdc-mainnet': { eid: EndpointId.XDC_V2_MAINNET, url: process.env.RPC_URL_XDC || 'https://rpc.xdcrpc.com', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const xdcContract: OmniPointHardhat = { eid: EndpointId.XDC_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> xdc // xdc <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. xdc) xdcContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → xdc, confirmations for xdc → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → xdc, options for xdc → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: xdcContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose xdc ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **XDC Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=xdc&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=xdc) and [Executor](../deployed-contracts.md?chains=xdc) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: XPLA Mainnet OFT Quickstart sidebar_label: XPLA Mainnet OFT Quickstart description: How to get started building on XPLA Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **XPLA Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // XPLA Mainnet (EID=30216) 'xpla-mainnet': { eid: EndpointId.XPLA_V2_MAINNET, url: process.env.RPC_URL_XPLA || 'https://dimension-evm-rpc.xpla.dev', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const xplaContract: OmniPointHardhat = { eid: EndpointId.XPLA_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> xpla // xpla <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. xpla) xplaContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → xpla, confirmations for xpla → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → xpla, options for xpla → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: xplaContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose xpla ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **XPLA Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=xpla&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=xpla) and [Executor](../deployed-contracts.md?chains=xpla) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Zircuit Mainnet OFT Quickstart sidebar_label: Zircuit Mainnet OFT Quickstart description: How to get started building on Zircuit Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Zircuit Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Zircuit Mainnet (EID=30303) 'zircuit-mainnet': { eid: EndpointId.ZIRCUIT_V2_MAINNET, url: process.env.RPC_URL_ZIRCUIT || 'https://zircuit1-mainnet.p2pify.com', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const zircuitContract: OmniPointHardhat = { eid: EndpointId.ZIRCUIT_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> zircuit // zircuit <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. zircuit) zircuitContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → zircuit, confirmations for zircuit → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → zircuit, options for zircuit → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: zircuitContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose zircuit ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Zircuit Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=zircuit&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=zircuit) and [Executor](../deployed-contracts.md?chains=zircuit) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: zkLink Mainnet OFT Quickstart sidebar_label: zkLink Mainnet OFT Quickstart description: How to get started building on zkLink Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **zkLink Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash LZ_ENABLE_ZKSOLC_EXAMPLE=1 npx create-lz-oapp@latest # select onft721-zksync, even if you are not using onft ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // your zkSync chain 'zklink-mainnet': { eid: EndpointId.ZKLINK_V2_MAINNET, url: process.env.RPC_URL_ZKLINK || 'https://rpc.zklink.io', accounts, zksync: true, // crucial for zkSync networks ethNetwork: 'mainnet', verifyURL: 'https://explorer.era.zksync.io/contract_verification', }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const zklinkContract: OmniPointHardhat = { eid: EndpointId.ZKLINK_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> zklink // zklink <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. zklink) zklinkContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → zklink, confirmations for zklink → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → zklink, options for zklink → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: zklinkContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose zklink ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **zkLink Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=zklink&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=zklink) and [Executor](../deployed-contracts.md?chains=zklink) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: zkSync Era Mainnet OFT Quickstart sidebar_label: zkSync Era Mainnet OFT Quickstart description: How to get started building on zkSync Era Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **zkSync Era Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash LZ_ENABLE_ZKSOLC_EXAMPLE=1 npx create-lz-oapp@latest # select onft721-zksync, even if you are not using onft ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // your zkSync chain 'zksync-mainnet': { eid: EndpointId.ZKSYNC_V2_MAINNET, url: process.env.RPC_URL_ZKSYNC || 'https://mainnet.era.zksync.io', accounts, zksync: true, // crucial for zkSync networks ethNetwork: 'mainnet', verifyURL: 'https://explorer.era.zksync.io/contract_verification', }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const zksyncContract: OmniPointHardhat = { eid: EndpointId.ZKSYNC_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> zksync // zksync <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. zksync) zksyncContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → zksync, confirmations for zksync → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → zksync, options for zksync → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: zksyncContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose zksync ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **zkSync Era Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=zksync&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=zksync) and [Executor](../deployed-contracts.md?chains=zksync) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: zkSync Sepolia Testnet OFT Quickstart sidebar_label: zkSync Sepolia Testnet OFT Quickstart description: How to get started building on zkSync Sepolia Testnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **zkSync Sepolia Testnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash LZ_ENABLE_ZKSOLC_EXAMPLE=1 npx create-lz-oapp@latest # select onft721-zksync, even if you are not using onft ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // your zkSync chain 'zksync-sepolia-testnet': { eid: EndpointId.ZKSYNCSEP_V2_TESTNET, url: process.env.RPC_URL_ZKSYNC_SEPOLIA || 'https://sepolia.era.zksync.dev', accounts, zksync: true, // crucial for zkSync networks ethNetwork: 'sepolia', verifyURL: 'https://explorer.sepolia.era.zksync.dev/contract_verification', }, // another network you want to connect to 'optimism-testnet': { eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import { EndpointId } from '@layerzerolabs/lz-definitions' import type { OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat' import { OAppEnforcedOption } from '@layerzerolabs/toolbox-hardhat' import { ExecutorOptionType } from '@layerzerolabs/lz-v2-utilities' import { TwoWayConfig, generateConnectionsConfig } from '@layerzerolabs/metadata-tools' const zksync-sepoliaContract: OmniPointHardhat = { eid: EndpointId.ZKSYNCSEP_V2_TESTNET, contractName: 'MyOFT', } const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', } // To connect all the above chains to each other, we need the following pathways: // Optimism <-> zksync-sepolia // zksync-sepolia <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ] const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. zksync-sepolia) zksync-sepoliaContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → zksync-sepolia, confirmations for zksync-sepolia → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → zksync-sepolia, options for zksync-sepolia → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ] export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways) return { contracts: [{ contract: optimismContract }, { contract: zksync-sepoliaContract }], connections, } } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose zksync-sepolia ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **zkSync Sepolia Testnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://testnet.layerzeroscan.com/tools/defaults?srcChainKey[0]=zksync-sepolia&version=V2&dstChainKey[0]=optimism-sepolia) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=zksync-sepolia) and [Executor](../deployed-contracts.md?chains=zksync-sepolia) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Zora Mainnet OFT Quickstart sidebar_label: Zora Mainnet OFT Quickstart description: How to get started building on Zora Mainnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Zora Mainnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Zora Mainnet (EID=30195) 'zora-mainnet': { eid: EndpointId.ZORA_V2_MAINNET, url: process.env.RPC_URL_ZORA || 'https://rpc.zora.energy', accounts, }, // another network you want to connect to 'optimism-mainnet': { eid: EndpointId.OPTIMISM_V2_MAINNET, url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const zoraContract: OmniPointHardhat = { eid: EndpointId.ZORA_V2_MAINNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTIMISM_V2_MAINNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> zora // zora <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. zora) zoraContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → zora, confirmations for zora → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → zora, options for zora → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: zoraContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose zora ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Zora Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey[0]=zora&version=V2&dstChainKey[0]=optimism) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=zora) and [Executor](../deployed-contracts.md?chains=zora) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Berachain Bepolia Testnet OFT Quickstart sidebar_label: Berachain Bepolia Testnet OFT Quickstart description: How to get started building on Berachain Bepolia Testnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Berachain Bepolia Testnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Berachain Bepolia Testnet (EID=40371) 'bepolia-testnet': { eid: EndpointId.BEPOLIA_V2_TESTNET, url: process.env.RPC_URL_BEPOLIA || 'https://bepolia.rpc.berachain.com', accounts, }, // another network you want to connect to 'optimism-testnet': { eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const bepoliaContract: OmniPointHardhat = { eid: EndpointId.BEPOLIA_V2_TESTNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> bepolia // bepolia <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. bepolia) bepoliaContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → bepolia, confirmations for bepolia → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → bepolia, options for bepolia → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: bepoliaContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose bepolia-testnet ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Berachain Bepolia Testnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://testnet.layerzeroscan.com/tools/defaults?srcChainKey[0]=bepolia-testnet&version=V2&dstChainKey[0]=optimism-sepolia) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=bepolia-testnet) and [Executor](../deployed-contracts.md?chains=bepolia-testnet) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Monad Testnet OFT Quickstart sidebar_label: Monad Testnet OFT Quickstart description: How to get started building on Monad Testnet and use the OFT Standard displayed_sidebar: null --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Monad Testnet** and any other supported chain. ## Project scaffold LayerZero's CLI lets you spin up an OFT workspace in seconds: ```bash npx create-lz-oapp@latest # choose → "OFT example" ``` The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts. ## Add private keys Rename `.env.example` file to `.env` and update it with needed configurations: ```js PRIVATE_KEY = your_private_key; // Required ``` 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. ## Hardhat network config Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to: ```ts networks: { // the network you are deploying to or are already on // Monad Testnet (EID=40204) 'monad-testnet': { eid: EndpointId.MONAD_V2_TESTNET, url: process.env.RPC_URL_MONAD || 'https://testnet-rpc.monad.xyz', accounts, }, // another network you want to connect to 'optimism-testnet': { eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, } ``` ## LayerZero wiring config Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection: ```ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const monadContract: OmniPointHardhat = { eid: EndpointId.MONAD_V2_TESTNET, contractName: 'MyOFT', }; const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> monad // monad <-> Optimism // For this example's simplicity, we will use the same enforced options values for sending to all chains // 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, }, ]; const pathways: TwoWayConfig[] = [ [ // 1) Chain B's contract (e.g. Optimism) optimismContract, // 2) Chain A's contract (e.g. monad) monadContract, // 3) Channel security settings: // • first array = "required" DVN names // • second array = "optional" DVN names array + threshold // • third value = threshold (i.e., number of optionalDVNs that must sign) // [ requiredDVN[], [ optionalDVN[], threshold ] ] [['LayerZero Labs' /* ← add more DVN names here */], []], // 4) Block confirmations: // [confirmations for Optimism → monad, confirmations for monad → Optimism] [1, 1], // 5) Enforced execution options: // [options for Optimism → monad, options for monad → Optimism] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: monadContract}], connections, }; } ``` :::caution It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security.md#layerzero's-channel-security-model) and understand the impact of each of these configuration settings. See [**Next Steps**](#nextsteps) to review the available providers and security settings. ::: ## The token contract ```solidity title="contracts/MyOFT.sol" // 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, Ownable { constructor(string memory name, string memory symbol, address endpoint, address owner) OFT(name, symbol, endpoint, owner) Ownable(owner) {} } ``` :::tip The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract. You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart.md) for a better understanding of how OFTs work and what contracts to use. ::: ## Deploy ```bash npx hardhat lz:deploy # choose monad-testnet ``` You will be presented with a list of networks to deploy to. Fund your deployer with native gas tokens beforehand. ## Connect the chains ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Verify peers: ```bash npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts ``` ## Transfer ### Calling `send` Since the `send` logic has already been defined, we'll instead view how the function should be called. ```typescript import {task} from 'hardhat/config'; import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities'; import {Options} from '@layerzerolabs/lz-v2-utilities'; import {BigNumberish, BytesLike} from 'ethers'; interface Args { amount: string; to: string; toEid: EndpointId; } interface SendParam { dstEid: EndpointId; // Destination endpoint ID, represented as a number. to: BytesLike; // Recipient address, represented as bytes. amountLD: BigNumberish; // Amount to send in local decimals. minAmountLD: BigNumberish; // Minimum amount to send in local decimals. extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message. composeMsg: BytesLike; // The composed message for the send() operation. oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations. } // send tokens from a contract on one network to another task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter') .addParam('to', 'contract address on network B', undefined, types.string) .addParam('toEid', 'destination endpoint ID', undefined, types.eid) .addParam('amount', 'amount to transfer in token decimals', undefined, types.string) .setAction(async (taskArgs: Args, {ethers, deployments}) => { const toAddress = taskArgs.to; const eidB = taskArgs.toEid; // Get the contract factories const oftDeployment = await deployments.get('MyOFT'); const [signer] = await ethers.getSigners(); // Create contract instances const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer); const decimals = await oftContract.decimals(); const amount = ethers.utils.parseUnits(taskArgs.amount, decimals); let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(); // Now you can interact with the correct contract const oft = oftContract; const sendParam: SendParam = { dstEid: eidB, to: addressToBytes32(toAddress), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed }; // Get the quote for the send operation const feeQuote = await oft.quoteSend(sendParam, false); const nativeFee = feeQuote.nativeFee; console.log( `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`, ); const ERC20Factory = await ethers.getContractFactory('ERC20'); const innerTokenAddress = await oft.token(); // // If the token address !== address(this), then this is an OFT Adapter // if (innerTokenAddress !== oft.address) { // // If the contract is OFT Adapter, get decimals from the inner token // const innerToken = ERC20Factory.attach(innerTokenAddress); // // Approve the amount to be spent by the oft contract // await innerToken.approve(oftDeployment.address, amount); // } const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, { value: nativeFee, }); console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`); }); ``` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; import { SendParam, OFTReceipt } 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"; import { MyOFT } from "../contracts/MyOFT.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; /** * @dev Converts an address to bytes32. * @param _addr The address to convert. * @return The bytes32 representation of the address. */ function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() public { // Fetching environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND"); // Fetch the private key from environment variable uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Start broadcasting with the private key vm.startBroadcast(privateKey); MyOFT sourceOFT = MyOFT(oftAddress); bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam( 30111, // You can also make this dynamic if needed addressToBytes32(toAddress), _tokensToSend, _tokensToSend * 9 / 10, _extraOptions, "", "" ); MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false); console.log("Fee amount: ", fee.nativeFee); sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender); // Stop broadcasting vm.stopBroadcast(); } } ``` ## Done! You've issued an omnichain token and bridged it from **Monad Testnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step. ## Troubleshooting If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it: 1. **Wiring Didn't Succeed** Run the following to inspect your on‑chain wiring configuration: ```bash npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` See that your source configuration has a valid send library, DVN address, and target eid. 2. **No Default Pathway** LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist. - **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://testnet.layerzeroscan.com/tools/defaults?srcChainKey[0]=monad-testnet&version=V2&dstChainKey[0]=optimism-sepolia) on LayerZero Scan. - **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section). - Re-run your wiring command for the connections so that the wiring on both chains is live. ```ts [ optimismContract, currentContract, [['LayerZero Labs'], []], // required & optional DVNs [1, 1], // required block confirmations [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], ], ``` Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting. ## Next steps - See the [Configuring Pathways](../../developers/evm/create-lz-oapp/configuring-pathways.md) section to learn more about managing your OFTs across the entire network mesh. - See the [Available DVNs](../dvn-addresses.md?chains=monad-testnet) and [Executor](../deployed-contracts.md?chains=monad-testnet) to configure between. - Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero.md) section. - Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart.md) in the EVM section. --- --- title: Workers Overview sidebar_label: Workers Overview --- # Workers in LayerZero V2 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 cross-chain 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. #### LayerZero Workers Configurations --- This architecture allows LayerZero V2 to provide robust, decentralized cross-chain communication while giving application developers the tools needed to fine-tune their security and operational parameters. --- --- title: Build Decentralized Verifier Networks (DVNs) --- 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 functions: `assignJob` and `getFee`. ```solidity 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 Name | Type | Description | | ------------------ | ------- | ---------------------------------------------------------------------------- | | `assignJob` | Payable | Called as part of `_lzSend`. | | `getFee` | View | Typically called by applications before sending the packet to estimate fees. |

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 PacketSent( bytes encodedPacket, bytes options, address sendLibrary) ``` The packet has the following structure: ```solidity 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 DVNFeePaid( address[] requiredDVNs, address[] optionalDVNs, uint256[] fees ); ``` :::tip 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 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 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 struct UlnConfig { uint64 confirmations; // ... ``` 5. Your DVN should next do an idempotency check: ```solidity 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 ULN._verify( _packetHeader, _payloadHash, _confirmations ); ``` :::tip To know your workflow has successfully fulfilled its obligation, your DVN should perform an idempotency check at the end of the DVN workflow. ::: --- --- title: Build Executors --- This document contains a high level overview of how to implement and integrate a basic third party Executor into the LayerZero V2 protocol. ## Fee Quoting, Collection, and Withdrawal Executors should implement and deploy an Executor contract on every chain they want to support. The contract must implement the `ILayerZeroExecutor` interface, which specifies two functions: `assignJob` and `getFee`. ```solidity 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); } ``` | Function Name | Type | Description | | ------------------ | ------- | ---------------------------------------------------------------------------- | | `assignJob` | Payable | Called as part of `_lzSend`. | | `getFee` | View | Typically called by applications before sending the packet to estimate fees. |

If your Executor is responsible for a packet, the LayerZero Endpoint will call your Executor contract's `assignJob` function. ## Building an Executor The Executor is divided into two off-chain workflows: the Committer and the Executor. ### Committer 1. The Committer role first listens for the `PacketSent` event: ```solidity PacketSent( bytes encodedPacket, bytes options, address sendLibrary ); ``` 2. After the `PacketSent` event, the `ExecutorFeePaid` is how you know your Executor has been assigned to commit and execute the packet. ```solidity ExecutorFeePaid( address executor, uint256 fee ); ``` 3. After receiving the fee, your Executor should listen for the `PacketVerified` event, signaling that the packet can now be committed to the destination messaging channel. ```solidity PayloadVerified( address dvn, bytes header, uint256 confirmations, bytes32 proofHash ); ``` 4. After listening for the previous events, your Executor should perform an idempotency check by calling **Ultra Light Node 301** and **Ultra Light Node 302**: ```solidity ULN.verifiable( _packetHeader, _payloadHash ); ``` This function will return the following possible states: ```solidity enum VerificationState { Verifying, Verifiable, Verified } ``` If the state is `Verifying`, your Executor must wait for more DVNs to sign the packet's payloadHash. After a DVN signs the payloadHash, it will emit `PayloadVerified`. ```solidity PayloadVerified( address dvn, bytes header, uint256 confirmations, bytes32 proofHash); ``` :::tip Your Executor only needs to perform subsequent checks of `VerificationState` when it hears `PayloadVerified` on the destination chain. :::

If the state is `Verifiable`, then your Executor must call `commitVerification`: ```solidity function commitVerification(bytes calldata _packetHeader, bytes32 _payloadHash) external; ``` If the state is `Verified`, the commit has already occurred and the commit workflow can be terminated. :::tip To know your workflow is finished, your Executor should perform an idempotency check at the end of the commit workflow. ::: ### Executor 1. The Executor role first listens for the `PacketSent` event: ```solidity PacketSent( bytes encodedPacket, bytes options, address sendLibrary) ``` 2. After the `PacketSent` event, the `ExecutorFeePaid` is how you know your Executor has been assigned to commit and execute the packet. ```solidity ExecutorFeePaid( address executor, uint256 fee); ``` 3. After receiving the fee, your Executor should listen for the `PacketVerified` event, signaling that the packet can now be executed. 4. After listening for the previous events, your Executor should perform an idempotency check: ```solidity endpoint.executable( _packetHeader, _payloadHash) ``` This function will return the following possible states: ```solidity enum ExecutionState { NotExecutable, Executable, Executed } ``` If the state is `NotExecutable`, your Executor must wait for the committer to commit the message packet, or you may have to wait for some previous nonces. If the state is `Executable`, your Executor should decode the packet's options using the `options.ts` package and call the Endpoint's `lzReceive` function with the packet information: ```solidity endpoint.lzReceive( _origin, _receiver, _guid, _message, _extraData) ``` :::tip To know your workflow is finished, your Executor should perform an idempotency check at the end of the execute workflow. :::

If the state is `Executed`, your Executor has fulfilled its obligation, and you can terminate the Executor workflow. ### Mock Executor Both [Paladin Blockchain Security](https://github.com/0xpaladinsecurity/zexecutor) and [Lazer Technologies](https://github.com/LazerTechnologies/LayerZero-Executor) have built an implementation and open-sourced the codebase for anyone interested in reviewing a sample Executor implementation. :::caution These codebases are not owned by LayerZero. Exercise caution when interacting with any third party contracts or sample materials. ::: --- --- id: overview title: Tools Overview slug: /tools/overview --- # LayerZero Tools Overview This section of the documentation covers **three** key resources that help developers inspect and integrate with LayerZero’s cross-chain 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 block explorer** that showcases: - **Cross-chain 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 cross-chain 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 cross-chain transaction data as the web UI, but in a programmatic manner: - `GET /messages/{messageId}` to fetch message details - `GET /transactions/{chainKey}/{txHash}` to see bridging logs for a particular chain TX - Query-based endpoints to **filter** or **search** messages by chain, status, or time **Use it if**: - You want to **automate** cross-chain 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 cross-chain 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.md). --- --- id: overview title: LayerZero Scan Overview --- # LayerZero Scan Overview LayerZero Scan is a **block explorer** specifically for observing cross-chain transaction activity facilitated by LayerZero. Here’s how to get started with navigating it: ## What Is LayerZero Scan? **LayerZero Scan** is designed to display cross-chain messaging details such as: - Transaction hashes and bridging events across multiple chains - Source and destination chain info - Status of messages (in-flight, delivered, failed) - On-chain addresses (contracts, wallets) participating in bridging The interface consolidates data from multiple blockchains to provide a single view of cross-chain messaging. ## Key Sections in LayerZero Scan 1. **Search Bar** - Allows you to search by cross-chain 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 cross-chain 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 cross-chain 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 cross-chain transaction or bridging message was routed. - **Debugging**: If your cross-chain message fails, you’ll see error statuses or partial deliveries. - **Confirming**: Ensure your bridging transaction has arrived on the destination chain with finality. ## Next Steps - If you want to automate data retrieval from these cross-chain events, check out the [`LayerZero Scan API`](./api) or the [`Endpoint Metadata`](../endpoint-metadata.md) for programmatic solutions. --- --- id: api title: LayerZero Scan Swagger API --- # LayerZero Scan Swagger API The LayerZero Scan Swagger API provides a comprehensive interface for programmatically tracking and analyzing cross-chain messages and transactions. With this API, developers can: - **Query Cross-Chain Messages:** Search for messages by message ID, transaction hash, GUID, wallet address, or by endpoint and omnichain application (OApp) address. - **Filter and List Messages:** Retrieve lists of messages filtered by status (e.g., INFLIGHT, DELIVERED, FAILED), by specific pathways, or by monthly periods. - **Access Detailed Transaction Data:** Get detailed information on message states, including source and destination transaction details, verification statuses, and configuration metadata. This API is particularly useful for monitoring bridging activities, tracking the progress of cross-chain interactions, and integrating LayerZero data into dashboards or other analytical tools. ## 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 additional information and interactive testing, please refer to the Swagger UI for your desired 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) --- --- id: endpoint-metadata title: LayerZero Endpoint Metadata --- # LayerZero Endpoint Metadata The LayerZero Endpoint Metadata provides a comprehensive JSON snapshot of all the key information needed to build and analyze cross-chain 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 cross-chain 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 on-chain 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 on-chain 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 on-chain 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. --- --- title: LayerZero Endpoint V2 Examples and Developer Tooling sidebar_label: LayerZero Examples and Packages --- 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` --- --- id: ai-resources title: AI Resources slug: /ai-resources sidebar_label: AI Resources --- # Download LLM Files 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 | | Full Documentation | Full content of all documentation pages | 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.