--- title: Get Started sidebar_label: Overview description: Choose your development path based on the virtual machines you want to target. --- LayerZero enables **omnichain messaging** - sending data and instructions between different blockchains. ## Developer setup Build on Solana using Rust and the Anchor framework for high-performance applications. ## Choose a network ## Start building --- --- title: Sample Projects sidebar_label: Sample Projects --- import SampleProjectsGallery from '@site/src/components/SampleProjectsGallery'; # Browse sample projects Explore the library of sample projects using LayerZero. --- --- 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**](../../developers/evm/oft/quickstart.md): Create an Omichain Fungible Token that works across chains. - [**Omnichain NFT**](../../developers/evm/onft/quickstart.md): Build an Omnichain Non-Fungible Token (ONFT) collection that works across chains. - [**Omnichain Read**](../../developers/evm/lzread/overview.md): Read external state from other chains and perform calculations, using LayerZero Read. --- --- title: Adding Networks 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. This guide shows how to add a new EVM network to your existing LayerZero project. **Example scenario:** You have an OFT deployed on Optimism Sepolia and Base Sepolia, and want to add Arbitrum Sepolia to your mesh. **Existing networks in your mesh:** - Optimism Sepolia (EID: 40232) - Base Sepolia (EID: 40245) **Network being added:** - Arbitrum Sepolia (EID: 40231) You will: - Update `hardhat.config.ts` with the new network's LayerZero [Endpoint ID (EID)](../../deployments/deployed-contracts.md) and RPC - Deploy your contract to the new network - Update `layerzero.config.ts` to declare the contract and connections - Wire peers and apply configuration across pathways ## 1) Add the network to `hardhat.config.ts` Add an entry with the LayerZero Endpoint ID, RPC URL, and your deployer accounts. Here's the diff showing what to add: ```typescript // hardhat.config.ts import { EndpointId } from '@layerzerolabs/lz-definitions' export default { networks: { // Existing networks 'optimism-sepolia': { eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OPT_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, 'base-sepolia': { eid: EndpointId.BASSEP_V2_TESTNET, url: process.env.RPC_URL_BASE_SEPOLIA || 'https://sepolia.base.org', accounts, }, // New network being added + 'arbitrum-sepolia': { + eid: EndpointId.ARBSEP_V2_TESTNET, + url: process.env.RPC_URL_ARB_SEPOLIA || 'https://sepolia-rollup.arbitrum.io/rpc', + 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`). ::: ## 2) Deploy your contract to the new network Use the CLI to deploy your contract to the added network. You can deploy interactively or target a specific network. ```bash # Interactive (select networks and tags when prompted) pnpm hardhat lz:deploy # Or target a specific network (example name from step 1) # pnpm hardhat deploy --network arbitrum-sepolia --tags MyOFT ``` ## 3) Add the new contract and connections to `layerzero.config.ts` Specify which contracts should be connected on a per pathway basis: ```typescript // layerzero.config.ts import {EndpointId} from '@layerzerolabs/lz-definitions'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; import {OAppEnforcedOption, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; + const arbitrumSepoliaContract: OmniPointHardhat = { + eid: EndpointId.ARBSEP_V2_TESTNET, + contractName: 'MyOFT', + }; const optimismSepoliaContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', }; const baseSepoliaContract: OmniPointHardhat = { eid: EndpointId.BASSEP_V2_TESTNET, contractName: 'MyOFT', }; // For this example's simplicity, we will use the same enforced options values for sending to all chains // For production, you should ensure `gas` is set to the correct value through profiling the gas usage of calling OApp._lzReceive(...) on the destination chain // To learn more, read https://docs.layerzero.network/v2/concepts/applications/oapp-standard#execution-options-and-enforced-settings const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 80000, value: 0, }, ]; // Add pathways to connect your new network with existing networks + const pathways: TwoWayConfig[] = [ + [ + arbitrumSepoliaContract, // New network contract + optimismSepoliaContract, // Existing network 1 + [['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 + ], + [ + arbitrumSepoliaContract, // New network contract + baseSepoliaContract, // Existing network 2 + [['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] + [1, 1], // [A to B confirmations, B to A confirmations] + [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain C enforcedOptions, Chain A enforcedOptions + ], + // ... existing pathways between your current networks + ]; export default async function () { // Generate the connections config based on the pathways + const connections = await generateConnectionsConfig(pathways); return { contracts: [ + {contract: arbitrumSepoliaContract}, {contract: optimismSepoliaContract}, {contract: baseSepoliaContract}, ], + connections, }; } ``` ## 4) Wire peers and apply configuration Set peers, libraries, [DVN](../../concepts/glossary.md#dvn-decentralized-verifier-network)/[Executor](../../concepts/glossary.md#executor) settings, and enforced options per your `layerzero.config.ts`. ```bash pnpm hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` You can refer to defaults with the [Defaults Checker](https://layerzeroscan.com/tools/defaults) before wiring in production. ## 5) Verify connections and configuration ```bash # Check peers pnpm hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts # Check pathway config pnpm hardhat lz:oapp:config:get --oapp-config layerzero.config.ts ``` ## Checking Pathway Configurations To check your OApp's current configuration, you can run: ```bash 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 │ │ │ │ │ └──────────────────────┴────────────────────────────────────────────────────┘ │ └──────────────────────┴────────────────────────────────────────────────────┘ │ └────────────────────┴─────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────┘ ``` ## Specifying Pathway Configurations For more specific configurations on a per pathway basis, review the [Configuring Pathways](./configuring-pathways) page. ## Related References - [Deployed Endpoints, Libraries, Executors](../../deployments/deployed-contracts.md) - [OFT Technical Reference](../../concepts/technical-reference/oft-reference.md) --- --- 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)](../../developers/evm/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)](../../developers/evm/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](../../developers/evm/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)](../../developers/evm/onft/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 --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; 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. LayerZero's CLI makes use of the `@layerzerolabs/metadata-tools` package, which allows for a human readable `layerzero.config.ts` file. Here's how to use it: 1. Install metadata-tools: `pnpm add -D @layerzerolabs/metadata-tools` 2. Create a new [LZ config](/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, }; } ``` 2b. If your pathways include Solana, run the Solana init config command: ``` npx hardhat lz:oft:solana:init-config --oapp-config layerzero.config.ts ``` - Note that only the Solana contract object requires `address` to be specified. Do not specify `address` for non-Solana contract objects. - The above examples contains a minimal mesh with only one pathway (two chains) for demonstration purposes. You are able to add as many pathways as you need into the `connections` param, via `generateConnectionsConfig`. 3. Run the wire command: ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` The wire command will process all the transactions required to connect the pathways specified in the config file. If you change anything in the config, run the command again. Each pathway contains a `config`, containing multiple configuration structs for changing how your OApp sends and receives messages, specifically for the chain your OApp is sending `from`: | Name | Type | Description | | ----------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sendLibrary` | Address | The message library used for configuring all sent messages `from` this chain. (e.g., `SendUln302.sol`) | | `receiveLibraryConfig` | Struct | A struct containing the receive message library address (e.g., `ReceiveUln302.sol`), and an optional BigInt, `gracePeriod`, the time to wait before updating to a new MessageLib version during version migration. Controls how the `from` chain receives messages. | | `receiveLibraryTimeoutConfig` | Struct | An optional param, defining when the old receive library (`lib`) will expire (`expiry`) during version migration. | | `sendConfig` | Struct | Controls how the OApp sends `from` this pathway, containing two more structs: `executorConfig` and `ulnConfig` (DVNs). | | `receiveConfig` | Struct | Controls how the OApp (`from`) receives messages, specifically the `ulnConfig` (DVNs). | | `enforcedOptions` | Struct | Controls the minimum destination gas sent to the destination, per message type (e.g., `_lzReceive`, `lzCompose`, etc.) in your OApp. | :::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.md) 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 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; ``` ### 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. ::: ## Applying Changes Wiring your contracts will set the `peer` address for your OApp or OFT and initialize the desired configuration in your `layerzero.config.ts`. ### Wiring Contracts The CLI Tool makes this one step easier by enabling you to wire and configure your contract pathways with a single command: ```bash $ npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` Before wiring your contracts, you should review your `layerzero.config.ts` to ensure that you have specified accurately the configuration you want to set. Wiring your contracts will set the `peer` address for your OApp or OFT and initialize the desired configuration in your `layerzero.config.ts`. The CLI Tool makes this one step easier by enabling you to wire and configure your contract pathways with a single command: ```bash $ 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: 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 V2 Glossary sidebar_label: Glossary toc_min_heading: 2 toc_max_heading: 5 --- This glossary defines and explains key LayerZero concepts and terminology. ## Chain ID The native blockchain identifier assigned by the network itself (for example, `1` for Ethereum Mainnet, `42161` for Arbitrum Mainnet). This is distinct from LayerZero's [Endpoint ID (EID)](#endpoint-id), which is the protocol's internal identifier used to route messages between chains. When interacting with the LayerZero protocol, you'll primarily work with EIDs rather than chain IDs. See [Endpoint](#endpoint) for more details. ## Channel / Lossless Channel A dedicated message pathway in LayerZero defined by four specific components: the sender OApp (source application contract), the source endpoint ID, the destination endpoint ID, and the receiver OApp (destination application contract). The channel maintains message ordering through nonce tracking, ensuring messages are delivered exactly once and in the correct sequence. For example, if a token bridge on Ethereum (sender OApp) is communicating with its counterpart on Arbitrum (receiver OApp), their messages flow through a unique channel distinct from all other application pathways between these chains. Each channel maintains its own independent message sequence, allowing multiple applications to communicate across the same chain pairs without interference. ## Compose / Composition {#compose} 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. ## Escrow Account An escrow account is a financial arrangement where a third party, holds funds or assets on behalf of another until specific conditions are met. **Vertical Composability** The traditional form of smart contract composability, where multiple function calls are stacked within a single transaction. In vertical composability, all operations must succeed together or the entire transaction reverts, providing atomic execution. For example, when a 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. ## CPI (Cross Program Invocation) A CPI in Solana is when one program calls the instruction of another program. For more, refer to the [official Solana documentation](https://solana.com/en/docs/core/cpi) ## Destination Chain The blockchain network that receives and processes a LayerZero message. The destination chain hosts the contract that will execute the received message's instructions through its `lzReceive` function. ## DVN (Decentralized Verifier Network) A network of independent verifiers that validate message integrity between chains. DVNs are part of LayerZero's modular security model, allowing applications to configure multiple verification schemes for their messages. ### Dead DVN A placeholder DVN used when the default LayerZero configuration is inactive for a specific pathway. Dead DVNs appear when new chains are added before default providers (e.g., Google Cloud, Polyhedra) support every pathway. They function as null addresses - no verification will match, and messages will be blocked until the Dead DVN is replaced with a functional DVN. ## Endpoint The core, immutable smart contract deployed on each blockchain that serves as the entry and exit point for LayerZero messages. The Endpoint provides standardized interfaces for sending, receiving, and configuring messages. It's the primary interface through which applications interact with LayerZero. ## Endpoint ID Endpoint ID (EID) is LayerZero's internal identifier used to route messages between chains. Each Endpoint contract has a unique EID for determining which chain's endpoint to send to or receive messages from. EID values have no relation to Chain ID values - since LayerZero spans both EVM and non-EVM chains, EIDs provide a unified addressing system across all supported blockchains. When using LayerZero contract methods, you'll work with EIDs rather than native chain IDs. The EID numbering convention follows a structured pattern: - **30xxx**: Mainnet chains - **40xxx**: Testnet chains To check if a LayerZero contract supports communication with another chain, use the `isSupportedEid()` method with the target chain's EID. See also: [FAQ — Endpoint ID vs Chain ID](/v2/faq#endpoint-id-vs-chain-id) ### Committer A committer is an off-chain process that monitors a cross-chain message and, once it receives the required confirmations from the configured DVNs, submits a commit transaction to the destination chain. This action validates the message, making it ready for execution by the Executor. ## Executor Ensures the seamless execution of messages on the destination chain by following instructions set by the OApp owner on how to automatically deliver omnichain messages to the destination chain. An off-chain service that monitors message verification status and executes verified messages on destination chains when all required DVNs have verified the message. Executors handle gas payments and message delivery. It's a permissionless service that can be run by any party. ## GUID (Global Unique ID) A unique identifier generated for each LayerZero message that combines the message's nonce, source chain, destination chain, and participating contracts. GUIDs ensure messages can be tracked across the network and prevent replay attacks. ## Lazy nonce (lazy inbound nonce) A mechanism that tracks the highest consecutively delivered message number for a channel. Messages can be verified out of order, but they can only be executed sequentially starting from the lazy nonce. All messages before the message with lazyNonce have been verified. This ensures lossless message delivery while allowing parallel verification. ## LZ Config The file that declares the configuration for the OApp. Configuration refers to things such as the pathways (connections), DVN (Security Stack), and more. In our examples, this file has the default name of `layerzero.config.ts` but its name can be arbitrary. When needed, the LayerZero CLI expects the LZ config file via the `--oapp-config` flag. Check out the [LZ config in the OFT example](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft/layerzero.config.ts). ## `lzCompose` _First, see [Compose](#compose) 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. ### Enforced Options Enforced options are OApp-level options that, when configured, must be included on every message to ensure minimum gas limits for a pathway. Enforced options can be set by the owner via `setEnforcedOptions`, typically populated during wiring from `layerzero.config.ts`. For each send call, the caller’s options are combined with the enforced options and any extra per-transaction options. See: [Enforcing Options](./message-options.md#enforcing-options) and [Simple Config](../tools/simple-config.md). ### Extra Options Per‑transaction options passed in `send`. Merged with enforced options to adjust gas/behavior or supply `msg.value`. See: [Extra Options](./message-options.md#extra-options). ## Nonce A unique identifier for the message _within specific messaging channel_. Prevents replay attacks and censorship by defining a strong gapless ordering between all nonces in each channel. Each channel maintains its own independent nonce counter. Difference between nonce and GUID: - Nonce is unique within a channel (between two endpoints) and sequential. - GUID is unique across all channels and is not sequential, allowing for tracking messages across the entire LayerZero network. ## OApp (Omnichain Application) A smart contract that implements LayerZero's messaging interface for 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 Adapter An OFT Adapter enables an existing token (e.g. ERC-20, SPL token) to function as an OFT. The OFT Adapter contract serves as a lockbox for the original token. OFT Adapters will utilize lock and mint: ![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. ## PDA (Program Derived Address) A PDA is a Solana account owned by a program and derived using "seeds". Refer to the official [Solana documentation](https://solana.com/docs/core/pda). ## Security Stack The combination of MessageLib, DVNs, and other security parameters that an application configures for its 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. --- --- id: faq title: Frequently Asked Questions (FAQ) sidebar_label: General FAQ --- import CollapsibleContent from '@site/src/components/CollapsibleContent'; ## General ### What is LayerZero and how does LayerZero Work? {#what-is-layerzero-and-how-does-layerzero-work} 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. For details on how it works, see the [Protocol Overview](concepts/protocol/protocol-overview). ### Where can I find LayerZero contract examples? {#where-can-i-find-layerzero-contract-examples} The [devtools repository](https://github.com/LayerZero-Labs/devtools/tree/main) contains contract examples across all supported VMs. It acts as the central hub for the LayerZero developer experience, including application contract standards, CLI examples, packages, scripting tools, and more. ### What is the estimated delivery time for a LayerZero message? {#what-is-the-estimated-delivery-time-for-a-layerzero-message} The delivery time of a LayerZero message can be broken down into a series of processing stages. Every message goes through the following high-level steps: - Source Block Confirmations: The message waits for the source chain to finalize a specified number of block confirmations. To view the default configuration for a given pathway, refer to the [default configs checker](deployments/deployed-contracts). - DVN/Verification: Each Decentralized Verifier Network (DVN) submits one transaction to verify the message. - Committer/Commit Verification: One additional transaction is required to commit the verified message. - Executor/Message Execution: A final transaction is submitted to execute the message on the destination chain. Estimated Total Delivery Time You can estimate the total message delivery time with the following formula: `Total Time ≈ (sourceBlockTime × number of block confirmations) + (destinationBlockTime × (2 blocks + number of DVNs))` Note: This formula offers a rough estimate for a message that does not implement `lzCompose`. It assumes that each transaction is included in the next block without delay and does not factor in network latency, or other real-world conditions that may affect transaction processing time. ### What's the difference between Endpoint ID (EID) and Chain ID? {#endpoint-id-vs-chain-id} **Chain ID** is an EVM-specific concept for the native identifier assigned by a blockchain network itself (for example, `1` for Ethereum Mainnet, `42161` for Arbitrum Mainnet). It’s used by node clients, wallets, and RPCs to identify the network. **Endpoint ID (EID)** is LayerZero’s internal identifier used by the protocol to route messages between chains. Every LayerZero Endpoint contract has a unique EID. EIDs are VM-agnostic and do not map 1:1 to chain IDs. Key points: - EIDs are what you use in LayerZero configs, CLI commands, and contract calls - Chain IDs are for EVM chains and used for RPCs/wallets; EIDs are for LayerZero messaging - For EIDs, Mainnets typically use the `30xxx` range; testnets use the `40xxx` range Where to find EIDs: see the list of deployed contracts and Endpoint IDs in [Deployed Contracts](deployments/deployed-contracts) ### What's the difference between Delegate and Owner? {#whats-the-difference-between-delegate-and-owner} Delegate manages LayerZero protocol configurations via the LayerZero Endpoint contract. Owner manages an OApp's peers, ownership(transfer & revoke) and who the delegate is. Owner is defined on the OApp contract itself using OpenZeppelin's Ownable. ⚠️ Important Configuration Note for Using Devtools The owner and delegate addresses should typically be the same. Assigning different accounts to these roles may result in an `LZ_Unauthorized()` error. This is because the delegate can call most wire functions (like `setPeer()`), but only the owner can call `setEnforcedOptions()`. If the accounts differ and you attempt to call `setEnforcedOptions()` through the delegate, the call will fail. Unless there's a specific need to separate permissions, it's recommended to use the same address for both owner and delegate to avoid unintended access issues. ### Can I use the OFT standard if I already have tokens deployed across multiple chains? {#can-i-use-the-oft-standard-if-i-already-have-tokens-deployed-across-multiple-chains} While it is possible to integrate existing tokens deployed across multiple chains with the OFT standard, this requires additional setup. Specifically, [MintBurnOFTAdapter](https://github.com/LayerZero-Labs/devtools/tree/main/examples/mint-burn-oft-adapter) — a variant of OFTAdapter.sol enables OFT functionality by calling the mint and burn methods of the innerToken on each chain. However, for this integration to work: - Each innerToken contract must expose externally callable `mint` and `burn` functions. - The innerToken must grant the MintBurnOFTAdapter the necessary permissions (`MINTER_ROLE` and `BURNER_ROLE`) to invoke these functions. ### What is sharedDecimals? Can I override default sharedDecimals in an OFT contract? {#what-is-shareddecimals-can-i-override-default-shareddecimals-in-an-oft-contract} The `sharedDecimals` is the "lowest common denominator" of decimal precision across all chains in the OFT system. It limits how many decimal places can be reliably represented when moving tokens cross‑chain. By default, `sharedDecimals` is set to 6, which is sufficient for most use cases across different VMs. However, OApps can override this default if the OApp's total token supply exceeds (2⁶⁴–1) / 10⁶. ⚠️ CAUTION: If you override the vanilla `sharedDecimals` amount or have an existing token supply exceeding 18,446,744,073,709.551615 tokens, extra caution should be applied to ensure `amountSD` and `amountLD` do not overflow. See more explanations [here](concepts/technical-reference/oft-reference). ### What should I set for msgType 1 and msgType 2 {#what-should-i-set-for-msgtype-1-and-msgType 2} msgType 1 and msgType 2 are used to distinguish between two types of messages when setting `enforcedOptions`. Use the `OptionsBuilder` to add the message execution options for each msgType. msgType 1 = `SEND` This msgType is a basic token transfer or message send. It does not include a composed message. Options you can use: - [`lzReceive` Option](../v2/tools/sdks/options#lzreceive-option) to specify the gas values the Executor uses when calling `lzReceive` on the destination chain. - [`lzNativeDrop` Option](../v2/tools/sdks/options#lznativedrop-option) to specify how much native gas to drop to any address on the destination chain. msgType 2 = `SEND_AND_CALL` This msgType includes additional composed message(s), allowing for one or more extra calls to be sent along with the message. Options you can use: - [`lzReceive` Option](../v2/tools/sdks/options#lzreceive-option) to specify the gas values the Executor uses when calling `lzReceive` on the destination chain. - [`lzCompose` Option](../v2/tools/sdks/options#lzcompose-option) to allocate gas and value for Composed Messages on the destination chain. [See more information for composed message options](../v2/developers/evm/composer/overview#composed-message-execution-options). Please follow the [best practice](../v2/tools/sdks/options#best-practices) to determine the gas cost for `lzRecieve` and `lzCompose` option. Overestimating wastes funds; underestimating may cause message execution to fail. ### Why is my transaction expensive? {#why-is-my-transaction-expensive} If you are using Sepolia as the destination chain, transaction costs can spike due to high and volatile gas prices on Sepolia. To avoid high fees during testing, it is recommended to use alternative testnets as the destination chain. LayerZero's transaction pricing model is designed to fairly distribute costs across the various components that enable secure, reliable cross-chain messaging. Understanding this model helps developers and users make informed decisions about gas allocation and fee optimization. Learn more: [Transaction Pricing Model](concepts/protocol/transaction-pricing) ### Can I cancel verification of an in-flight message? {#can-i-cancel-verification-of-an-in-flight-message} Yes. An OApp's delegate can call the `skip()` method on the endpoint to stop delivery. The skip function should be used only in instances where either message verification fails or must be stopped, not message execution. LayerZero provides separate handling for retrying or removing messages that have successfully been verified, but fail to execute. Learn more: [Skip Message Guide](developers/evm/troubleshooting/debugging-messages#skipping-nonce) ### What is "LZ Dead DVN"? {#what-is-lz-dead-dvn} LZ Dead DVN Represents a [Dead Decentralized Verifier Network (DVN)](concepts/glossary#dead-dvn). These contracts are placeholders used when the default LayerZero config is inactive and will require the OApp owner to manually configure the contract's config to use the pathway. LayerZero allows anyone to permissionlessly run DVNs, but default providers (e.g. Google Cloud, Polyhedra) may not cover every chain immediately. If a pathway lacks default DVN support, OApps must explicitly configure supported DVNs on both the source and destination chains. ### How do I deploy my OFT to a new EVM network? {#how-do-i-deploy-my-oft-to-a-new-network} To add a new network to your existing OFT deployment, you'll need to update your project configuration and deploy the OFT to the new chain. This involves: 1. Adding the new network to your `hardhat.config.ts` with the correct LayerZero Endpoint ID and RPC URL 2. Deploying the OFT on the new network 3. Updating your `layerzero.config.ts` to include the new contract and connections 4. Wiring peers and applying configuration (If the current OFT deployment involves a non-EVM chain, follow the specific instructions for that VM) For detailed step-by-step instructions, see [Adding Networks](get-started/create-lz-oapp/project-config). ## Error & Troubleshooting ### How do I resolve the "Please set your OApp's DVNs and/or Executor" error? {#how-do-i-resolve-the-please-set-your-oapps-dvns-and-or-executor-error} This error indicates that your OApp configuration is missing the required DVN and/or Executor settings. On Mainnet, default DVNs are not guaranteed to be available for every pathway. OApps must explicitly configure supported DVNs on both the source and destination chains. DVN Addresses can be found [here](deployments/dvn-addresses). To simplify setup, use [simple config generator](/v2/tools/simple-config) , which provides CLI commands for setting DVNs. Be sure to also consult the VM-specific configuration sections to complete your configurations properly. ### What are the common causes for \_quote() function failures? {#what-are-the-common-causes-for-\_quote()-function-failures} Failures in `_quote()` often stem from: - **Incomplete Network Pathway**: Not all pathways are fully wired, especially on testnets. Contact LayerZero if you require support for a specific pathway. - **Missing [`enforcedOptions`](concepts/message-options#enforcing-options) or [`extraOptions`](concepts/message-options#extra-options)**: At least one must be set for a successful quote. - **[LZ Dead DVN](concepts/glossary#dead-dvn)**: If your configuration includes LZ Dead DVN for a particular pathway, the quote will fail. OApps must configure DVNs explicitly on Mainnet. ### What does "NotInitializable" mean on LayerZero Scan? {#what-does-notinitializable-mean-on-layerzero-scan} This status typically indicates that the destination OApp is either missing trusted peer settings or the pathway has not been properly initialized. Common causes: - Incorrect peer configuration: Ensure that `setPeer()` is correctly called on both the source and destination chains during deployment. Double-check that the address format and EID (endpoint ID) are accurate. - Pathway not initialized correctly: Confirm that `allowInitializePath()` is properly implemented in your OApp contract. Learn more: [Integration Checklist](tools/integration-checklist.md#set-peers-on-every-pathway) On Solana, if you have a custom writing implementation, ensure that `endpoint.initOAppNonce` was called with the correct parameters. ### Why is my message marked as "Blocked"? {#why-is-my-message-marked-as-blocked} A "Blocked" message usually points to configuration issues: - **NotInitializable**: Ensure peers are set correctly on both ends or pathway is initialized correctly as explained above. - **DVN mismatch**: All DVN providers must be the same on source and destination. - **Block confirmations mismatch**: Outbound confirmations must be ≥ inbound confirmations. ### How do I prevent out-of-gas errors on the destination chain? {#how-do-i-prevent-out-of-gas-errors-on-the-destination-chain} To avoid out-of-gas errors when executing `lzReceive()` on the destination: - Use `enforcedOptions` (set at the OApp/OFT contract level) to define a default gas value for all sends. - Use `extraOptions` (specified in the `send` function call) for transaction-specific overrides. Both values must sum to at least the gas required by `lzReceive()` on the destination. It is also common to simply use `enforcedOptions` and set `extraOptions` to `0x`. If the destination message fails due to insufficient gas, you can retry it manually: - [Retry a failed message on EVM](developers/evm/troubleshooting/debugging-messages#retry-message) - [Retry a failed message on Solana](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/solana/retryPayload.ts) ## Ecosystem ### Which projects are part of the LayerZero ecosystem? {#which-projects-are-part-of-the-layerzero-ecosystem} You can explore the current list of projects on the [Ecosystem page](https://layerzero.network/ecosystem/dapps). This list is not exhaustive—projects can submit a request to be listed. ### How can my project be listed on the ecosystem page? {#how-can-my-project-be-listed-on-the-ecosystem-page} Submit your application via the [Ecosystem Listing Form](https://layerzeronetwork.typeform.com/lzecosystem) if your dApp uses LayerZero for omnichain messaging. ### Are there grants available for builders on LayerZero? {#are-there-grants-available-for-builders-on-layerzero} LayerZero does not have a traditional grant program. However, the **LayerZero Foundation** runs **lzCatalyst**, a program connecting top builders with leading VCs in the space. More info: [lzCatalyst Program](https://info.layerzero.foundation/lzcatalyst-5f11ee16cc12) --- --- sidebar_label: Omnichain Applications (OApp) title: 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 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: Omnichain Tokens (OFT & ONFT) title: Omnichain Tokens --- LayerZero’s omnichain token standards provide a unified framework to **transfer both fungible and non-fungible tokens across different blockchain networks**. ### Omnichain Token Standards #### OFT (Omnichain Fungible Token) A standard for fungible tokens that uses LayerZero messaging to debit on the source chain **(burn or lock)** and credit on the destination chain **(mint or unlock)**, preserving a single unified global supply across all connected networks. For new tokens, regular OFTs can be used, which utilizes the **burn/mint** mechanism on all chains. For existing tokens without an owner or mint authority, the **OFT Adapter** variation can be used which uses the **lock/unlock** mechanism on the original chain. An adapter contract can lock tokens on the source chain and **mint** on the destination, enabling omnichain transfers without modifying the original token contract. #### ONFT (Omnichain Non-Fungible Token) A standard for non-fungible tokens that uses LayerZero messaging to move NFTs between chains while preserving uniqueness and ownership. ONFTs support both burn-and-mint and adapter-based lock/mint/unlock patterns. ### Omnichain Tokens Principles Regardless of whether the tokens are built on EVM, Solana, or Aptos (or other environments), the underlying design follows the same core principles. ## Unified 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/overview.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 Composers title: Omnichain Composers --- **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 Vaults (OVault) title: Omnichain Vaults (OVault) --- **Omnichain Vaults** extend the [ERC-4626 tokenized vault standard](https://ethereum.org/en/developers/docs/standards/tokens/erc-4626/) with LayerZero's omnichain messaging, enabling users to **deposit assets from any chain** and **receive yield-bearing vault shares on their preferred network** in a single transaction. ## What Are Omnichain Vaults? - **Beyond single-chain vaults:** Traditional ERC-4626 vaults restrict users to depositing and withdrawing on a single blockchain. OVault removes this limitation by making vault shares **omnichain fungible tokens (OFTs)** that can move seamlessly between any LayerZero-connected chain. - **Hub-and-spoke architecture:** The vault itself lives on one "hub" chain, while users can interact with it from any "spoke" chain. This design maintains the security and simplicity of a single vault while providing universal access across the entire omnichain ecosystem. ![OVault Comparison](/img/ovault-light-comparison.svg#gh-light-mode-only) ![OVault Comparison](/img/ovault-dark-comparison.svg#gh-dark-mode-only) ## Mental Model: Two OFT Meshes + Vault To understand OVault architecture, think of it as **two separate OFT meshes** (`asset` + `share`) connected by an ERC-4626 vault and composer contract on a hub chain: - **Asset OFT Mesh**: Enables the vault's underlying assets (e.g., `USDT`) to move across chains using standard OFT implementation - **Share OFT Mesh**: Enables vault shares to move across chains, using OFTAdapter (lockbox model) on the hub chain and standard OFT elsewhere - **ERC-4626 Vault**: Lives on the hub chain, implements standard `deposit`/`redeem` operations - **OVault Composer**: Orchestrates cross-chain vault operations by receiving assets or shares with special instructions and coordinating the vault interactions with OFT transfers **Key insight**: Users never interact directly with the vault - they send assets or shares cross-chain to the composer with encoded instructions, and the composer handles all vault operations and transfers out to the target destination. This design leverages existing LayerZero standards ([OFT](./oft-standard.md) + [Composer](./composer-standard.md)) to make asset movement seamless between multiple blockchains. ## Why Omnichain Vaults Matter - **Unified liquidity across chains:** Instead of fragmenting liquidity across multiple single-chain vaults, OVault aggregates all deposits into one vault. This creates deeper liquidity, more efficient yield generation, and simpler management for vault operators. - **Seamless user experience:** Users no longer need to bridge assets manually, switch networks, or manage multiple transactions. A single transaction handles the entire flow—from depositing assets on one chain to receiving shares on another. - **Cross-chain DeFi composability:** Vault shares as OFTs can be used as collateral, traded on DEXs, or integrated into any DeFi protocol on any chain. This unlocks new possibilities for yield-bearing assets in the omnichain ecosystem. ## How Omnichain Vaults Work 1. **Asset deposit flow:** When a user deposits `assets` from a source chain, the OVault system: - Transfers the `assets` to the hub chain via **LayerZero's OFT standard** - Executes the deposit workflow via **LayerZero's Composer standard** which: - Deposits `assets` into the `ERC-4626` vault - Mints vault `shares` - Sends the `shares` to the user's desired destination chain address via the OFT standard 2. **Share redemption flow:** When redeeming shares for underlying `assets`: - `shares` are sent from the user's current chain back to the hub - The vault redeems `shares` for the underlying `assets` - `assets` are then sent to the user's chosen destination chain address 3. **Automatic error recovery:** If any step fails (due to slippage, gas issues, or configuration errors), the OVault Composer provides permissionless recovery mechanisms to refund or retry the operation, ensuring users never lose funds. ## Core Design Principles - **Full ERC-4626 compatibility:** OVault maintains complete compatibility with the ERC-4626 standard. The vault contract itself is a standard implementation—the omnichain functionality is added through LayerZero's OFT and Composer patterns. - **Deterministic pricing:** Unlike AMM-based systems, ERC-4626 vaults use deterministic share pricing based on `totalAssets / totalSupply`. This eliminates the need for oracles and reduces cross-chain complexity. - **Permissionless recovery:** All error recovery functions are permissionless—anyone can trigger refunds or retries for failed operations. This ensures that users always have a path to recover their assets without relying on admin intervention. - **Configurable security:** Vault operators can configure their security settings, including DVN selection, executor parameters, and rate limits, to match their risk tolerance and use case requirements. ## Common Use Cases - **Yield-bearing stablecoins:** Issue stablecoins backed by yield-generating vaults where users can mint and redeem from any chain while the underlying yield accrues to all holders. - **Real World Asset (RWA) tokenization:** Deploy RWA vaults on regulated chains while providing global access. Users worldwide can gain exposure to real-world yields without jurisdictional limitations. - **Cross-chain lending collateral:** Use vault shares as collateral on any chain. As the shares appreciate from yield accrual, borrowing power automatically increases. - **Omnichain yield aggregation:** Aggregate yield strategies from multiple chains into a single vault, giving users exposure to the best opportunities across the entire ecosystem. ## Integration with LayerZero Standards - **Built on OFT Standard:** Both the asset and share tokens use LayerZero's OFT standard for cross-chain transfers, ensuring consistent supply accounting and seamless movement between chains. - **Leverages Composer Pattern:** The OVault Composer handles complex multi-step operations (receive assets → deposit → send shares) in a single atomic transaction with automatic error handling. - **Protocol-level security:** Inherits LayerZero's security model with configurable DVNs, executors, and rate limiting to protect cross-chain operations. ## Further Reading For implementation guides and technical details: - [EVM OVault Implementation](../../developers/evm/ovault/overview.md) - [OFT Standard](./oft-standard.md) - [Composer Standard](./composer-standard.md) --- --- 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/protocol-contracts-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 --- 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 title: 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 {#layerzeros-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**](../tools/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 Ordering --- LayerZero offers both **unordered delivery** and **ordered delivery**, providing developers with the flexibility to choose the most appropriate transaction ordering mechanism based on the specific requirements of their application. ## Unordered Delivery By default, the LayerZero protocol uses **unordered delivery**, where transactions can be executed out of order if all transactions prior have been verified. If transactions `1` and `2` have not been verified, then transaction `3` cannot be executed until the previous nonces have been verified. Once nonces `1`, `2`, `3` have been verified: - If nonce `2` failed to execute (due to some gas or user logic related issue), nonce `3` can still proceed and execute. ![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. ::: ## Enabling Ordered Delivery {#enabling-ordered-delivery} To implement strict nonce enforcement, you need to implement the following: - a mapping to track the maximum received nonce. - override `_acceptNonce` and `nextNonce`. - add `ExecutorOrderedExecutionOption` in `_options` when calling `_lzSend`. - a governance function to keep the nonce mapping between the protocol and application in sync when skipping nonces. :::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](../developers/evm/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(); ``` ## Keeping Nonces In Sync When skipping nonces at the protocol level, such as calling `endpoint.skip`, your OApp's local mapping must be incremented as well. If the local `receivedNonce` mapping falls behind the protocol's stored nonce, subsequent messages will revert with an invalid nonce error. A governance helper could look like: ```solidity /** * @notice skips exactly the next‐in‐line message, and keeps our mapping in perfect sync * @param _srcEid the LayerZero source chain ID * @param _sender the address of the remote sender (packed as bytes32) * @param _nonce the nonce to skip — must equal nextNonce(_srcEid,_sender) */ function skipInboundNonce( uint32 _srcEid, bytes32 _sender, uint64 _nonce ) public onlyOwner { // 1) sanity‐check that you're skipping exactly the next message uint64 expected = nextNonce(); require(_nonce == expected, "OApp: invalid skip nonce"); // 2) fire the skip on the endpoint IMessagingChannel(address(endpoint)).skip( address(this), _srcEid, _sender, _nonce ); // 3) sync our mapping receivedNonce[_srcEid][_sender] = _nonce; } ``` Keeping these values aligned ensures `nextNonce` returns the correct value and prevents ordered messages from being blocked. Implement strict nonce enforcement via function override: ```solidity pragma solidity ^0.8.20; import { OApp } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; // Import OApp and other necessary contracts/interfaces /** * @title OmniChain Nonce Ordered Enforcement Example * @dev Implements nonce ordered enforcement for your OApp. */ contract MyNonceEnforcementExample is OApp { // Mapping to track the maximum received nonce for each source endpoint and sender mapping(uint32 eid => mapping(bytes32 sender => uint64 nonce)) private receivedNonce; /** * @dev Constructor to initialize the omnichain contract. * @param _endpoint Address of the LayerZero endpoint. * @param _owner Address of the contract owner. */ constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) {} /** * @dev Public function to get the next expected nonce for a given source endpoint and sender. * @param _srcEid Source endpoint ID. * @param _sender Sender's address in bytes32 format. * @return uint64 Next expected nonce. */ function nextNonce(uint32 _srcEid, bytes32 _sender) public view virtual override returns (uint64) { return receivedNonce[_srcEid][_sender] + 1; } /** * @dev Internal function to accept nonce from the specified source endpoint and sender. * @param _srcEid Source endpoint ID. * @param _sender Sender's address in bytes32 format. * @param _nonce The nonce to be accepted. */ function _acceptNonce(uint32 _srcEid, bytes32 _sender, uint64 _nonce) internal virtual override { receivedNonce[_srcEid][_sender] += 1; require(_nonce == receivedNonce[_srcEid][_sender], "OApp: invalid nonce"); } // @dev Override receive function to enforce strict nonce enforcement. function _lzReceive( Origin calldata _origin, bytes32 _guid, bytes calldata _message, address _executor, bytes calldata _extraData ) public payable virtual override { _acceptNonce(_origin.srcEid, _origin.sender, _origin.nonce); // your _lzReceive(...) logic continues here } } ``` --- --- 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 in LayerZero V2 sidebar_label: Workers Overview --- 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) title: 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](../glossary#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](../glossary#dead-dvn)**? Since LayerZero allows for anyone to permissionlessly run DVNs, the network may occassionally add new chain Endpoints before the default providers (Google Cloud or Polyhedra) support every possible pathway to and from that chain. A default configuration with a **Dead DVN** will require you to either configure an available DVN provider for that Send or Receive pathway, or run your own DVN if no other security providers exist, before messages can safely be delivered to and from that chain. ::: :::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 title: 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#lznativedrop-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. ::: --- --- title: Transaction Pricing Model sidebar_label: Transaction Pricing Model description: Understanding how LayerZero calculates fees for cross-chain messaging. --- LayerZero's transaction pricing model is designed to fairly distribute costs across the various components that enable secure, reliable cross-chain messaging. Understanding this model helps developers and users make informed decisions about gas allocation and fee optimization. ## Why Cross-Chain Pricing is Complex Traditional blockchain transactions occur within a single network where gas costs are predictable and uniform. Cross-chain messaging introduces unique challenges: - **Source chains have no knowledge** of destination chain state, gas prices, or execution requirements - **Multiple networks** with different native tokens, gas mechanisms, and pricing models must be coordinated - **Off-chain infrastructure** (DVNs and Executors) provides critical services that require compensation - **Message execution** on the destination must be funded upfront from the source chain LayerZero's pricing model addresses these challenges through a transparent, component-based fee structure. ## Four-Component Fee Structure Every LayerZero transaction consists of four distinct cost elements: ### 1. Source Chain Transaction The standard blockchain transaction fee paid to miners/validators on the source network for including your transaction in a block. This follows each chain's native fee mechanism (gas on Ethereum, compute units on Solana, etc.). ### 2. Security Stack Fees Payment to your configured [Decentralized Verifier Networks (DVNs)](../modular-security/security-stack-dvns.md) for verifying and attesting to your message. These fees: - Vary based on your security configuration (number and type of DVNs) - Scale with the complexity of verification required - Are split among your chosen verifier networks ### 3. Executor Fees Compensation to [Executors](../permissionless-execution/executors.md) for delivering and executing your message on the destination chain. This covers: - Monitoring source chains for new messages - Submitting transactions on destination chains - Managing the operational infrastructure for reliable delivery ### 4. Destination Gas Purchase The cost of purchasing destination chain gas tokens to fund your message execution. This is calculated by converting your specified gas amount from destination pricing to source chain tokens. ## Cross-Chain Gas Conversion Since you pay on the source chain but consume gas on the destination chain, LayerZero workers perform real-time conversion using market prices: $$ \Large \text{Source Chain Cost} = \text{gasUnits} \times \text{dstGasPrice} \times \frac{\text{dstTokenPrice}}{\text{srcTokenPrice}} $$ Where: - **gasUnits**: Amount of gas needed on destination chain (e.g., 200,000) - **dstGasPrice**: Gas price on destination chain (e.g., 50 gwei) - **dstTokenPrice**: USD price of destination chain's native token (e.g., $3,000 for ETH) - **srcTokenPrice**: USD price of source chain's native token (e.g., $1.50 for MATIC) The formula works in two steps: 1. **Calculate destination gas cost**: `gasUnits × dstGasPrice` = cost in destination tokens 2. **Convert to source tokens**: Multiply by the price ratio to get equivalent cost in source tokens ### Example Scenario Sending from **Polygon** (MATIC) to **Ethereum** (ETH): - **gasUnits**: 200,000 units - **dstGasPriceWei**: 50 gwei - **dstTokenPrice**: ETH = $3,000 - **srcTokenPrice**: MATIC = $1.50 **Calculation**: ``` Step 1: Calculate gas cost on destination chain 200,000 gas units × 50 gwei = 10,000,000 gwei = 0.01 ETH Step 2: Convert to source chain tokens using price ratio 0.01 ETH × ($3,000 ETH ÷ $1.50 MATIC) = 0.01 × 2,000 = 20 MATIC ``` This ensures you pay the correct amount in your source chain's currency to fund execution on any destination chain. ## Dynamic Pricing Factors Several factors influence the final transaction cost: ### Chain-Specific Variations - **Gas mechanisms** differ across chains (Ethereum's EIP-1559, Arbitrum's L2 fees, Solana's compute units) - **Network congestion** affects base gas prices - **Token price volatility** impacts cross-chain conversion rates ### Security Configuration Impact - More DVNs increase verification costs but enhance security - Premium DVN services may charge higher fees - Custom security thresholds affect overall pricing ### Execution Requirements - Complex contract logic requires more destination gas - Composed messages need additional execution allowances - Message size affects processing costs ## Fee Estimation and Quotes LayerZero provides on-chain quote mechanisms that calculate exact fees before message submission: ### Quote Components - **Native fee**: Cost in the source chain's native token - **LZ token fee**: Alternative payment option using LayerZero's utility token - **Real-time pricing**: Updates based on current gas prices and token values ### Payment Flexibility Applications can choose between: - **Native token payment**: Using the source chain's gas token (ETH, MATIC, AVAX, etc.) - **LZ token payment**: Using LayerZero's cross-chain utility token for consistent pricing ## Gas Profiling Considerations Destination gas requirements vary significantly based on your application logic: ### Typical Gas Ranges - **Simple token transfers**: 60,000-80,000 gas - **Complex DeFi interactions**: 200,000-500,000 gas - **Multi-step composed operations**: 300,000+ gas ### Optimization Strategies - **Profile your contracts** on each target chain to understand actual consumption - **Include gas buffers** to account for network-specific variations - **Test execution paths** thoroughly to avoid failed deliveries - **Monitor gas costs** across different chains and adjust allocations accordingly ## Best Practices ### For Developers - **Design gas-efficient contracts** to minimize destination execution costs - **Implement proper fee estimation** in your application interfaces - **Consider chain-specific optimizations** for frequently used pathways - **Plan for gas price volatility** in your economic models ### For Users - **Understand total cost breakdown** before initiating transactions - **Consider timing** transactions during periods of lower network congestion - **Monitor cross-chain fee patterns** to optimize transaction scheduling - **Plan gas allocations** based on the complexity of your destination operations ## Economic Alignment LayerZero's pricing model creates proper economic incentives: - **Security providers** are compensated for verification services - **Infrastructure operators** earn fees for reliable message delivery - **Gas efficiency** is rewarded through lower total costs - **Fair pricing** ensures each pathway pays for its actual resource consumption This transparent, component-based approach ensures that cross-chain messaging costs reflect the true value provided by each part of the LayerZero ecosystem while maintaining predictable pricing for applications and users. --- --- title: OApp Technical Reference sidebar_label: OApp Technical Reference description: Reference for LayerZero’s Omnichain Application (OApp) standard, detailing deployment, message flow, and core concepts. --- LayerZero’s **Omnichain Application (OApp)** standard defines a common set of patterns and interfaces for any smart contract that needs to send and receive messages across multiple blockchains. By inheriting OApp’s core functionality, higher-level primitives (such as OFT, ONFT, or any custom cross-chain logic) can rely on a unified, secure messaging layer. All OApp implementations must handle: - **Message sending**: Encode and dispatch outbound messages - **Message receiving**: Decode and process inbound messages - **Fee handling**: Quote, collect, and refund native & ZRO fees - **Peer management**: Maintain trusted mappings between chains - **Channel management and security**: Control security and execution settings between chains ## Deployment Every OApp needs to be deployed on each chain where it will operate. Initialization involves two steps: ### 1. Integrate with the local Endpoint 1. Pass the local Endpoint V2 address into your constructor or initializer. 2. The Endpoint’s delegate authority is set to your OApp and the address initializing unless overridden. 3. As a delegate, your OApp can call any `endpoint.*` security method (`setSendLibrary`, `setConfig`, etc.) in a secure, authorized manner. ### 2. Configure peers (directional peering) 1. On each chain, the owner calls `setPeer(eid, peerAddress)` to register the remote OApp’s address for a given Endpoint ID. 2. Repeat on the destination chain: register the source chain’s OApp address under its Endpoint ID. 3. Because trust is directional, the receiving OApp checks `peers[srcEid] == origin.sender` before processing inbound messages. :::info For guidelines on channel security, see [**Message Channel Security**](../protocol/message-security.md). For an example implementation, see the [**OFT Technical Reference**](./oft-reference.md). ::: ## Core Message Flow OApps follow a three-step life cycle. Developers focus on local state changes and message encoding; LayerZero handles secure routing and final delivery. | Phase | Actors | Responsibility | | ----------------------- | ---------------------------- | ------------------------------------------------------------ | | **1. `send(...)`** | OApp | Perform local state change and encode the message | | **3. Transport** | LayerZero, DVNs, & Executors | Build, verify, and route the packet to the destination chain | | **4. `lzReceive(...)`** | OApp | Validate origin, decode message, apply state change | ### 1. `send(...)` Entrypoint - **Developer-defined logic** 1. Perform a local state change (e.g., burn or lock tokens, record intent). 2. Encode all necessary data (addresses, amounts, or arbitrary instructions) into a byte array. 3. Optionally accept execution options (gas limits, native gas transfers, or LayerZero Executor services). - **Key points** - Your public `send(...)` handles only local logic and message construction. - All packet assembly, peer lookup, and fee handling occur inside the internal call to `endpoint.send(...)`. ### 2. Transport and Routing - **Fee payment and validation** 1. Ensure the caller has supplied exactly the required native or ZRO fee. 2. When `endpoint.send(...)` executes, the Endpoint verifies that the fees match the quote from the chosen messaging library. Underpayment causes a revert. - **Packet construction and dispatch** 1. The Endpoint computes the next outbound nonce for `(sender, dstEid, receiver)` and builds a `Packet` struct with `nonce`, `srcEid`, `sender`, `dstEid`, `receiver`, `GUID`, and the raw `message`. 2. It looks up which send library to use, either a per-OApp override or a default, for `(sender, dstEid)`. 3. The send library serializes the `Packet` into an `encodedPacket` and returns a `MessagingFee` struct. 4. The Endpoint emits a `PacketSent(...)` event so DVNs and Executors know which packet to process. - **DVNs & Executors** - Paid DVNs pick up the packet, verify its integrity, and relay it to the destination chain’s Endpoint V2. - The destination library enforces DVN verification and block-confirmation requirements based on your receive config. - **Destination Endpoint validation** 1. Verify that the packet’s `srcEid` has a registered peer. 2. Confirm that `origin.sender` matches `peers[srcEid]`. - **Invoke `lzReceive(...)`** - If validation succeeds, the destination Endpoint calls your OApp’s public `lzReceive(origin, guid, message, executor, extraData)`. ### 3. `lzReceive(...)` Entrypoint - **Access control and peer check** - Only the Endpoint may call `lzReceive`. - Immediately validate that `_origin.sender == peers[_origin.srcEid]`. - **Internal `_lzReceive(...)` logic** 1. Decode the byte array into original data types (addresses, amounts, or instructions). 2. Execute the intended on-chain business logic (e.g., mint tokens, unlock collateral, update balances). 3. If there’s a composable hook, your OApp can invoke `sendCompose(...)` to bundle further cross-chain calls. - **Outcome** - Upon completion, the destination chain’s state reflects the source chain’s intent. Any post-processing (events, composable calls) occurs here. This clear separation between local state updates in `send(...)` versus remote updates in `_lzReceive(...)` lets you focus on business logic while LayerZero’s Endpoint V2 manages transport intricacies. ## Security and Channel Management Whether you’re using Solidity, Rust, or Move, these foundational patterns ensure consistent security, extensibility, and developer ergonomics. ### Security and roles - **Owner** - Manages delegates, peers, and enforced gas settings - `setPeer(...)`: update trust mappings - `setDelegate(...)`: assign a new delegate for Endpoint configurations - `setEnforcedOptions(...)`: define per-chain minimum gas for inbound execution - **Delegate** - Manages Endpoint settings and message-channel controls - `setSendLibrary(oappAddress, eid, newLibrary)`: override send library for `(oappAddress, eid)`. - `setReceiveLibrary(oappAddress, eid, newLibrary, gracePeriod)`: override receive library; `gracePeriod` lets the previous library handle retries. - `setReceiveLibraryTimeout(oappAddress, eid, library, newTimeout)`: update how long an old receive library remains valid. - `setConfig(oappAddress, libraryAddress, params[])`: adjust per-library settings (DVNs, Executors, confirmations). - `skip(oappAddress, srcEid, srcSender, nonce)`: advance the inbound nonce without processing when verification fails. - `nilify(oappAddress, srcEid, srcSender, nonce, payloadHash)`: treat the payload as empty and advance the inbound nonce. - `burn(oappAddress, srcEid, srcSender, nonce, payloadHash)`: permanently discard a malicious or irrecoverable payload. :::tip Use multisigs or your preferred governance to manage Owner and Delegate roles. ::: ### Peering and Trust Management - **`peers` mapping** - Store a mapping from `eid → bytes32 peerAddress`. Using `bytes32` lets you store addresses for various chains. - `setPeer(eid, peerAddress)` updates that mapping. Passing `bytes32(0)` disables the pathway. - **Directional trust** - Registering on Chain A → Chain B does not register the reverse. Each side must call `setPeer` for the other. - On receipt, enforce `peers[origin.srcEid] == origin.sender` to confirm the message is from the expected contract. - **Updating peers** - If you redeploy or upgrade an OApp, call `setPeer` on both old and new deployments to maintain continuity. ## Further Reading - **[Message Channel Security](../protocol/message-security.md)** Deep dive into Endpoint V2's cryptographic guarantees, signature verification, and relayer/oracle incentives. - **[OFT Technical Reference](./oft-reference.md)** A concrete OApp example for fungible token transfers, illustrating how to use OApp's core patterns without platform-specific code. - **[Omnichain Composability](../applications/composer-standard.md)** Patterns for building advanced cross-chain primitives (AMM routers, multi-chain staking, governance) on top of OApp's hooks and the Executor model. --- --- sidebar_label: OFT Technical Reference title: Omnichain Fungible Token (OFT) Technical Reference --- LayerZero's **Omnichain Fungible Token** (OFT) standard enables a single fungible token to exist across many chains while preserving one global supply. The standard abstracts away differences in contract languages, so the high-level behavior is identical no matter which VM you deploy on. ## Deployment An OFT contract must be deployed on every network where a token currently exists or will exist. Since OFT contracts inherit all of the core properties of a LayerZero OApp, connecting OFT deployments requires setting a directional channel configuration between the source chain and the destination blockchain. ### Channel Configuration Every OFT deployment must have a directional channel configuration for messaging to be successful. This means the deployer must: - **Connect the messaging channel at the Endpoint level** (establishing the underlying pathway for cross-chain messages). - **Pair the OFT deployments at the OApp level** using `setPeer(...)`, so each contract knows its trusted counterpart on the destination chain. For an overview of what a messaging channel is, see [Message Channel Security](../protocol/message-security.md). For a more thorough explanation of channel configuration and peer relationships, see the [OApp Reference](./oapp-reference.md). ## Core Transfer Flow When an OFT transfer is initiated, the token balance on the source chain is **debited**. This either burns or locks the tokens inside the OFT contract, similar to an [escrow account](../glossary.md#escrow-account). A message is then sent via LayerZero to the destination chain where the paired OFT **credits** the recipient by minting or unlocking the same amount. This mechanism guarantees a unified supply across all chains. 1. **Debit on the source chain** The sender calls the OFT's `send(...)` function, burning or locking an amount of tokens. 2. **Message dispatch via LayerZero** The source OFT packages the transfer details into a LayerZero message and routes it through the protocol's messaging layer. LayerZero's messaging rails handle cross-chain routing, verification of the encoded message, and delivery of the message to the destination chain's receiver OFT contract. 3. **Credit on the destination chain** The paired OFT receives the message and _credits_ the recipient by minting new tokens or unlocking previously-held tokens. The total supply across all chains remains constant, since burned or locked tokens on the source chain are matched 1:1 with minted or unlocked tokens on the destination. 4. **(Optional) Trigger a composing call** A composing contract uses the tokens received in a new transaction, delivered automatically by the LayerZero Executor, to trigger some state change (e.g., swap, stake, vote). For more details on how to implement composable OFT transfers, see [Omnichain Composability](../applications/composer-standard.md). ## Core Concepts This section explains the fundamental design principles that make OFT a flexible, developer-friendly standard for fungible tokens. ### 1. Transferring Value Across Different VMs When transferring tokens across different virtual machines, OFT needs to handle varying decimal precision between chains. This is managed through a few key concepts: - **Local Decimals** Blockchains use [integer mathematics](https://www.helius.dev/blog/solana-arithmetic#use-integers-and-minor-units) to represent token amounts, avoiding floating-point precision issues. Each chain's recommended token standard stores tokens as integers but with different decimal place conventions to represent fractional units. For example: - **EVM chains**: ERC-20 tokens typically use 18 decimal places. What users see as "1.0 USDC" is stored on-chain as `1000000000000000000` (1 × 10^18) - the smallest unit often called "wei" - **Solana**: SPL tokens commonly use 6 or 9 decimal places. The same "1.0 USDC" would be stored as `1000000` (1 × 10^6) - the smallest unit of SOL called "lamports" (10^-9) - **Aptos**: Fungible Assets may use 6 or 8 decimal places depending on the asset Without proper conversion, transferring the integer value `1000000000000000000` from an 18-decimal EVM chain to a 6-decimal Solana chain would result in an astronomically large amount instead of the intended 1 token. The `localDecimals` field tells the OFT contract how many decimal places that specific blockchain uses to represent the token's smallest units. - **Shared Decimals** To ensure consistent value representation, every OFT declares a `sharedDecimals` parameter. Before sending a token cross-chain, the OFT logic converts the "local" amount into a normalized "shared" unit. Upon arrival, the destination OFT reconverts that shared unit back into the local representation of its own decimal precision. - **Dust Removal** Before converting the local unit amount (`amountLD`) into the shared unit amount (`amountSD`), OFT implementations first "floor" the local amount to the nearest multiple of the conversion rate so that no remainder ("dust") is included in the cross-chain transfer. The normalization process works as follows: 1. Compute the conversion rate: $$ \Large {decimalConversionRate} \;=\; 10^{\,(\text{localDecimals} - \text{sharedDecimals})} $$ 2. Remove dust by flooring to that multiple (e.g., integer division on the EVM): $$ \Large{flooredAmountLD} \;=\; \Bigl\lfloor \tfrac{\text{amountLD}}{\text{decimalConversionRate}} \Bigr\rfloor \times \text{decimalConversionRate} $$ 3. Compute and return the dust remainder to the sender: $$ \Large{dust} \;=\; \text{amountLD} \;-\; \text{flooredAmountLD} $$ That `dust` is refunded to the sender's balance before proceeding with **debiting** the sender's account, and the `flooredAmountLD` is now used as the `amountLD`. 4. Convert the amount in local decimals (`amountLD`) to shared units on the source chain: $$ \Large amountSD = \frac{amountLD}{decimalConversionRate} $$ 5. Transmit the amount in shared decimals (`amountSD`) as part of the LayerZero message. 6. On the destination chain, reconstruct the local amount (`amountLD`): $$ \Large amountLD = {amountSD}*{decimalConversionRate} $$ - **Why This Matters** - **Consistent Economic Value:** "1 OFT" means the same thing on any chain, regardless of differing decimal precision. - **DeFi Compatibility:** Prevents rounding errors and ensures seamless integration with on-chain tooling (e.g., AMMs, lending protocols) that expect familiar decimal behavior. - **No Precision Loss:** By using a common `sharedDecimals`, you avoid truncation or expansion mistakes when moving large or small amounts across networks. :::caution If you override the vanilla `sharedDecimals` amount or have an existing token supply exceeding `18,446,744,073,709.551615` tokens, extra caution should be applied to ensure `amountSD` and `amountLD` do not overflow. Vanilla OFTs can disregard this admonition. 1. **Shared‐Unit Overflow (`amountSD`)** OFT encodes `amountSD` as a 64-bit unsigned integer (`uint64`). The largest representable shared‐unit value is `2^64 − 1`. Therefore, the maximum token supply (in whole‐token terms) is: $$ \Large \frac{2^{64} - 1}{10^{\,\text{sharedDecimals}}} $$ In vanilla OFT implementations, `sharedDecimals = 6`, yielding a max supply of $$ \Large \frac{2^{64} - 1}{10^6} \;=\; 18{,}446{,}744{,}073{,}709.551615 \text{ tokens} $$ If you choose a smaller `sharedDecimals`, the divisor shrinks and you may exceed the `uint64` limit when converting a large `amountLD` into `amountSD`. 2. **Local‐Unit Overflow (`amountLD`)** On some chains (e.g., Solana's SPL Token or Aptos's Fungible Asset), the native token amount is also stored as a 64-bit unsigned integer (`uint64`). In those environments, the maximum local amount is `2^64 − 1`. But because `amountLD` must be a multiple of $$ \Large \text{decimalConversionRate} \;=\; 10^{(\text{localDecimals} - \text{sharedDecimals})} $$ If `amountSD × decimalConversionRate` would exceed `2^64 − 1`, the reconstructed `amountLD` cannot fit in the native `uint64` type. To avoid both overflow risks: - **Pick `sharedDecimals`** so that your target maximum supply divided by `10^{sharedDecimals}` is ≤ `2^64 − 1`. - **Verify each chain's local type** (e.g., `uint64` on Solana/Aptos or `uint256` on most EVM chains) can accommodate the resulting `amountLD` (i.e., `amountSD × decimalConversionRate` must not exceed the local limit). ::: ### 2. Adapter vs. Direct Patterns: Contract Structure & Bridge Logic #### What Is "Direct" vs. "Adapter"? - **Direct Pattern** - The **token contract itself** contains all bridge logic (send/receive) along with standard token functions (mint, burn, transfer). - When a user initiates a cross-chain transfer, the token contract on the source chain invokes internal "debit" logic to burn tokens, packages the message, and sends it through LayerZero. On the destination chain, the **same contract** (deployed there) receives the message and invokes internal "credit" logic to mint new tokens. - **Adapter Pattern** - The **token contract is separate** from the bridge logic. Instead of embedding send/receive in the token, an **adapter contract** handles all cross-chain operations. - The adapter holds (locks) user tokens (or has burn and mint roles, e.g., "Mint and Burn Adapter") and communicates with a paired OFT contract on the destination chain, which mints/unlocks or transfers the equivalent amount to the recipient. - From the developer's perspective, the only requirement is that the adapter exists as a standalone contract; the original token contract remains unaware of LayerZero or cross-chain flows. #### Key Distinctions - **Separate vs. Combined** - **Direct:** Token + bridge = single deployable. - **Adapter:** Token = unmodified existing contract; Bridge logic = standalone adapter contract. - **Mint And Burn Adapter Example** - Even though it uses mint/burn semantics, it is still an "Adapter" because the **adapter contract**, not the token contract itself, contains all LayerZero business logic. - The adapter delegates calls to mint or burn on a "wrapper" token or calls an interface on the underlying token, separating concerns without requiring the original token code to change. - **User/Integrator Perspective** - **No Difference in UX:** Users call a standard `send` function (or "transfer" wrapper) without caring whether the token is Direct or Adapter. - **Meshability:** Any two OFT-enabled contracts (Direct or Adapter) on different chains can interoperate. This means liquidity can span adapters and direct tokens seamlessly, making the system truly omnichain. #### Implications for Asset Issuers - **Direct Pattern Suits New Tokens** - When launching a brand-new token, embedding OFT logic directly can save on contract count and gas. - Simplifies deployment paths since your token and cross-chain logic are co-located. - **Adapter Pattern Suits Existing Tokens** - If you already have an active ERC-20 (or SPL, or Move) token with liquidity and integrations, deploying an adapter contract lets you plug into OFT without migrating your token. - The adapter can implement **mint and burn**, **lock and unlock**, or any hybrid, as long as it abides by the OFT interface. - **Access Control & Governance** - **Direct Token:** You manage roles (Admin, Delegate) within a single contract. - **Adapter + Token:** You may need to coordinate roles and permissions across two deployables. ### 3. Extensibility & Composability OFT's design prioritizes flexibility and extensibility, allowing developers to customize token behavior and build complex cross-chain applications. The standard provides hooks for custom logic and supports composable transfers that can trigger additional actions on the destination chain. #### Hooks Around Debit/Credit - **Beyond Value Transfer** - Many applications require extra functionality during or after cross-chain value transfer for example: - **Protocol Fees:** Automatically deduct a small fee on each cross-chain transfer and route it to a treasury. - **Rate Limiting:** Applying a limit on the number of tokens that can be sent in a given time-interval. - **Access Control:** Enforce time-based or role-based restrictions, such as requiring KYC verification for large transfers. - **Overrideable Functions** - OFT's core `_debit` and `_credit` methods are declared `virtual` (or their equivalent in non-EVM languages), allowing developers to override them in custom subclasses/modules. - Inject additional checks or side effects (e.g., take fees off transfers, check for rate limits, or validate off-chain context) without rewriting the entire message flow. #### Composability with LayerZero Messaging - **Cross-Chain Value Transfer + Call** - You can bundle **arbitrary data** with your OFT transfer. For example, trigger a staking action on the destination chain if a recipient stakes a minimum amount, or execute a cross-chain governance vote. - The OFT contract simply forwards any extra bytes as a `composeMsg` through LayerZero's endpoint. On the destination, your custom `lzCompose(...)` hook can decode and act on that arbitrary data and token transfer. ## Security & Roles OFTs inherit LayerZero's admin/delegate role model: - **Owner** - Sets required gas limit requests for execution. - Can peer new OApp contracts or remove peers in emergencies. - **Delegate** - Configures connected chain's and messaging channel properties (e.g., Message Libraries, DVNs, and executors). - Can pause or unpause cross-chain functionality in emergencies. > **Best Practice:** Use a multisig to manage both Owner and Delegate privileges. ## Further Reading - [EVM OFT Quickstart](../../developers/evm/oft/quickstart.md) A step-by-step guide to deploying Direct or Adapter OFT contracts on Ethereum-compatible networks. - [Solana OFT Quickstart](../../developers/solana/oft/overview.md) Detailed instructions and example code for setting up an OFT program with SPL Token / Token 2022 integration. - [Aptos Move OFT Quickstart](../../developers/aptos-move/contract-modules/oft.md) In-depth documentation on Move module structure, sharedDecimals math, and composability best practices. --- --- title: Debugging Messages --- import InteractiveContract from '@site/src/components/InteractiveContract'; import EndpointV2ABI from '@site/node_modules/@layerzerolabs/lz-evm-sdk-v2/artifacts/contracts/EndpointV2.sol/EndpointV2.json'; ## Message Lifecycle Every LayerZero message goes through the following high-level steps: - **Source Block Confirmations**: The message remains pending until the source chain finalizes the required number of block confirmations. This ensures that the transaction is securely committed on the source chain. - **[DVN](../glossary.md#dvn-decentralized-verifier-network)/Verification**: Each Decentralized Verifier Network (DVN) independently verifies the message and submits an on-chain transaction attesting to its validity. - **[Committer](../glossary.md#committer)/Commit Verification**: Once all required DVN attestations are available, a Committer submits a transaction to aggregate and commit these verifications on the destination chain. This step guarantees that the message has been sufficiently validated. - **[Executor](../glossary.md#executor)/Message Execution**: Finally, an Executor submits a transaction to deliver and execute the verified message on the destination chain. ## Debugging Messages using LayerZero Scan After a LayerZero message is successfully submitted on the source chain, it can be tracked using [LayerZero Scan](https://layerzeroscan.com). OApps can monitor the full message lifecycle, including delivery status and configuration details, directly through the [LayerZero Scan](../../tools/layerzeroscan/overview). For programmatic access, the [LayerZero Scan Swagger API](../../tools/layerzeroscan/api) provides a comprehensive set of endpoints to query, track, and analyze cross-chain messages and transactions. With the LayerZero Scan API, you can retrieve messages using parameters such as transaction hash, OApp address, wallet address, status, pathwayId, GUID, and more. ## Message Statuses Overview Message status is an important indicator of what’s happening with your message. Always check the status first before diving deeper into debugging—it can save significant time. Below are the main statuses on LayerZero Scan: ### _Delivered_ The message has been successfully sent and received by the destination chain. :::info The **Delivered** status indicates that the `lzReceive` function was successfully invoked when the message arrived at the destination chain. However, in some cases, the subsequent `lzCompose` execution may fail. If there is a [Composer](../../developers/evm/composer/overview) implemented, review the `lzCompose` message status on LayerZero Scan and follow the provided instructions to [retry message](#retry-message). ::: ### _Inflight_ The message is currently being transmitted between chains and has not yet reached its destination. - If DVN verification has not yet started, verify the number of block confirmations required on the source chain (configured in the receiveConfig). DVNs will only begin verification after the source transaction has reached the configured confirmation threshold. - If the required confirmations are reached but the message remains in an inflight state, the issue may fall into one of the following categories: - One or more DVNs have not yet submitted their verification for the message. - All DVNs have submitted verifications, but the Committer has not yet aggregated and committed them on the destination chain. - The Committer has successfully committed the verifications, but the Executor has not yet executed the message. - If a pathway has [Ordered Execution](../../tools/sdks/options#orderedexecution-option) enabled, a message cannot be executed until all preceding messages have been fully verified. Check the Message Execution Options in LayerZero Scan to confirm whether the ordered execution option is set to `true` and identify the first unverified message in the sequence, as subsequent messages will not be executed until it is verified. - At the stage of pending execution, execution can also be triggered manually by calling on the Endpoint's `lzReceive` function. This call is permissionless and can be initiated by anyone. Alternatively, LayerZero Scan provides a built-in option to execute the message directly through its interface. If the message remains inflight and is not delivered within the expected timeframe, contact [community support](https://layerzero.network/community) for further assistance. ### _Failed_ The message is delivered at destination chain but the message execution failed. LayerZero Scan displays any errors encountered during message execution. If the underlying issue can be resolved, the message can then be retried through the interface. See [Message Execution](#message-execution) for more details. ### _Blocked_ The message is prevented from progressing due to configuration issues and requires manual intervention or updates to resolve. A "Blocked" message usually points to configuration issues: - **NotInitializable**: This status typically indicates that the destination OApp is either missing trusted peer settings or the pathway has not been properly initialized. Common causes: - **Incorrect peer configuration**: Ensure that `setPeer()` is correctly called on both the source and destination chains during deployment. Double-check that the address format and endpoint ID are accurate. - **Pathway not initialized correctly**: Confirm that `allowInitializePath()` is properly implemented in your OApp contract. Learn more: [allowInitializePath](../../tools/integration-checklist.md#set-peers-on-every-pathway). - **Dst OApp Not Found**: The receiver is not a valid contract. - **DVN Mismatch**: All DVN providers must be the same on source and destination. See [DVN Mismatch](../../developers/evm/configuration/dvn-executor-config#dvn-mismatch) for more details. - **Dead DVN**: This configuration includes a Dead DVN. See [Dead DVN](../../developers/evm/configuration/dvn-executor-config#dead-dvn) for more details. - **Block Confirmations Mismatch**: Outbound confirmations must be ≥ inbound confirmations. See [Block Confirmation Mismatch](../../developers/evm/configuration/dvn-executor-config#block-confirmation-mismatch) for more details. ### _Confirming_ The system is currently validating transaction finality. This status may appear at any stage of the message lifecycle and typically represents a transitional state before progressing to the next step. ### _Malformed Command_ The command is malformed. The status is only applied to lzRead message. To debug, see [Debugging Malformed or Unresolvable Commands](../../developers/evm/lzread/read-cli#debugging-malformed-or-unresolvable-commands) for more details. ### _Unresolvable Command_ The command is unresolvable. This status is only applied to lzRead message. To debug, see [Debugging Malformed or Unresolvable Commands](../../developers/evm/lzread/read-cli#debugging-malformed-or-unresolvable-commands) for more details. For Malformed Command and Unresolvable Command, an OApp must call `skip()` to unblock the message pathway. If `skip()` is not invoked, subsequent messages will not be delivered. See [Skipping Nonce](../../developers/evm/troubleshooting/debugging-messages#skipping-nonce) for more details. To troubleshoot common errors in `lzRead` messages, See [debugging](../../developers/evm/lzread/overview#debugging) for more details. ### _SIMULATION_REVERTED_ This status can be found in the LayerZero Scan API as a sub status inside the `destination` section, indicating the `lzReceive` or `lzCompose` has failed on the destination chain. ## General Debugging Steps ### If the message was not sent successfully #### Quick triage - Confirm the transaction: - Did the source chain transaction finalize? (Check explorer receipt status and logs.) - Look for packet emission - Verify whether the expected LayerZero “PacketSent” event is emitted on the source chain. - Capture context: - Source and destiantion chain - OApp addresses - send params - DVN and Exeutor Configs #### Identify the revert / error trace Run a trace (Foundry/Tenderly/Trace on explorer) and map to the failing contract and error codes. Common errors (causes & fixes): `Please set your OApp's DVNs and/or Executor` - Cause: This error occurs during `getFee`, indicating your OApp configuration is missing the required DVN and/or Executor settings. - Fix: Set valid DVNs and/or executor in the OApp configs. `InsufficientFee()` - Cause: `required.nativeFee` > `suppliedNativeFee` or `required.lzTokenFee` > `suppliedLzTokenFee`; or `msg.value` lower than the quoted amount. - Fix: Call the quote function first, pass the exact fee, and forward enough msg.value. `NativeAmountExceedsCap()` - Cause: Requested native drop on destination exceeds the configured native drop cap. - Fix: Reduce requested airdrop amount in options or raise the cap in the Executor/destination config (owner action). `InvalidWorkerOptions()` - Cause: Worker options is malformed. - Fix: Rebuild options via the Options Builders. `Unauthorized()` - Cause: This error normally occurs at the wiring step. The call is not made by the OApp or an approved delegate. - Fix: Use the permissioned wallet to sign the transactions. `Unsorted()` - Cause: DVNs array contains duplicates or is not strictly sorted. - Fix: Deduplicate and sort DVN addresses deterministically before passing; keep canonical order in code. `UnsupportedEid()` - Cause: The pathway is not connected. - Fix: Use the correct destination EID, verify chain mapping, and contact the support team if a pathway is not wired. #### Configuration & connectivity checklist - **Delegate & Ownership** - Verify the owner and delegate address - [Understand their respective permissions](../../faq#whats-the-difference-between-delegate-and-owner) - **Peers**: - peers are set on both source and destination chain - addresses & EIDs match, and in correct format - **Message Libraries**: - `sendLibrary` and `receiveLibrary` are set to expected addresses/versions. - **DVNs**: - DVN provider set(s) exist - Identical provider(s) on source and destination - Contain no LZ Dead DVNs unless intended - **Executor**: - Executor address is set as intended - Message size doesn't exceed Executor limit - Native drop amount doesn't exceed cap - **Message Exeuction Options**: - Ensure enforcedOptions and/or extraOptions is present; - Profiling destination gas for lzReceive and/or lzCompose to determine the gas units applied in the message execution options - **Connected pathways**: - Confirm whether a pathway is fully connected If these pass but the `send` still fails, simulate the send with the same params and use the error trace to narrow the root cause. ### If the message was sent Now the message is visible on LayerZero Scan. Use Scan to locate the message and walk the lifecycle. - Get transaction hash on the source chain - Start with [Message Status](#message-statuses-overview) on Scan - Statuses map directly to lifecycle stages and tell you where to focus first: - Identify which stage the message is at (decision tree) - No DVN confirmations yet? - Check source confirmations vs. threshold; verify DVN set correctly. - DVNs verified, but not committed? - Check Executor config and contact support team. - Committed, but not Executed? - If it is `orderedExecution`, inspect the first unverified prior message. - Inspect revert transaction and revert reason - Fix root cause - Retry the message - [Retry messages on EVM](../../developers/evm/troubleshooting/debugging-messages#retry-message) - [Retry messages on Solana](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oapp-solana/tasks/solana/retryPayload.ts) - `lzReceive` succeeded but `lzCompose` failed? - Inspect `lzcompose` revert reason - retry `lzCompose`. ### Retrieve Retry Parameters via LayerZero Scan API Use the LayerZero Scan API to fetch a message bundle by source tx hash and inspect the destination execution. If execution failed, the destination section includes failedTx, which typically points to an Executor alert call (e.g., `lzReceiveAlert` or `lzComposeAlert`). The alert transaction’s call data contains the parameters required to retry `lzReceive` or `lzCompose`. #### Endpoint Base URL: `https://scan.layerzero-api.com/v1` Method: `GET` `/messages/tx/{tx}` `tx`: source-chain transaction hash (hex string) #### Response Shape Each response returns a `data` array of message objects: - pathway: source/destination networks and EIDs, sender/receiver address, application information - source: transaction details and status on source chain - verification: DVN verification transactions and the committer/sealer transaction for committing verifications - destination: execution status on the target chain (including failed txs) - config: ULN configurations (Confirmations, DVNs, Executor) in effect for this pathway - status: overall message status - guid, created timestamps and updated timestamp In the API response, look for `failedTx` for the transactions that contains the eror for `lzReceive` message. Examine the `revertReason` in the destination section to identify the root cause. API Response Example in the destination section: ```json "destination": { "nativeDrop": { "status": "N/A" }, "lzCompose": { "status": "N/A" }, "failedTx": [ { "txHash": "0xa6cf8347a8679866955fbf83175ccc3191f592c27865e89ce69bf99d71542b53", "txError": "CouldNotParseError(string) 0x", "blockHash": "0xa3130ed1fbb60b45bd99638a07f415892118442e72d5be6eda58214a6a41c610", "blockNumber": 23266956, "revertReason": "0x" } ], "status": "SIMULATION_REVERTED" } ``` #### How to get retry parameters - Call `GET /messages/tx/{tx}` with the source tx hash. In `data[0].destination.failedTx`, take the `txHash` (usually an `lzReceiveAlert` or `lzComposeAlert` transaction that the executor called to signal the failure). - Fetch that destination transaction and inspect. - If revertReason is empty (0x), it commonly indicates out-of-gas or a contract-level revert without a reason string. - For custom error, decode the selector using [4byte directory](https://www.4byte.directory/). - Function selector & args of the alert call input; it embeds everything needed to re-invoke lzReceive/lzCompose - Alternatively, all the information can also be retrieved directly from the scan API. #### Skip/Clear/Burn/Nilify **`skip`**: Called by the receiver to skip verification and delivery of a nonce. **`clear`**: Called by the receiver to skip a nonce that has been verified. **`nilify`**: Called by the receiver to temporarily invalidate a nonce. `nilify` can be used to proactively invalidate maliciously generated packets from compromised DVNs. Message can be re-executed. **`burn`**: Called by the receiver to delete and skip a nonce. `burn` can be used if a faulty Security Stack commits an invalid hash to the endpoint, or if an OApp needs to clear a nilified nonce. Message can not be re-executed. See: [Skip/Clear/Burn/Nilify on EVM](../../developers/evm/troubleshooting/debugging-messages#skipping-nonce) and [Skip/Clear/Burn/Nilify on Solana](../../tools/sdks/solana-sdk#skip-a-message) for full semantics and usage. --- --- sidebar_label: Start Here title: 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

:::info To find all of LayerZero's contract standards visit the [**LayerZero Devtools**](https://github.com/LayerZero-Labs/devtools). To see the core protocol contracts, visit the [**LayerZero V2**](https://github.com/LayerZero-Labs/layerzero-v2) repository. ::: You can also ask for help or follow development in the [Discord](https://layerzero.network/community). --- --- title: Interactive Contract Playground sidebar_label: Contract Playground --- import InteractiveContract from '@site/src/components/InteractiveContract'; import ContractInterface from '@site/src/components/ContractInterface'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import EndpointV2ABI from '@site/node_modules/@layerzerolabs/lz-evm-sdk-v2/artifacts/contracts/EndpointV2.sol/EndpointV2.json'; import OFTABI from '@site/docgen-out/oft-evm/abis/implementations/OFT.sol.json'; import OFTAdapterABI from '@site/docgen-out/oft-evm/abis/implementations/OFTAdapter.sol.json'; import OAppABI from '@site/docgen-out/oapp-evm/abis/implementations/OApp.sol.json'; import OAppReadABI from '@site/docgen-out/oapp-evm/abis/implementations/OAppRead.sol.json'; # Interactive Contract Playground Test LayerZero contracts directly from your browser. No coding required. Explore key application functions for message fee calculation, sending, receiving, configuration, and state management. This page focuses on methods relevant to building applications and does not include worker-related functions. :::tip Real On-Chain Methods All functions shown in this playground are **real methods** available in the LayerZero contracts today: - **Endpoint Contract**: [Source Code](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/evm/protocol/contracts) - **OFT Contract**: [Source Code](https://github.com/LayerZero-Labs/devtools/tree/main/packages/oft-evm) We only document OApp-relevant instructions, excluding admin-only functions. State variables are clearly marked as direct account data reads, not instructions. ::: ## LayerZero EndpointV2 The main entry point for all cross-chain messaging operations. This contract handles message routing, fee calculation, and configuration management. ### Message Routing Core functions for sending and receiving messages between smart contracts. #### quote() - Get Fee Estimates #### send() - Send Messages #### lzReceive() - Receive Messages #### sendCompose() - Send Compose Messages #### lzCompose() - Execute Compose Messages ### Configuration Management Functions for setting custom verification, execution, and pathway management. #### eid() - Get Endpoint ID #### isRegisteredLibrary() - Check Library Registration #### receiveLibraryTimeout() - Get Library Timeout #### setDelegate() - Set Delegate Address #### setSendLibrary() - Configure Send Library #### setReceiveLibrary() - Configure Receive Library #### setConfig() - Set Configuration Parameters ### Message Recovery & Security Functions for handling message exceptions, security threats, and recovery scenarios. #### clear() - Clear Stored Message #### burn() - Permanently Block Message #### skip() - Skip Message Nonce #### nilify() - Mark Message as Nil ### Status Checks Functions for querying current configuration settings, library assignments, nonce tracking, and message states. #### getConfig() - Check Configuration #### delegates() - Check Delegate Address #### getSendLibrary() - Get Send Library #### getReceiveLibrary() - Get Receive Library #### inboundNonce() - Get Processed Nonce #### lazyInboundNonce() - Get Lazy Nonce #### initializable() - Check Message Initialization #### verifiable() - Check Message Verification #### inboundPayloadHash() - Get Message Payload Hash #### nextGuid() - Get Next Message GUID #### composeQueue() - Check Compose Message Queue #### isSendingMessage() - Check Send State #### getSendContext() - Get Current Send Context ### Events Key events emitted by the EndpointV2 contract. #### PacketSent - Message Sent Event #### PacketVerified - Message Verified Event #### PacketDelivered - Message Delivered Event #### ComposeSent - Compose Message Queued Event #### ComposeDelivered - Compose Message Delivered Event #### DelegateSet - Delegate Configuration Event #### SendLibrarySet - Send Library Configuration Event #### ReceiveLibrarySet - Receive Library Configuration Event #### ReceiveLibraryTimeoutSet - Library Timeout Configuration Event #### InboundNonceSkipped - Nonce Skip Event #### PacketNilified - Message Nilified Event #### PacketBurnt - Message Burnt Event ### Errors #### LZ_InsufficientFee - Insufficient Fee #### LZ_InvalidNonce - Invalid Nonce #### LZ_Unauthorized - Unauthorized Access #### LZ_SendReentrancy - Send Reentrancy Detected #### Key Functions to Try: - **Messaging Operations:** - `quote()` - Get fee estimates for cross-chain messages - `send()` - Send messages to other chains - `lzReceive()` - Receive messages from other chains - `sendCompose()` - Queue compose messages - `lzCompose()` - Execute compose messages - **Configuration Management:** - `setDelegate()` - Assign configuration permissions - `setSendLibrary()` - Choose send message library - `setReceiveLibrary()` - Choose receive message library - `setConfig()` - Set library-specific parameters - **Message Recovery & Security:** - `burn()` - Permanently block malicious messages - `skip()` - Skip flagged message nonces - `nilify()` - Mark messages for re-verification - `clear()` - Clear verified but unexecuted messages - **Status Checks:** - `getConfig()` - Check current configurations - `delegates()` - View current delegate address - `getSendLibrary()` - Check send library for endpoint - `getReceiveLibrary()` - Check receive library for endpoint - `inboundNonce()` - Get highest processed message nonce - `lazyInboundNonce()` - Get highest verified/skipped nonce - `nextGuid()` - Get next message GUID - `composeQueue()` - Check compose queue - `isSendingMessage()` - Check send state - `getSendContext()` - Get send context ## Omnichain Application (OApp) The foundation for building any cross-chain application. **OApp** provides the core messaging infrastructure for a smart contract interacting with the **EndpointV2**. ### Core Information #### oAppVersion() - Get OApp Version #### endpoint() - Get Endpoint Address ### Peer Configuration #### peers() - Get Remote Peer Address #### setPeer() - Connect Remote Chains #### setDelegate() - Set Configuration Delegate ### Message Reception #### allowInitializePath() - Check Path Initialization #### nextNonce() - Get Next Message Nonce #### lzReceive() - Receive Cross-Chain Messages ### Composability #### isComposeMsgSender() - Verify Compose Sender ### Events and Errors Key events and errors emitted by the OApp contract. #### Events #### PeerSet - Peer Configuration Updated #### Errors #### OnlyPeer - Unauthorized Peer Message #### NoPeer - Missing Peer Configuration #### InvalidEndpointCall - Invalid Endpoint Call #### InvalidDelegate - Invalid Delegate Configuration #### NotEnoughNative - Insufficient Native Fee #### OnlyEndpoint - Unauthorized Endpoint Call #### LzTokenUnavailable - LayerZero Token Not Available #### Key Functions to Try: - **Core Information:** - `oAppVersion()` - Get OApp version information - `endpoint()` - Get connected endpoint address - **Peer Configuration:** - `setPeer()` - Connect to remote chains - `peers()` - Check connected chains - `setDelegate()` - Set configuration delegate - **Message Reception:** - `lzReceive()` - Receive cross-chain messages - `allowInitializePath()` - Check path initialization - `nextNonce()` - Get message ordering info - **Composability:** - `isComposeMsgSender()` - Verify compose sender #### Tips: - Peers must be set before messaging - Nonces ensure ordered delivery - Options control execution parameters ## Omnichain Application Read (OAppRead) **OAppRead** extends the standard **OApp** with LayerZero Read functionality, enabling cross-chain data reading capabilities. It includes all standard methods plus the read channel configuration. #### setReadChannel() - Configure Read Channel #### Key Functions to Try: - `setReadChannel()` - Configure read channel for cross-chain data reading - Plus all standard OApp functions listed above ## Omnichain Fungible Token (OFT) **OFT** inherits from **OApp**, providing all cross-chain messaging capabilities plus token-specific functionality. Create tokens that work seamlessly across multiple blockchains while maintaining a unified supply. ### Send Tokens #### quoteSend() - Get Transfer Fees #### quoteOFT() - Get Detailed Transfer Quote #### send() - Transfer Tokens ### Token Details #### sharedDecimals() - Get Shared Decimals #### approvalRequired() - Check Approval Requirement #### oftVersion() - Get OFT Version #### token() - Get Underlying Token Address #### decimalConversionRate() - Get Decimal Conversion Factor ### Management Functions #### owner() - Get Current Owner #### transferOwnership() - Transfer Contract Ownership #### renounceOwnership() - Renounce Ownership #### setPeer() - Connect to Remote OFTs #### setEnforcedOptions() - Configure Message Options #### setMsgInspector() - Set Message Inspector ### Events Key events emitted by the OFT contract. #### OFTSent - Token Transfer Sent #### OFTReceived - Token Transfer Received #### Transfer - Standard ERC20 Transfer ### Errors #### InvalidLocalDecimals - Invalid Decimal Configuration #### SlippageExceeded - Transfer Slippage Too High #### AmountSDOverflowed - Shared Decimal Overflow #### Key Functions to Try: - **Transfer Operations:** - `quoteSend()` - Get transfer fee estimates - `send()` - Transfer tokens cross-chain - `quoteOFT()` - Get comprehensive transfer quotes - **Token Information:** - `sharedDecimals()` - Check decimal configuration - `approvalRequired()` - Check if approval is needed - `oftVersion()` - Get OFT implementation version - `token()` - Get underlying token address - `decimalConversionRate()` - Get decimal conversion factor - **Management Functions:** - `owner()` - Check current contract owner - `setPeer()` - Connect to OFTs on other chains - `setEnforcedOptions()` - Configure security parameters - `setMsgInspector()` - Set message inspector - `transferOwnership()` - Transfer contract ownership - `renounceOwnership()` - Permanently remove ownership #### Tips: - Always call `quoteSend()` before `send()` to get accurate fees - The `minAmountLD` parameter provides slippage protection - Shared decimals (typically 6-8) may differ from local decimals (e.g., 18 for most ERC20s) - `approvalRequired()` returns false for OFT and true for OFTAdapter - Use `quoteOFT()` for detailed information including transfer limits and fee breakdowns - OFTAdapter requires approval on the underlying token before sending - Must call `setPeer()` to connect OFTs on different chains before transfers - Only the contract owner can call management functions - Use `setEnforcedOptions()` to enforce minimum gas limits for security - `renounceOwnership()` is irreversible - use with extreme caution For complete contract documentation including all functions, events, and technical details: - [Contract Standards Overview](/v2/developers/evm/overview) - [OApp Technical Reference](/v2/concepts/technical-reference/oapp-reference) - [OFT Technical Reference](/v2/concepts/technical-reference/oft-reference) - [Protocol Contracts](/v2/developers/evm/protocol-contracts-overview) :::info Contract ABIs shown here are from the latest deployment. Always verify addresses and ABIs for your specific use case. ::: --- --- 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** using LayerZero contract standards. - **Send native gas tokens** as part of your message's execution options. - **Utilize a native bridge** built on top of LayerZero (e.g., Stargate). ## Building Your Own Omnichain Token The **Omnichain Fungible Token (OFT) Standard** and **Omnichain Non-Fungible Token (ONFT) Standard** are ideal for creating tokens that exist on multiple chains. These standards allow tokens to be transferred across multiple blockchains without asset wrapping or middlechains, ensuring consistency and interoperability for holders. For new tokens, inherit from `OFT` or `ONFT`. For existing tokens, use `OFTAdapter` or `ONFTAdapter`. To build a token using `OFT` or `ONFT`, you need to deploy the standard contracts on each chain where the token you own will or currently exists. Read the [OFT Quickstart](../oft/quickstart.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](../../../concepts/protocol/transaction-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) for transferring native assets between chains. **Functionality:** Stargate and similar platforms handle the creation of asset pools, facilitating the easy movement of native assets across multiple chains. **Advantages:** This option enables you to utilize existing liquidity and composability with your smart contracts without the need for deploying the OFT Standards directly. Read the [Stargate Docs](https://stargateprotocol.gitbook.io/stargate/v2-developer-docs) for how to transfer and swap 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 OApp Quickstart sidebar_label: Omnichain Application (OApp) --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; The **OApp standard** lets your contract send and receive arbitrary _messages_ across chains. With OApp, you can update on-chain state on one network and trigger custom business logic on another. ![OApp Example](/img/learn/ABLight.svg#gh-light-mode-only) ![OApp Example](/img/learn/ABDark.svg#gh-dark-mode-only) `OApp.sol` implements the core interface for calling LayerZero's Endpoint V2 on EVM chains. It also provides hookable `_lzSend` and `_lzReceive` methods so you can inject your own business logic: ![OApp Inheritance](/img/oapp-inheritance-light.svg#gh-light-mode-only) ![OApp Inheritance](/img/oapp-inheritance-dark.svg#gh-dark-mode-only) :::tip If your use case only involves cross-chain token transfers, consider inheriting the [**OFT Standard**](../oft/quickstart.md) instead of OApp. ::: ## Installation To start using LayerZero contracts in a new project, use the LayerZero CLI tool, [**create-lz-oapp**](../../../get-started/create-lz-oapp/start.md). The CLI tool is an npx package that allows developers to create any omnichain application in <4 minutes! Get started by running the following from your command line: ```bash npx create-lz-oapp@latest --example oapp ``` This will create an example repository containing both the Hardhat and Foundry frameworks, LayerZero development utilities, as well as the **OApp contract package** pre-installed. To use LayerZero contracts in an existing project, you can install the **OApp package** directly: ```bash npm install @layerzerolabs/oapp-evm ``` ```bash yarn add @layerzerolabs/oapp-evm ``` ```bash pnpm add @layerzerolabs/oapp-evm ``` ```bash forge init ``` ```bash forge install layerzero-labs/devtools forge install layerzero-labs/LayerZero-v2 forge install OpenZeppelin/openzeppelin-contracts git submodule add https://github.com/GNSPS/solidity-bytes-utils.git lib/solidity-bytes-utils ``` Then add to your `foundry.toml` under `[profile.default]`: ```toml [profile.default] src = "src" out = "out" libs = ["lib"] remappings = [ '@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/', '@layerzerolabs/lz-evm-protocol-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/protocol', '@layerzerolabs/lz-evm-messagelib-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/messagelib', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', 'solidity-bytes-utils/=lib/solidity-bytes-utils/', ] ``` :::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", } ``` ::: ## Custom OApp Contract To build your own cross-chain application, inherit from `OApp.sol` and implement two key pieces: 1. **Send business logic**: how you encode and dispatch a custom `_message` on the source 2. **Receive business logic**: how you decode and apply an incoming `_message` on the destination Below is a complete example skeleton structure showing: - A constructor wiring in the local Endpoint and owner - A `sendString(...)` function that updates state, encodes a string, and calls `_lzSend(...)` - An override of `_lzReceive(...)` that decodes the string and applies business logic - (Optional) a `quoteSendString(...)` function to query the fee details needed to call `sendString(...)` ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { OApp, Origin, MessagingFee } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; contract MyOApp is OApp, OAppOptionsType3 { /// @notice Last string received from any remote chain string public lastMessage; /// @notice Msg type for sending a string, for use in OAppOptionsType3 as an enforced option uint16 public constant SEND = 1; /// @notice Initialize with Endpoint V2 and owner address /// @param _endpoint The local chain's LayerZero Endpoint V2 address /// @param _owner The address permitted to configure this OApp constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) Ownable(_owner) {} // ────────────────────────────────────────────────────────────────────────────── // 0. (Optional) Quote business logic // // Example: Get a quote from the Endpoint for a cost estimate of sending a message. // Replace this to mirror your own send business logic. // ────────────────────────────────────────────────────────────────────────────── /** * @notice Quotes the gas needed to pay for the full omnichain transaction in native gas or ZRO token. * @param _dstEid Destination chain's endpoint ID. * @param _string The string to send. * @param _options Message execution options (e.g., for sending gas to destination). * @param _payInLzToken Whether to return fee in ZRO token. * @return fee A `MessagingFee` struct containing the calculated gas fee in either the native token or ZRO token. */ function quoteSendString( uint32 _dstEid, string calldata _string, bytes calldata _options, bool _payInLzToken ) public view returns (MessagingFee memory fee) { bytes memory _message = abi.encode(_string); // combineOptions (from OAppOptionsType3) merges enforced options set by the contract owner // with any additional execution options provided by the caller fee = _quote(_dstEid, _message, combineOptions(_dstEid, SEND, _options), _payInLzToken); } // ────────────────────────────────────────────────────────────────────────────── // 1. Send business logic // // Example: send a simple string to a remote chain. Replace this with your // own state-update logic, then encode whatever data your application needs. // ────────────────────────────────────────────────────────────────────────────── /// @notice Send a string to a remote OApp on another chain /// @param _dstEid Destination Endpoint ID (uint32) /// @param _string The string to send /// @param _options Execution options for gas on the destination (bytes) function sendString(uint32 _dstEid, string calldata _string, bytes calldata _options) external payable { // 1. (Optional) Update any local state here. // e.g., record that a message was "sent": // sentCount += 1; // 2. Encode any data structures you wish to send into bytes // You can use abi.encode, abi.encodePacked, or directly splice bytes // if you know the format of your data structures bytes memory _message = abi.encode(_string); // 3. Call OAppSender._lzSend to package and dispatch the cross-chain message // - _dstEid: remote chain's Endpoint ID // - _message: ABI-encoded string // - _options: combined execution options (enforced + caller-provided) // - MessagingFee(msg.value, 0): pay all gas as native token; no ZRO // - payable(msg.sender): refund excess gas to caller // // combineOptions (from OAppOptionsType3) merges enforced options set by the contract owner // with any additional execution options provided by the caller _lzSend( _dstEid, _message, combineOptions(_dstEid, SEND, _options), MessagingFee(msg.value, 0), payable(msg.sender) ); } // ────────────────────────────────────────────────────────────────────────────── // 2. Receive business logic // // Override _lzReceive to decode the incoming bytes and apply your logic. // The base OAppReceiver.lzReceive ensures: // • Only the LayerZero Endpoint can call this method // • The sender is a registered peer (peers[srcEid] == origin.sender) // ────────────────────────────────────────────────────────────────────────────── /// @notice Invoked by OAppReceiver when EndpointV2.lzReceive is called /// @dev _origin Metadata (source chain, sender address, nonce) /// @dev _guid Global unique ID for tracking this message /// @param _message ABI-encoded bytes (the string we sent earlier) /// @dev _executor Executor address that delivered the message /// @dev _extraData Additional data from the Executor (unused here) function _lzReceive( Origin calldata /*_origin*/, bytes32 /*_guid*/, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { // 1. Decode the incoming bytes into a string // You can use abi.decode, abi.decodePacked, or directly splice bytes // if you know the format of your data structures string memory _string = abi.decode(_message, (string)); // 2. Apply your custom logic. In this example, store it in `lastMessage`. lastMessage = _string; // 3. (Optional) Trigger further on-chain actions. // e.g., emit an event, mint tokens, call another contract, etc. // emit MessageReceived(_origin.srcEid, _string); } } ``` ### Constructor - Pass the Endpoint V2 address and owner address into the base contracts. - `OApp(_endpoint, _owner)` binds your contract to the local LayerZero Endpoint V2 and registers the owner as the delegate, making it the only address that can change configurations (such as libraries, DVNs, and Executors. - `Ownable(_owner)` makes `_owner` the only address that can change configurations (such as peers, enforced options, and delegate). - After deployment, the owner can call: - `setConfig(...)` to adjust library or DVN parameters - `setSendLibrary(...)` and `setReceiveLibrary(...)` to override default libraries - `setPeer(...)` to whitelist remote OApp addresses - `setDelegate(...)` to assign a different delegate address :::info A full overview of how to use these adminstrative functions can be found below under [**Deployment & Wiring**](#deployment-and-wiring). ::: ### sendString(...) 1. **Update local state (optional)** - Before sending, you might update a counter, lock tokens, or perform any on-chain action specific to your app. 2. **Encode the message** - Use `abi.encode(_message)`, `abi.encodePacked(_message)`, or manual byte shifting/offsets to turn the string into a `bytes` array. LayerZero [packets](../../../concepts/protocol/packet.md#packet-endpoint) carry raw `bytes`, so you must encode any data type into bytes first. 3. **Call `_lzSend(...)`** - `_dstEid` is the destination chain's [Endpoint ID](/v2/concepts/glossary#endpoint-id). LayerZero uses numeric IDs (e.g., `30101` for Ethereum, `30168` for Solana). - `_message` is the ABI-encoded string (`bytes memory`). - `_options` is a `bytes` array specifying gas or executor instructions for the destination. For example, an `ExecutorLzReceiveOption` tells the destination how much gas to allocate to your receive call. - `MessagingFee(msg.value, 0)` pays fees in native gas. If you wanted to pay in ZRO tokens, set the second field instead. - `payable(msg.sender)` specifies the refund address for any unused gas. This can be any address (EOA or contract), but if it's a contract, the contract must have a fallback function to receive the refund. ### \_lzReceive(...) 1. **Endpoint verification** - Only the LayerZero Endpoint V2 contract can invoke this function. The base `OAppReceiver` enforces that. - The call succeeds only if `_origin.sender == peers[_origin.srcEid]`. In other words, the sender's address must match the registered peer for that source chain. 2. **Decode the incoming bytes** - Use `abi.decode(_message, (string))` to extract the original string. If you sent a different data type (e.g., a struct), decode with the matching types. - Alternatively, you can use `abi.decodePacked()` for packed encoding, or manually splice bytes from specific offsets if you know the exact format of your data structures. 3. **Apply your business logic** - In this example, we store the decoded string in `lastMessage`. - You could instead: - Emit an event (e.g., `emit MessageReceived(_origin.srcEid, decoded)`) - Mint or unlock tokens based on the message - Call another contract to trigger a downstream workflow :::tip Always include all five parameters (`_origin`, `_guid`, `_message`, `_executor`, `_extraData`) in your override. Even if you only use `_message`, matching the function signature ensures the Endpoint can call your method correctly. ::: ### (Optional) quoteSendString(...) You can optionally call the internal `OAppSender._quote(...)` method in a public function to provide accurate estimation for the gas cost of calling `MyOApp.sendString(...)`. The internal `_quote` method queries the send library selected by the OApp and asks the workers (DVNs and Executor) for fee details for the given encoded message: 1. **Fee estimation before sending** - Before calling `sendString(...)`, you need to know how much native gas (or ZRO tokens) to send with your transaction. The `quoteSendString(...)` function provides this cost estimate. 2. **Mirrors send logic** - The quote function uses the same message encoding (`abi.encode(_string)`) and option handling (`combineOptions(_dstEid, SEND, _options)`) as the actual send function, ensuring accurate fee estimates. 3. **Enforced options integration** - By inheriting `OAppOptionsType3` and using `combineOptions(...)`, the quote function automatically includes any enforced options that the contract owner has configured for the `SEND` message type, plus any additional options provided by the caller. 4. **Flexible payment options** - The `_payInLzToken` parameter lets you choose whether to pay fees in the native gas token of the source chain or in ZRO tokens. **Example usage:** ```solidity // Get fee estimate first MessagingFee memory fee = myOApp.quoteSendString( dstEid, "Hello World", "0x", // no additional options false // pay in native gas ); // Then send with the estimated fee myOApp.sendString{value: fee.nativeFee}( dstEid, "Hello World", "0x" ); ``` --- This section shows you exactly: - **Where** to update or check local state before sending - **How** to encode and send your application data over LayerZero - **Where** to decode incoming data and execute your custom logic Replace the `string` examples with whatever data structures and state changes your application requires. ## Deployment and Wiring After you finish writing and testing your `MyOApp` contract, follow these steps to deploy it on each network and wire up the messaging stack. :::tip We **strongly recommend** using the LayerZero CLI tool to manage your configurations. Our config generator simplifies access to all available deployments across networks and is the preferred method for cross-chain messaging. See the [**CLI Guide**](../../../get-started/create-lz-oapp/start.md) for examples and how to use it in your project. ::: ### 1. Deploy Your OApp Contract Deploy `MyOApp` on each chain using either the LayerZero CLI (recommended) or manual deployment scripts. After running `pnpm compile` at the root level of your example repo, you can deploy your contracts. #### Network Configuration Before using the CLI, you'll need to configure your networks in `hardhat.config.ts` with LayerZero Endpoint IDs and declare an RPC URL in your `.env` or directly in the config file: ```typescript // hardhat.config.ts import { EndpointId } from '@layerzerolabs/lz-definitions' // ... rest of hardhat config omitted for brevity networks: { 'optimism-sepolia-testnet': { // highlight-next-line eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, 'avalanche-fuji-testnet': { // highlight-next-line eid: EndpointId.AVALANCHE_V2_TESTNET, url: process.env.RPC_URL_FUJI || 'https://avalanche-fuji.drpc.org', accounts, }, 'arbitrum-sepolia-testnet': { // highlight-next-line eid: EndpointId.ARBSEP_V2_TESTNET, url: process.env.RPC_URL_ARB_SEPOLIA || 'https://arbitrum-sepolia.gateway.tenderly.co', accounts, }, } ``` :::info The key addition to a standard `hardhat.config.ts` is the inclusion of LayerZero Endpoint IDs (`eid`) for each network. Check the [Deployments](../../../deployments/deployed-contracts.md) section for all available endpoint IDs. ::: The LayerZero CLI provides automated deployment with built-in endpoint detection based on your `hardhat.config.ts` networks object: ```bash # Deploy using interactive prompts npx hardhat lz:deploy ``` The CLI will prompt you to: 1. **Select chains to deploy to:** ```bash ? Which networks would you like to deploy? › ◉ fuji ◉ amoy ◉ sepolia ``` 2. **Choose deploy script tags:** ```bash ? Which deploy script tags would you like to use? › MyOApp ``` 3. **Confirm deployment:** ```bash ✔ Do you want to continue? … yes Network: amoy Deployer: 0x0000000000000000000000000000000000000000 Network: sepolia Deployer: 0x0000000000000000000000000000000000000000 Deployed contract: MyOApp, network: amoy, address: 0x0000000000000000000000000000000000000000 Deployed contract: MyOApp, network: sepolia, address: 0x0000000000000000000000000000000000000000 ``` The CLI automatically: - Detects the correct LayerZero Endpoint V2 address for each chain - Deploys your OApp contract with proper constructor arguments - Generates deployment artifacts in `./deployments/` folder - Creates network-specific deployment files (e.g., `deployments/sepolia/MyOApp.json`) For manual deployment using Foundry, create a deployment script that handles endpoint addresses: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { MyOApp } from "../contracts/MyOApp.sol"; contract DeployOApp is Script { function run() external { // Replace these env vars with your own values address endpoint = vm.envAddress("ENDPOINT_ADDRESS"); address owner = vm.envAddress("OWNER_ADDRESS"); vm.startBroadcast(vm.envUint("PRIVATE_KEY")); MyOApp oapp = new MyOApp(endpoint, owner); vm.stopBroadcast(); console.log("MyOApp deployed to:", address(oapp)); } } ``` Run the deployment script: ```bash # Deploy to testnet forge script script/DeployOApp.s.sol --rpc-url $RPC_URL --broadcast --verify # Deploy to multiple chains forge script script/DeployOApp.s.sol --rpc-url $ETHEREUM_RPC --broadcast --verify forge script script/DeployOApp.s.sol --rpc-url $POLYGON_RPC --broadcast --verify ``` You'll need to set the correct LayerZero Endpoint V2 addresses for each chain in your environment variables. Check the [Deployments](../../../deployments/deployed-contracts.md) section for endpoint addresses. ### 2. Wire Messaging Libraries and Configurations Once your contracts are on-chain, you must set up send/receive libraries and DVN/Executor settings so cross-chain messages flow correctly. The LayerZero CLI automatically handles all wiring via a single configuration file and command: #### Configuration File In your project root, you can find a `layerzero.config.ts` file: ```typescript import {EndpointId} from '@layerzerolabs/lz-definitions'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; import {OAppEnforcedOption, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; // This contract object defines the OApp deployment on Optimism Sepolia testnet // The config references the contract deployment from your ./deployments folder const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOApp', }; const avalancheContract: OmniPointHardhat = { eid: EndpointId.AVALANCHE_V2_TESTNET, contractName: 'MyOApp', }; const arbitrumContract: OmniPointHardhat = { eid: EndpointId.ARBSEP_V2_TESTNET, contractName: 'MyOApp', }; // For this example's simplicity, we will use the same enforced options values for sending to all chains // For production, you should ensure `gas` is set to the correct value through profiling the gas usage of calling OApp._lzReceive(...) on the destination chain // To learn more, read https://docs.layerzero.network/v2/concepts/applications/oapp-standard#execution-options-and-enforced-settings const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 80000, value: 0, }, ]; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> Avalanche // Optimism <-> Arbitrum // Avalanche <-> Arbitrum // With the config generator, pathways declared are automatically bidirectional // i.e. if you declare A,B there's no need to declare B,A const pathways: TwoWayConfig[] = [ [ optimismContract, // Chain A contract avalancheContract, // Chain B contract [['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [1, 1], // [A to B confirmations, B to A confirmations] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions ], [ optimismContract, // Chain A contract arbitrumContract, // Chain C contract [['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [1, 1], // [A to B confirmations, B to A confirmations] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain C enforcedOptions, Chain A enforcedOptions ], [ avalancheContract, // Chain B contract arbitrumContract, // Chain C contract [['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [1, 1], // [A to B confirmations, B to A confirmations] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain C enforcedOptions, Chain B enforcedOptions ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [ {contract: optimismContract}, {contract: avalancheContract}, {contract: arbitrumContract}, ], connections, }; } ``` Make sure your contract object's `contractName` matches the named deployment file for the network under `./deployments/`. #### Wire Everything Run a single command to configure all pathways: ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` This automatically handles: - Fetching the necessary contract addresses for each network from metadata - Setting send and receive libraries - Configuring DVNs and Executors - Setting up peers between contracts - Applying enforced options - All bidirectional pathways in your config For manual configuration using Foundry scripts, follow these steps: #### Environment Setup Here's a comprehensive `.env.example` file showing all the environment variables needed for the different configuration scripts: ```bash # Common variables used across scripts ENDPOINT_ADDRESS=0x... # LayerZero Endpoint V2 address OAPP_ADDRESS=0x... # Your OApp contract address SIGNER=0x... # Address with permissions to configure/send # Library Configuration (SetLibraries.s.sol) SEND_LIB_ADDRESS=0x... # SendUln302 address RECEIVE_LIB_ADDRESS=0x... # ReceiveUln302 address DST_EID=30101 # Destination chain EID SRC_EID=30110 # Source chain EID GRACE_PERIOD=0 # Grace period for library switch (0 for immediate) # Send Config (SetSendConfig.s.sol) SOURCE_ENDPOINT_ADDRESS=0x... # Chain A Endpoint address SENDER_OAPP_ADDRESS=0x... # OApp on Chain A REMOTE_EID=30101 # Endpoint ID for Chain B # Peer Configuration (SetPeers.s.sol) CHAIN1_EID=30101 # First chain EID CHAIN1_PEER=0x... # OApp address on first chain CHAIN2_EID=30110 # Second chain EID CHAIN2_PEER=0x... # OApp address on second chain CHAIN3_EID=30111 # Third chain EID CHAIN3_PEER=0x... # OApp address on third chain # Message Sending (SendMessage.s.sol) MESSAGE="Hello World" # Message to send cross-chain ``` #### 2.1 Set Send and Receive Libraries 1. **Choose your libraries** (addresses of deployed MessageLib contracts). For standard cross-chain messaging, you should use `SendUln302.sol` for `setSendLibrary(...)` and `ReceiveUln302.sol` for `setReceiveLibrary(...)`. You can find the deployments for these contracts under the [Deployments](../../../deployments/deployed-contracts.md) section. 2. Call `setSendLibrary(oappAddress, dstEid, sendLibAddress)` on the Endpoint. 3. Call `setReceiveLibrary(oappAddress, srcEid, receiveLibAddress, gracePeriod)` on the Endpoint. ```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"; /// @title LayerZero Library Configuration Script /// @notice Sets up send and receive libraries for OApp messaging contract SetLibraries is Script { function run() external { // Load environment variables address endpoint = vm.envAddress("ENDPOINT_ADDRESS"); // LayerZero Endpoint address address oapp = vm.envAddress("OAPP_ADDRESS"); // Your OApp contract address address signer = vm.envAddress("SIGNER"); // Address with permissions to configure // Library addresses address sendLib = vm.envAddress("SEND_LIB_ADDRESS"); // SendUln302 address address receiveLib = vm.envAddress("RECEIVE_LIB_ADDRESS"); // ReceiveUln302 address // Chain configurations uint32 dstEid = uint32(vm.envUint("DST_EID")); // Destination chain EID uint32 srcEid = uint32(vm.envUint("SRC_EID")); // Source chain EID uint32 gracePeriod = uint32(vm.envUint("GRACE_PERIOD")); // Grace period for library switch vm.startBroadcast(signer); // Set send library for outbound messages ILayerZeroEndpointV2(endpoint).setSendLibrary( oapp, // OApp address dstEid, // Destination chain EID sendLib // SendUln302 address ); // Set receive library for inbound messages ILayerZeroEndpointV2(endpoint).setReceiveLibrary( oapp, // OApp address srcEid, // Source chain EID receiveLib, // ReceiveUln302 address gracePeriod // Grace period for library switch ); vm.stopBroadcast(); } } ``` You would need to set up your `.env` file with the appropriate values: ```env ENDPOINT_ADDRESS=0x... OAPP_ADDRESS=0x... SIGNER=0x... SEND_LIB_ADDRESS=0x... # SendUln302 address RECEIVE_LIB_ADDRESS=0x... # ReceiveUln302 address DST_EID=30101 SRC_EID=30110 GRACE_PERIOD=0 # Set to 0 for immediate switch, or block number for gradual migration ``` #### 2.2 Set Send Config and Receive Config If you need non-default DVN or Executor settings (block confirmations, required DVNs, max message size, etc.), call `setConfig(...)` next. To see defaults, use `getConfig(...)`. **Send Config (A → B):** The send config is set on the source chain (Chain A) and applies to messages being sent from Chain A to Chain B. This config determines the DVN and Executor settings for outbound messages leaving Chain A and destined for Chain B. You must call `setConfig` on the Endpoint contract on Chain A, specifying the remote Endpoint ID for Chain B and the appropriate SendLib address for the A → B pathway. ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { ILayerZeroEndpointV2, SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; import { ExecutorConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBase.sol"; /// @title LayerZero Send Configuration Script (A → B) /// @notice Defines and applies ULN (DVN) + Executor configs for cross‑chain messages sent from Chain A to Chain B via LayerZero Endpoint V2. contract SetSendConfig is Script { uint32 constant EXECUTOR_CONFIG_TYPE = 1; uint32 constant ULN_CONFIG_TYPE = 2; /// @notice Broadcasts transactions to set both Send ULN and Executor configurations for messages sent from Chain A to Chain B function run() external { address endpoint = vm.envAddress("SOURCE_ENDPOINT_ADDRESS"); // Chain A Endpoint address oapp = vm.envAddress("SENDER_OAPP_ADDRESS"); // OApp on Chain A uint32 eid = uint32(vm.envUint("REMOTE_EID")); // Endpoint ID for Chain B address sendLib = vm.envAddress("SEND_LIB_ADDRESS"); // SendLib for A → B address signer = vm.envAddress("SIGNER"); /// @notice ULNConfig defines security parameters (DVNs + confirmation threshold) for A → B /// @notice Send config requests these settings to be applied to the DVNs and Executor for messages sent from A to B /// @dev 0 values will be interpretted as defaults, so to apply NIL settings, use: /// @dev uint8 internal constant NIL_DVN_COUNT = type(uint8).max; /// @dev uint64 internal constant NIL_CONFIRMATIONS = type(uint64).max; UlnConfig memory uln = UlnConfig({ confirmations: 15, // minimum block confirmations required on A before sending to B requiredDVNCount: 2, // number of DVNs required optionalDVNCount: type(uint8).max, // optional DVNs count, uint8 optionalDVNThreshold: 0, // optional DVN threshold requiredDVNs: [address(0x1111...), address(0x2222...)], // sorted list of required DVN addresses optionalDVNs: [] // sorted list of optional DVNs }); /// @notice ExecutorConfig sets message size limit + fee‑paying executor for A → B ExecutorConfig memory exec = ExecutorConfig({ maxMessageSize: 10000, // max bytes per cross-chain message executor: address(0x3333...) // address that pays destination execution fees on B }); bytes memory encodedUln = abi.encode(uln); bytes memory encodedExec = abi.encode(exec); SetConfigParam[] memory params = new SetConfigParam[](2); params[0] = SetConfigParam(eid, EXECUTOR_CONFIG_TYPE, encodedExec); params[1] = SetConfigParam(eid, ULN_CONFIG_TYPE, encodedUln); vm.startBroadcast(signer); ILayerZeroEndpointV2(endpoint).setConfig(oapp, sendLib, params); // Set config for messages sent from A to B vm.stopBroadcast(); } } ``` **Receive Config (B ← A):** The receive config is set on the destination chain (Chain B) and applies to messages being received on Chain B from Chain A. This config determines the DVN settings for inbound messages arriving from Chain A. You must call `setConfig` on the Endpoint contract on Chain B, specifying the remote Endpoint ID for Chain A and the appropriate ReceiveLib address for the B ← A pathway. ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { ILayerZeroEndpointV2, SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; /// @title LayerZero Receive Configuration Script (B ← A) /// @notice Defines and applies ULN (DVN) config for inbound message verification on Chain B for messages received from Chain A via LayerZero Endpoint V2. contract SetReceiveConfig is Script { uint32 constant RECEIVE_CONFIG_TYPE = 2; function run() external { address endpoint = vm.envAddress("ENDPOINT_ADDRESS"); // Chain B Endpoint address oapp = vm.envAddress("OAPP_ADDRESS"); // OApp on Chain B uint32 eid = uint32(vm.envUint("REMOTE_EID")); // Endpoint ID for Chain A address receiveLib= vm.envAddress("RECEIVE_LIB_ADDRESS"); // ReceiveLib for B ← A address signer = vm.envAddress("SIGNER"); /// @notice UlnConfig controls verification threshold for incoming messages from A to B /// @notice Receive config enforces these settings have been applied to the DVNs for messages received from A /// @dev 0 values will be interpretted as defaults, so to apply NIL settings, use: /// @dev uint8 internal constant NIL_DVN_COUNT = type(uint8).max; /// @dev uint64 internal constant NIL_CONFIRMATIONS = type(uint64).max; UlnConfig memory uln = UlnConfig({ confirmations: 15, // min block confirmations from source (A) requiredDVNCount: 2, // required DVNs for message acceptance optionalDVNCount: type(uint8).max, // optional DVNs count optionalDVNThreshold: 0, // optional DVN threshold requiredDVNs: [address(0x1111...), address(0x2222...)], // sorted required DVNs optionalDVNs: [] // no optional DVNs }); bytes memory encodedUln = abi.encode(uln); SetConfigParam[] memory params = new SetConfigParam[](1); params[0] = SetConfigParam(eid, RECEIVE_CONFIG_TYPE, encodedUln); vm.startBroadcast(signer); ILayerZeroEndpointV2(endpoint).setConfig(oapp, receiveLib, params); // Set config for messages received on B from A vm.stopBroadcast(); } } ``` #### 2.3 Set Peers Once you've finished your **OApp Configuration** you can open the messaging channel and connect your OApp deployments by calling `setPeer`. A peer is required to be set for each EID (or network). Ideally an OApp (or OFT) will have multiple peers set where one and only one peer exists for one EID. The function takes 2 arguments: `_eid`, the destination endpoint ID for the chain our other OApp contract lives on, and `_peer`, the destination OApp contract address in `bytes32` format. ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { MyOApp } from "../contracts/MyOApp.sol"; /// @title LayerZero OApp Peer Configuration Script /// @notice Sets up peer connections between OApp deployments on different chains contract SetPeers is Script { function run() external { // Load environment variables address oapp = vm.envAddress("OAPP_ADDRESS"); // Your OApp contract address address signer = vm.envAddress("SIGNER"); // Address with owner permissions // Example: Set peers for different chains // Format: (chain EID, peer address in bytes32) (uint32 eid1, bytes32 peer1) = (uint32(vm.envUint("CHAIN1_EID")), bytes32(uint256(uint160(vm.envAddress("CHAIN1_PEER"))))); (uint32 eid2, bytes32 peer2) = (uint32(vm.envUint("CHAIN2_EID")), bytes32(uint256(uint160(vm.envAddress("CHAIN2_PEER"))))); (uint32 eid3, bytes32 peer3) = (uint32(vm.envUint("CHAIN3_EID")), bytes32(uint256(uint160(vm.envAddress("CHAIN3_PEER"))))); vm.startBroadcast(signer); // Set peers for each chain MyOApp(oapp).setPeer(eid1, peer1); MyOApp(oapp).setPeer(eid2, peer2); MyOApp(oapp).setPeer(eid3, peer3); vm.stopBroadcast(); } } ``` :::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. ::: #### 2.4 Set Enforced Options Enforced options allow the OApp owner to set mandatory execution parameters that will be applied to all messages of a specific type sent to a destination chain. These options are automatically combined with any caller-provided options when using `OAppOptionsType3`. **Why use enforced options?** - Ensure sufficient gas is always allocated for message execution on the destination - Enforce payment for additional services like PreCrime verification - Set consistent execution parameters across all users of your OApp - Prevent failed deliveries due to insufficient gas ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { MyOApp } from "../contracts/MyOApp.sol"; import { EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; /// @title LayerZero OApp Enforced Options Configuration Script /// @notice Sets enforced execution options for specific message types and destinations contract SetEnforcedOptions is Script { using OptionsBuilder for bytes; function run() external { // Load environment variables address oapp = vm.envAddress("OAPP_ADDRESS"); // Your OApp contract address address signer = vm.envAddress("SIGNER"); // Address with owner permissions // Destination chain configurations uint32 dstEid1 = uint32(vm.envUint("DST_EID_1")); // First destination EID uint32 dstEid2 = uint32(vm.envUint("DST_EID_2")); // Second destination EID // Message type (should match your contract's constant) uint16 SEND = 1; // Message type for sendString function // Build options using OptionsBuilder bytes memory options1 = OptionsBuilder.newOptions().addExecutorLzReceiveOption(80000, 0); bytes memory options2 = OptionsBuilder.newOptions().addExecutorLzReceiveOption(100000, 0); // Create enforced options array EnforcedOptionParam[] memory enforcedOptions = new EnforcedOptionParam[](2); // Set enforced options for first destination enforcedOptions[0] = EnforcedOptionParam({ eid: dstEid1, msgType: SEND, options: options1 }); // Set enforced options for second destination enforcedOptions[1] = EnforcedOptionParam({ eid: dstEid2, msgType: SEND, options: options2 }); vm.startBroadcast(signer); // Set enforced options on the OApp MyOApp(oapp).setEnforcedOptions(enforcedOptions); vm.stopBroadcast(); console.log("Enforced options set successfully!"); console.log("Destination 1 EID:", dstEid1, "Gas:", 80000); console.log("Destination 2 EID:", dstEid2, "Gas:", 100000); } } ``` **Environment variables needed:** ```env OAPP_ADDRESS=0x... # Your deployed MyOApp address SIGNER=0x... # Address with owner permissions DST_EID_1=30101 # First destination endpoint ID DST_EID_2=30110 # Second destination endpoint ID ``` **Run the script:** ```bash forge script script/SetEnforcedOptions.s.sol --rpc-url $RPC_URL --broadcast ``` Once set, these enforced options will be automatically applied when using `combineOptions()` in your send functions, ensuring consistent execution parameters across all messages.

## Usage Once deployed and wired, you can begin sending cross-chain messages. ### Calling `send` The LayerZero CLI provides a convenient task for sending messages that automatically handles fee estimation and transaction execution. #### Using the Send Task The CLI includes a built-in `lz:oapp:send` task that: 1. Quotes the gas cost using your OApp's `quoteSendString()` function 2. Sends the message with the correct fee 3. Waits for confirmation and provides tracking links **Basic usage:** ```bash npx hardhat lz:oapp:send --dst-eid 30101 --string "Hello ethereum" --network arbitrum-sepolia-testnet ``` **Parameters:** - `--dst-eid`: Destination endpoint ID (required) - `--string`: Message to send (required) - `--network`: Source network name from your hardhat config (required) - `--options`: Execution options in hex format (optional, defaults to `0x`) **Example output:** ```bash Initiating string send from arbitrum-sepolia-testnet to ethereum-sepolia-testnet String to send: "Hello ethereum" Destination EID: 30101 Using signer: 0x1234567890123456789012345678901234567890 MyOApp contract found at: 0xabcdefabcdefabcdefabcdefabcdefabcdefabcd Execution options: 0x Quoting gas cost for the send transaction... Native fee: 0.001234567890123456 ETH LZ token fee: 0 LZ Sending the string transaction... Transaction hash: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef Waiting for transaction confirmation... Gas used: 123456 Block number: 1234567 ✅ SENT_VIA_OAPP: Successfully sent "Hello ethereum" from arbitrum-sepolia-testnet to ethereum-sepolia-testnet ✅ TX_HASH: Block explorer link for source chain arbitrum-sepolia-testnet: https://sepolia.arbiscan.io/tx/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef ✅ EXPLORER_LINK: LayerZero Scan link for tracking cross-chain delivery: https://testnet.layerzeroscan.com/tx/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef ``` The task automatically: - Finds your deployed `MyOApp` contract - Quotes the exact gas fee needed - Sends the transaction with proper gas estimation - Provides block explorer and LayerZero Scan links for tracking For manual message sending using Foundry, create a script that handles fee estimation and message transmission: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { MyOApp } from "../contracts/MyOApp.sol"; import { MessagingFee } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; /// @title LayerZero OApp Message Sending Script /// @notice Demonstrates how to send messages between OApp deployments contract SendMessage is Script { function run() external { // Load environment variables address oapp = vm.envAddress("OAPP_ADDRESS"); // Your OApp contract address address signer = vm.envAddress("SIGNER"); // Address with permissions to send // Destination chain configuration uint32 dstEid = uint32(vm.envUint("DST_EID")); // Destination chain EID // Message to send string memory message = vm.envString("MESSAGE"); // Your cross-chain message bytes memory options = vm.envBytes("OPTIONS"); // Execution options (or use "0x" for default) // Get the MyOApp contract instance MyOApp myOApp = MyOApp(oapp); // 1. Quote the gas cost first MessagingFee memory fee = myOApp.quoteSendString( dstEid, message, options, false // Pay in native gas, not ZRO tokens ); console.log("Estimated native fee:", fee.nativeFee); console.log("Estimated LZ token fee:", fee.lzTokenFee); // 2. Send the message with the quoted fee vm.startBroadcast(signer); myOApp.sendString{value: fee.nativeFee}( dstEid, message, options ); vm.stopBroadcast(); console.log("Message sent successfully!"); } } ``` **Environment variables needed:** ```env OAPP_ADDRESS=0x... # Your deployed MyOApp address SIGNER=0x... # Private key or address with permissions DST_EID=30101 # Destination endpoint ID MESSAGE="Hello World" # Message to send OPTIONS=0x # Execution options (0x for default) ``` **Run the script:** ```bash forge script script/SendMessage.s.sol --rpc-url $RPC_URL --broadcast ``` ## Extensions The OApp Standard can be extended with various messaging patterns to support complex cross-chain applications. Each pattern functions as a distinct omnichain building block, capable of being used independently or in combination. ### ABA (Ping-Pong) Pattern The **ABA** pattern enables nested messaging where a message sent from Chain A to Chain B triggers another message back to Chain A (`A` → `B` → `A`). This is useful for cross-chain authentication, data feeds, or conditional contract execution. ![ABA Light](/img/learn/ABAlight.svg#gh-light-mode-only) ![ABA Dark](/img/learn/ABAdark.svg#gh-dark-mode-only) #### Implementation The key is to nest an `_lzSend` call within your `_lzReceive` function: ```solidity function _lzReceive( Origin calldata _origin, bytes32 /*_guid*/, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { // Decode the incoming message (string memory data, uint16 msgType, bytes memory returnOptions) = abi.decode(_message, (string, uint16, bytes)); // Process the message lastMessage = data; if (msgType == SEND_ABA) { // Send response back to origin chain _lzSend( _origin.srcEid, abi.encode("Response from Chain B", SEND), returnOptions, MessagingFee(msg.value, 0), payable(address(this)) ); } } ``` :::tip **ABA Pattern Gas Planning**: When implementing the ABA pattern, consider these important factors: 1. **Encode return options in your message**: Include the `_options` parameter for the B→A transaction within your A→B message encoding, as shown in the example above with `returnOptions`. 2. **Calculate total gas costs upfront**: The source OApp (A) needs to know the full transaction cost for the entire A→B→A flow. You should: - Quote the cost of the B→A transaction beforehand - Include this cost in your `lzReceiveOption` gas allocation for the A→B transaction - Ensure sufficient `msg.value` is forwarded to cover both legs of the journey 3. **Example gas calculation**: ```solidity // Quote B→A cost first MessagingFee memory returnFee = quoteBtoA(returnOptions); // Include return fee in A→B options bytes memory abaOptions = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(baseGas + returnGas, returnFee.nativeFee); ``` This ensures your ABA transaction has sufficient gas to complete the full round trip. ::: ### Batch Send **Batch Send** allows a single transaction to initiate multiple `_lzSend` calls to various destination chains, reducing operational overhead for multi-chain operations. ![Batch Send Light](/img/learn/BatchSendLight.svg#gh-light-mode-only) ![Batch Send Dark](/img/learn/BatchSendDark.svg#gh-dark-mode-only) #### Key Implementation Points The batch send pattern includes several important design decisions: 1. **Fee Validation**: Override `_payNative` to change fee check from equivalency to `<` since batch fees are cumulative 2. **Consistent Loop Pattern**: Both `quote` and `send` functions use identical for loops to iterate through destinations for predictable behavior #### Implementation ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import { OApp, MessagingFee, Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; /** * @title BatchSendMock contract for demonstrating multiple outbound 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 BatchSendMock is OApp, OAppOptionsType3 { /// @notice Last received message data. string public data = "Nothing received yet"; /// @notice Message types that are used to identify the various OApp operations. /// @dev These values are used in things like combineOptions() in OAppOptionsType3 (enforcedOptions). uint16 public constant SEND = 1; /// @notice Emitted when a message is received from another chain. event MessageReceived(string message, uint32 senderEid, bytes32 sender); /// @notice Emitted when a message is sent to another chain (A -> B). event MessageSent(string message, uint32 dstEid); /// @dev Revert with this error when an invalid message type is used. error InvalidMsgType(); /** * @dev Constructs a new BatchSend contract instance. * @param _endpoint The LayerZero endpoint for this contract to interact with. * @param _owner The owner address that will be set as the owner of the contract. */ constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) Ownable(msg.sender) {} // Override to change fee check from equivalency to < since batch fees are cumulative function _payNative(uint256 _nativeFee) internal override returns (uint256 nativeFee) { if (msg.value < _nativeFee) revert NotEnoughNative(msg.value); return _nativeFee; } /** * @notice Returns the estimated messaging fee for a given message. * @param _dstEids Destination endpoint ID array where the message will be batch sent. * @param _msgType The type of message being sent. * @param _message The message content. * @param _extraSendOptions Extra gas options for receiving the send call (A -> B). * Will be summed with enforcedOptions, even if no enforcedOptions are set. * @param _payInLzToken Boolean flag indicating whether to pay in LZ token. * @return totalFee The estimated messaging fee for sending to all pathways. */ function quote( uint32[] memory _dstEids, uint16 _msgType, string memory _message, // Semantic naming for message content bytes calldata _extraSendOptions, bool _payInLzToken ) public view returns (MessagingFee memory totalFee) { bytes memory encodedMessage = abi.encode(_message); // Clear distinction: input vs processed for (uint i = 0; i < _dstEids.length; i++) { bytes memory options = combineOptions(_dstEids[i], _msgType, _extraSendOptions); MessagingFee memory fee = _quote(_dstEids[i], encodedMessage, options, _payInLzToken); totalFee.nativeFee += fee.nativeFee; totalFee.lzTokenFee += fee.lzTokenFee; } } function send( uint32[] memory _dstEids, uint16 _msgType, string memory _message, bytes calldata _extraSendOptions // gas settings for A -> B ) external payable { // Message type validation for security and extensibility if (_msgType != SEND) { revert InvalidMsgType(); } // Gas efficiency: calculate total fees upfront (fail-fast pattern) MessagingFee memory totalFee = quote(_dstEids, _msgType, _message, _extraSendOptions, false); require(msg.value >= totalFee.nativeFee, "Insufficient fee provided"); // Encodes the message before invoking _lzSend. bytes memory _encodedMessage = abi.encode(_message); uint256 totalNativeFeeUsed = 0; uint256 remainingValue = msg.value; for (uint i = 0; i < _dstEids.length; i++) { bytes memory options = combineOptions(_dstEids[i], _msgType, _extraSendOptions); MessagingFee memory fee = _quote(_dstEids[i], _encodedMessage, options, false); totalNativeFeeUsed += fee.nativeFee; remainingValue -= fee.nativeFee; // Granular fee tracking per destination require(remainingValue >= 0, "Insufficient fee for this destination"); _lzSend( _dstEids[i], _encodedMessage, options, fee, payable(msg.sender) ); emit MessageSent(_message, _dstEids[i]); // Event emission for tracking } } /** * @notice Internal function to handle receiving messages from another chain. * @dev Decodes and processes the received message based on its type. * @param _origin Data about the origin of the received message. * @param message The received message content. */ function _lzReceive( Origin calldata _origin, bytes32 /*guid*/, bytes calldata message, address, // Executor address as specified by the OApp. bytes calldata // Any extra data or options to trigger on receipt. ) internal override { string memory _data = abi.decode(message, (string)); data = _data; emit MessageReceived(data, _origin.srcEid, _origin.sender); } } ``` This pattern is particularly useful for **mass updating state from a single call** - allowing you to push data from one chain to many chains efficiently. Common use cases include configuration updates, price feeds, or state synchronization across multiple destination chains. ### Call Composer **Composed** messaging enables **horizontal composability** where a message triggers external contract calls on the destination chain through `lzCompose`. Unlike vertical composability (multiple calls in a single transaction), horizontal composability processes operations as separate, containerized message packets. ![Composed Light](/img/learn/Composed-Light.svg#gh-light-mode-only) ![Composed Dark](/img/learn/Composed-Dark.svg#gh-dark-mode-only) #### Benefits of Horizontal Composability - **Fault Isolation**: If a composed call fails, it doesn't revert the main token transfer or message - **Gas Efficiency**: Each step can have independent gas limits and execution options - **Flexible Workflows**: Complex multi-step operations can be broken into manageable pieces #### Sending Side ```solidity function sendStringToComposer( uint32 _dstEid, string memory _string, address _composer, bytes calldata _extraOptions ) external payable { // Include both lzReceive and lzCompose options in enforcedOptions or extraOptions bytes memory composedOptions = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(65000, 0) // For the main receive .addExecutorLzComposeOption(0, 50000, 0); // For the compose call bytes memory _message = abi.encode(_string, _composer); _lzSend( _dstEid, _message, composedOptions, MessagingFee(msg.value, 0), payable(msg.sender) ); } ``` #### Receiving Side ```solidity function _lzReceive( Origin calldata _origin, bytes32 _guid, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { (string memory _string, address composer) = abi.decode(_message, (string, address)); // Store the message and perform primary logic lastMessage = _string; // Send composed message to external contract as separate message packet endpoint.sendCompose(composer, _guid, 0, _message); } ``` #### Composer Contract ```solidity import { IOAppComposer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol"; contract Composer is IOAppComposer { address public immutable endpoint; address public immutable trustedOApp; constructor(address _endpoint, address _trustedOApp) { endpoint = _endpoint; trustedOApp = _trustedOApp; } function lzCompose( address _oApp, bytes32 /*_guid*/, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) external payable override { // Security checks require(msg.sender == endpoint, "!endpoint"); require(_oApp == trustedOApp, "!oApp"); // Decode the message payload (string memory _string, ) = abi.decode(_message, (string, address)); // Execute custom business logic performCustomAction(_string); } function performCustomAction(string memory message) internal { // Your custom logic here (swap, stake, mint, etc.) } } ``` :::tip **Execution Options for Composed Messages**: You must provide gas for both the main `lzReceive` call and the `lzCompose` call: ```solidity bytes memory options = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(baseGas, 0) // Main message processing .addExecutorLzComposeOption(0, composeGas, value); // Composed call (index 0) ``` The `_index` parameter allows multiple composed calls with different gas allocations. ::: ### Message Ordering LayerZero supports both **unordered** (default) and **ordered** delivery patterns. #### Ordered Delivery Implementation ```solidity pragma solidity ^0.8.22; import { OApp, Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; /** * @title OmniChain Nonce Ordered Enforcement Example * @dev Implements nonce ordered enforcement for your OApp. */ contract OrderedOApp is OApp { // Mapping to track the maximum received nonce for each source endpoint and sender mapping(uint32 eid => mapping(bytes32 sender => uint64 nonce)) private receivedNonce; constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) {} /** * @dev Public function to get the next expected nonce for a given source endpoint and sender. * @param _srcEid Source endpoint ID. * @param _sender Sender's address in bytes32 format. * @return uint64 Next expected nonce. */ function nextNonce(uint32 _srcEid, bytes32 _sender) public view virtual override returns (uint64) { return receivedNonce[_srcEid][_sender] + 1; } /** * @dev Internal function to accept nonce from the specified source endpoint and sender. * @param _srcEid Source endpoint ID. * @param _sender Sender's address in bytes32 format. * @param _nonce The nonce to be accepted. */ function _acceptNonce(uint32 _srcEid, bytes32 _sender, uint64 _nonce) internal virtual override { uint64 expectedNonce = receivedNonce[_srcEid][_sender] + 1; require(_nonce == expectedNonce, "OApp: invalid nonce"); receivedNonce[_srcEid][_sender] = _nonce; // Update to the accepted nonce } /** * @dev Override receive function to enforce strict nonce enforcement. * @dev This function is internal and should not be public. */ function _lzReceive( Origin calldata _origin, bytes32 _guid, bytes calldata _message, address _executor, bytes calldata _extraData ) internal override { // Enforce nonce ordering before processing the message _acceptNonce(_origin.srcEid, _origin.sender, _origin.nonce); // Process your message logic here // Example: string memory receivedMessage = abi.decode(_message, (string)); } // Must include ExecutorOrderedExecutionOption in your send options function sendOrdered(uint32 _dstEid, string memory _message) external payable { bytes memory options = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(200000, 0) .addExecutorOrderedExecutionOption(); // Required for ordered execution _lzSend(_dstEid, abi.encode(_message), options, MessagingFee(msg.value, 0), payable(msg.sender)); } } ``` #### Important Nonce Management Considerations When implementing ordered delivery, be aware of these critical nonce synchronization issues: 1. **Nonce Validation**: The `_acceptNonce` function must be called in `_lzReceive` to verify the incoming nonce matches the expected sequence before processing any message. 2. **Protocol vs Local Nonce Mismatch**: Functions like `skip()`, `burn()`, and `clear()` advance the protocol's nonce but **do not** automatically update your OApp's local nonce mapping. This creates a dangerous mismatch where: - Protocol nonce: 15 (after skipping message 15) - OApp mapping: 14 (still expecting message 15) - Result: All future messages will be rejected 3. **Solution**: If your OApp needs to use `skip()`, `burn()`, or `clear()`, you must **manually increment your local nonce** to stay synchronized: ```solidity // When skipping a message, update your local tracking function skipMessage(uint32 _srcEid, bytes32 _sender, uint64 _nonce) external onlyOwner { // Skip the message at protocol level endpoint.skip(this, _srcEid, _sender, _nonce); // Critical: Update local nonce to match protocol receivedNonce[_srcEid][_sender] = _nonce; } ``` **Best Practice**: Only call these recovery functions from within your OApp contract, never externally, to ensure nonce synchronization is maintained. ### Rate Limiting Control message frequency to prevent spam and ensure controlled cross-chain interactions: ```solidity contract RateLimitedOApp is OApp, RateLimiter { constructor( address _endpoint, address _owner, RateLimitConfig[] memory _rateLimitConfigs ) OApp(_endpoint, _owner) { _setRateLimits(_rateLimitConfigs); } function sendWithRateLimit( uint32 _dstEid, string memory _message, bytes calldata _options ) external payable { // Check rate limit before sending _outflow(_dstEid, 1); // 1 message _lzSend( _dstEid, abi.encode(_message), _options, MessagingFee(msg.value, 0), payable(msg.sender) ); } } ``` ### Further Reading For detailed implementations and advanced patterns, see: - [Message Execution Options](../configuration/options.md) - Options configuration - [OApp Technical Reference](../../../concepts/technical-reference/oapp-reference.md) - Deep dive into OApp mechanics - [Integration Checklist](../../../tools/integration-checklist.md) - Security considerations and best practices ### Tracing and Troubleshooting You can follow your testnet and mainnet transaction statuses using [LayerZero Scan](https://layerzeroscan.com/). Refer to [Debugging Messages](../troubleshooting/debugging-messages.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: Omnichain Queries (LayerZero Read) sidebar_label: Omnichain Queries (lzRead) --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; **LayerZero Read (lzRead)** enables smart contracts to request and retrieve on-chain state from other blockchains using LayerZero's cross-chain infrastructure. Unlike traditional messaging that sends data from source to destination, lzRead implements a request-response pattern where contracts can pull external state data from other blockchains. For conceptual information about how Omnichain Queries work, see the [Read Standard Overview](../../../concepts/applications/read-standard.md). #### Key Differences from Push-based Messaging | Feature | **Omnichain Message** | **Omnichain Read** | | ----------- | ---------------------------------------- | ---------------------------------------------- | | **Flow** | Source sends data to destination | Source requests data, source receives response | | **Data** | `bytes` sent = `bytes` received | `bytes` request ≠ `bytes` response | | **Purpose** | _**Push**_ state changes to other chains | _**Pull**_ external state from other chains | ## Supported Chains lzRead requires compatible Message Libraries (`ReadLib1002`) and DVNs with archival node access. See [Read Paths](../../../deployments/read-contracts.md) for available chains and DVNs. ![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) ## Installation To start using LayerZero Read in a new project, use the LayerZero CLI tool, [**create-lz-oapp**](../../../get-started/create-lz-oapp/start.md). The CLI tool allows developers to create any omnichain application with read capabilities quickly! Get started by running the following from your command line: ```bash LZ_ENABLE_READ_EXAMPLE=1 npx create-lz-oapp@latest --example oapp-read ``` Select the **OApp Read** template when prompted. This creates a complete project with: - Example contracts with read capabilities - Cross-chain unit tests for read operations - Custom LayerZero read configuration files - Deployment scripts and setup To use LayerZero Read contracts in an existing project, you can install the **OApp package** directly: ```bash npm install @layerzerolabs/oapp-evm ``` ```bash yarn add @layerzerolabs/oapp-evm ``` ```bash pnpm add @layerzerolabs/oapp-evm ``` ```bash forge init ``` ```bash forge install layerzero-labs/devtools forge install layerzero-labs/LayerZero-v2 forge install OpenZeppelin/openzeppelin-contracts git submodule add https://github.com/GNSPS/solidity-bytes-utils.git lib/solidity-bytes-utils ``` Then add to your `foundry.toml` under `[profile.default]`: ```toml [profile.default] src = "src" out = "out" libs = ["lib"] remappings = [ '@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/', '@layerzerolabs/lz-evm-protocol-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/protocol', '@layerzerolabs/lz-evm-messagelib-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/messagelib', 'solidity-bytes-utils/=lib/solidity-bytes-utils/', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', ] ``` :::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", } ``` ::: ## Custom Read Contract To build your own cross-chain read application, inherit from `OAppRead.sol` and implement three key pieces: 1. **Read request construction**: How you build queries for external data 2. **Fee estimation**: How you calculate costs before sending requests 3. **Response handling**: How you process returned data in `_lzReceive` Below is the complete example that comes with the CLI tool, showing: - A constructor setting up the LayerZero endpoint, owner, and read channel - A `readData(...)` function that builds and sends read requests - A `quoteReadFee(...)` function to estimate costs before sending - An override of `_lzReceive(...)` that processes returned data - A target contract interface for type-safe interactions ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; // Import necessary interfaces and contracts import { AddressCast } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/AddressCast.sol"; import { MessagingFee, MessagingReceipt } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { ReadCodecV1, EVMCallRequestV1 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/ReadCodecV1.sol"; import { OAppRead } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppRead.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; /// @title IExampleContract /// @notice Interface for the ExampleContract's `data()` function. interface IExampleContract { function data() external view returns (uint256); } /// @title ReadPublic /// @notice An OAppRead contract example to read a public state variable from another chain. contract ReadPublic is OAppRead, OAppOptionsType3 { /// @notice Emitted when the data is received. /// @param data The value of the public state variable. event DataReceived(uint256 data); /// @notice LayerZero read channel ID. uint32 public READ_CHANNEL; /// @notice Message type for the read operation. uint16 public constant READ_TYPE = 1; /** * @notice Constructor to initialize the OAppRead contract. * * @param _endpoint The LayerZero endpoint contract address. * @param _delegate The address that will have ownership privileges. * @param _readChannel The LayerZero read channel ID. */ constructor( address _endpoint, address _delegate, uint32 _readChannel ) OAppRead(_endpoint, _delegate) Ownable(_delegate) { READ_CHANNEL = _readChannel; _setPeer(_readChannel, AddressCast.toBytes32(address(this))); } // ────────────────────────────────────────────────────────────────────────────── // 0. (Optional) Quote business logic // // Example: Get a quote from the Endpoint for a cost estimate of reading data. // Replace this to mirror your own read business logic. // ────────────────────────────────────────────────────────────────────────────── /** * @notice Estimates the messaging fee required to perform the read operation. * * @param _targetContractAddress The address of the contract on the target chain containing the `data` variable. * @param _targetEid The target chain's Endpoint ID. * @param _extraOptions Additional messaging options. * * @return fee The estimated messaging fee. */ function quoteReadFee( address _targetContractAddress, uint32 _targetEid, bytes calldata _extraOptions ) external view returns (MessagingFee memory fee) { return _quote( READ_CHANNEL, _getCmd(_targetContractAddress, _targetEid), combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions), false ); } // ────────────────────────────────────────────────────────────────────────────── // 1a. Send business logic // // Example: send a read request to fetch data from a remote contract. // Replace this with your own read request logic. // ────────────────────────────────────────────────────────────────────────────── /** * @notice Sends a read request to fetch the public state variable `data`. * * @dev The caller must send enough ETH to cover the messaging fee. * * @param _targetContractAddress The address of the contract on the target chain containing the `data` variable. * @param _targetEid The target chain's Endpoint ID. * @param _extraOptions Additional messaging options. * * @return receipt The LayerZero messaging receipt for the request. */ function readData( address _targetContractAddress, uint32 _targetEid, bytes calldata _extraOptions ) external payable returns (MessagingReceipt memory receipt) { // 1. Build the read command for the target contract and function bytes memory cmd = _getCmd(_targetContractAddress, _targetEid); // 2. Send the read request via LayerZero // - READ_CHANNEL: Special channel ID for read operations // - cmd: Encoded read command with target details // - combineOptions: Merge enforced options with caller-provided options // - MessagingFee(msg.value, 0): Pay all fees in native gas; no ZRO // - payable(msg.sender): Refund excess gas to caller return _lzSend( READ_CHANNEL, cmd, combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions), MessagingFee(msg.value, 0), payable(msg.sender) ); } // ────────────────────────────────────────────────────────────────────────────── // 1b. Read command construction // // This function defines WHAT data to fetch from the target network and WHERE to fetch it from. // This is the core of LayerZero Read - specifying exactly which contract function to call // on which chain and how to handle the request. // ────────────────────────────────────────────────────────────────────────────── /** * @notice Constructs the read command to fetch the `data` variable from target chain. * @dev This function defines the core read operation - what data to fetch and from where. * Replace this logic to read different functions or data from your target contracts. * * @param _targetContractAddress The address of the contract containing the `data` variable. * @param _targetEid The target chain's Endpoint ID. * * @return cmd The encoded command that specifies what data to read. */ function _getCmd(address _targetContractAddress, uint32 _targetEid) internal view returns (bytes memory cmd) { // 1. Define WHAT function to call on the target contract // Using the interface selector ensures type safety and correctness // You can replace this with any public/external function or state variable bytes memory callData = abi.encodeWithSelector(IExampleContract.data.selector); // 2. Build the read request specifying WHERE and HOW to fetch the data EVMCallRequestV1[] memory readRequests = new EVMCallRequestV1[](1); readRequests[0] = EVMCallRequestV1({ appRequestLabel: 1, // Label for tracking this specific request targetEid: _targetEid, // WHICH chain to read from isBlockNum: false, // Use timestamp (not block number) blockNumOrTimestamp: uint64(block.timestamp), // WHEN to read the state (current time) confirmations: 15, // HOW many confirmations to wait for to: _targetContractAddress, // WHERE - the contract address to call callData: callData // WHAT - the function call to execute }); // 3. Encode the complete read command // No compute logic needed for simple data reading // The appLabel (0) can be used to identify different types of read operations cmd = ReadCodecV1.encode(0, readRequests); } // ────────────────────────────────────────────────────────────────────────────── // 2. Receive business logic // // Override _lzReceive to handle the returned data from the read request. // The base OAppReceiver.lzReceive ensures: // • Only the LayerZero Endpoint can call this method // • The sender is a registered peer (peers[srcEid] == origin.sender) // ────────────────────────────────────────────────────────────────────────────── /** * @notice Handles the received data from the target chain. * * @dev This function is called internally by the LayerZero protocol. * @dev _origin Metadata (source chain, sender address, nonce) * @dev _guid Global unique ID for tracking this response * @param _message The data returned from the read request (uint256 in this case) * @dev _executor Executor address that delivered the response * @dev _extraData Additional data from the Executor (unused here) */ function _lzReceive( Origin calldata /*_origin*/, bytes32 /*_guid*/, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { // 1. Decode the returned data from bytes to uint256 uint256 data = abi.decode(_message, (uint256)); // 2. Emit an event with the received data emit DataReceived(data); // 3. (Optional) Apply your custom logic here. // e.g., store the data, trigger additional actions, etc. } // ────────────────────────────────────────────────────────────────────────────── // 3. Admin functions // // Functions for managing the read channel configuration. // ────────────────────────────────────────────────────────────────────────────── /** * @notice Sets the LayerZero read channel. * * @dev Only callable by the owner. * * @param _channelId The channel ID to set. * @param _active Flag to activate or deactivate the channel. */ function setReadChannel(uint32 _channelId, bool _active) public override onlyOwner { _setPeer(_channelId, _active ? AddressCast.toBytes32(address(this)) : bytes32(0)); READ_CHANNEL = _channelId; } } ``` ### Target Contract The read contract interacts with a simple target contract deployed on other chains. This example demonstrates how you can **call a public data variable on a destination network and get the current state from another network** using LayerZero Read: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; /** * @title ExampleContract * @notice A simple contract with a public state variable that can be read cross-chain. * @dev This contract would be deployed on target chains (e.g., Ethereum, Polygon, Arbitrum) * and the ReadPublic contract can fetch its `data` value from any other supported chain. */ contract ExampleContract { /// @notice Public state variable that can be read from other chains /// @dev The public keyword automatically generates a getter function data() uint256 public data; constructor(uint256 _data) { data = _data; } } ``` **Cross-Chain Reading Example:** - Deploy `ExampleContract` on a target network with `data = 100` - Deploy `ReadPublic` on your source network - Call `readData(targetContractAddress, targetEid, "0x")` from source - The contract's configured DVNs will fetch the current value of `data` (100) from the target and emit `DataReceived(100)` on source This enables real-time access to state from any supported blockchain without complex bridging or manual oracle updates. ### Constructor - Pass the Endpoint V2 address, owner address, and read channel ID into the base contracts. - `OAppRead(_endpoint, _delegate)` binds your contract to LayerZero and sets the delegate - `Ownable(_delegate)` makes the delegate the only address that can change configurations - `_setPeer(_readChannel, AddressCast.toBytes32(address(this)))` establishes the read channel ### readData(...) 1. **Build the read command** - `_getCmd()` constructs the query specifying what data to fetch and from where - Uses `IExampleContract.data.selector` for type-safe function selection 2. **Send the read request** - `_lzSend()` packages and dispatches the read request via LayerZero - `READ_CHANNEL` is the special channel ID for read operations - `_combineOptions()` merges enforced options with caller-provided options ### \_lzReceive(...) 1. **Endpoint verification** - Only the LayerZero Endpoint can invoke this function - The call succeeds only if the sender matches the registered read channel peer 2. **Decode the returned data** - Use `abi.decode(_message, (uint256))` to extract the original data - The data format matches what the target contract's `data()` function returns 3. **Process the result** - Emit `DataReceived` event with the fetched data - Add any custom business logic needed for your application ### (Optional) quoteReadFee(...) You can call the internal `_quote(...)` method to get accurate cost estimates before sending read requests. **Example usage:** ```solidity // Get fee estimate first MessagingFee memory fee = readPublic.quoteReadFee( targetContractAddress, targetEid, "0x" // no additional options ); // Then send with the estimated fee readPublic.readData{value: fee.nativeFee}( targetContractAddress, targetEid, "0x" ); ``` ## Deployment and Wiring lzRead wiring is **significantly simpler** than traditional cross-chain messaging setup. Unlike OApp messaging where you need to configure peer connections between each contract on every chain pathway, lzRead only requires configuring the **source chain** (where your `OAppRead` child contract lives and where response data will be returned to). **Key Simplifications:** - **Single-sided peer wiring**: You only need to set the `OAppRead` address itself as the peer - **Dynamic target selection**: Target chains are specified in the read command itself, not in wiring - **DVN requirements**: DVNs must support the target chains and `block.number` or `block.timestamp` you want to read - **Single-direction setup**: Only configure the source chain to receive responses This means you can read from **any supported target chain** without additional wiring by just specifying the target in your `EVMCallRequestV1` and ensuring your DVNs support that chain. #### Execution Options for lzRead lzRead uses different execution options than standard messaging. Instead of `addExecutorLzReceiveOption`, you must use `addExecutorLzReadOption` with **calldata size estimation**: ```solidity // Standard messaging options OptionsBuilder.newOptions().addExecutorLzReceiveOption(100000, 0); // lzRead options (note the size parameter) OptionsBuilder.newOptions().addExecutorLzReadOption(100000, 64, 0); // gas size value ``` **Key difference**: The `size` parameter estimates your response data size in bytes. If your actual response exceeds this size, the executor won't deliver automatically. :::tip **Size estimation**: uint256 = 32 bytes, address = 20 bytes, etc. The `enforcedOptions` in your configuration (shown below) should account for your expected response sizes. For details on options configuration, see [**Execution Options**](../configuration/options.md). ::: #### Deploy and Wire Deploy your lzRead OApp: ```bash # Deploy contracts npx hardhat lz:deploy ``` Then, review the `layerzero.config.ts` with read-specific settings: ```typescript import {ChannelId, EndpointId} from '@layerzerolabs/lz-definitions'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {type OAppReadOmniGraphHardhat, type OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; const arbsepContract: OmniPointHardhat = { eid: EndpointId.ARBSEP_V2_TESTNET, contractName: 'ReadPublic', }; const config: OAppReadOmniGraphHardhat = { contracts: [ { contract: arbsepContract, config: { readChannelConfigs: [ { channelId: ChannelId.READ_CHANNEL_1, active: true, readLibrary: '0x54320b901FDe49Ba98de821Ccf374BA4358a8bf6', ulnConfig: { requiredDVNs: ['0x5c8c267174e1f345234ff5315d6cfd6716763bac'], executor: '0x5Df3a1cEbBD9c8BA7F8dF51Fd632A9aef8308897', }, enforcedOptions: [ { msgType: 1, optionType: ExecutorOptionType.LZ_READ, gas: 80000, size: 1000000, value: 0, }, ], }, ], }, }, ], connections: [], }; export default config; ``` Deploy and configure your read OApp: ```bash # Wire read configuration npx hardhat lz:oapp-read:wire --oapp-config layerzero.config.ts ``` This automatically: - Sets the ReadLib1002 as send/receive library - Configures required DVNs for read operations - Activates specified read channels - Sets up executor configuration For manual setup, you need to configure four components **on your source chain only**: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import { Script, console } from "forge-std/Script.sol"; import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; import { ExecutorOptions } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/ExecutorOptions.sol"; import { EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { ReadLibConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/readlib/ReadLibBase.sol"; import { SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManager.sol"; import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { ReadPublic } from "../src/ReadPublic.sol"; contract SetConfigScript is Script { using OptionsBuilder for bytes; // Configuration constants - REPLACE WITH YOUR VALUES uint32 public constant READ_CHANNEL = 4294967295; // LayerZero Read Channel ID address public constant ENDPOINT_ADDRESS = 0x1a44076050125825900e736c501f859c50fE728c; // LayerZero V2 Endpoint address public constant READ_LIB_ADDRESS = 0xbcd4CADCac3F767C57c4F402932C4705DF62BEFf; // ReadLib1002 address for your chain - UPDATE THIS address public constant READ_COMPATIBLE_DVN = 0x1308151a7ebaC14f435d3Ad5fF95c34160D539A5; // DVN that supports read operations - UPDATE THIS // Contract addresses to configure - SET THESE AFTER DEPLOYMENT address public readPublicAddress; function setUp() public { // Set your deployed ReadPublic contract address here readPublicAddress = vm.envAddress("READ_PUBLIC_ADDRESS"); } function run() public { vm.startBroadcast(); console.log("Configuring ReadPublic contract at:", readPublicAddress); // Get contract instances ILayerZeroEndpointV2 endpoint = ILayerZeroEndpointV2(ENDPOINT_ADDRESS); ReadPublic myReadApp = ReadPublic(readPublicAddress); // 1. Set Read Library (only on source chain) console.log("Step 1: Setting Read Library..."); endpoint.setSendLibrary(readPublicAddress, READ_CHANNEL, READ_LIB_ADDRESS); endpoint.setReceiveLibrary(readPublicAddress, READ_CHANNEL, READ_LIB_ADDRESS, 0); // 2. Configure DVNs (must support target chains you want to read from) console.log("Step 2: Configuring DVNs..."); SetConfigParam[] memory params = new SetConfigParam[](1); address[] memory requiredDVNs = new address[](1); requiredDVNs[0] = READ_COMPATIBLE_DVN; address[] memory optionalDVNs = new address[](0); params[0] = SetConfigParam({ eid: READ_CHANNEL, configType: 1, // LZ_READ_LID_CONFIG_TYPE config: abi.encode(ReadLibConfig({ executor: address(0x31CAe3B7fB82d847621859fb1585353c5720660D), // Executor address - UPDATE THIS requiredDVNCount: 1, optionalDVNCount: 0, optionalDVNThreshold: 0, requiredDVNs: requiredDVNs, optionalDVNs: optionalDVNs })) }); endpoint.setConfig(readPublicAddress, READ_LIB_ADDRESS, params); // 3. Activate Read Channel (enables receiving responses) console.log("Step 3: Activating Read Channel..."); myReadApp.setReadChannel(READ_CHANNEL, true); // 4. Set Enforced Options (with lzRead-specific options) console.log("Step 4: Setting Enforced Options..."); EnforcedOptionParam[] memory enforcedOptions = new EnforcedOptionParam[](1); enforcedOptions[0] = EnforcedOptionParam({ eid: READ_CHANNEL, msgType: 1, // READ_MSG_TYPE options: OptionsBuilder.newOptions().addExecutorLzReadOption(50000, 128, 0) }); myReadApp.setEnforcedOptions(enforcedOptions); console.log("Configuration complete!"); vm.stopBroadcast(); } } ``` :::info **No target chain configuration needed!** Target chains and contracts are specified dynamically in your `_getCmd()` function via the `targetEid` and `to` parameters in `EVMCallRequestV1`. ::: ## Usage Once deployed and wired, you can begin reading data from contracts on other chains. ### Read data The LayerZero CLI provides a convenient task for reading cross-chain data that automatically handles fee estimation and transaction execution. #### Using the Read Task The CLI includes a built-in `lz:oapp-read:read` task that: 1. Finds your deployed ReadPublic contract automatically 2. Quotes the gas cost using your contract's `quoteReadFee()` function 3. Sends the read request with the correct fee 4. Provides tracking links for the transaction **Basic usage:** ```bash npx hardhat lz:oapp-read:read --target-contract 0x1234567890123456789012345678901234567890 --target-eid 30101 ``` **Required Parameters:** - `--target-contract`: Address of the contract to read from on the target chain - `--target-eid`: Target chain endpoint ID (e.g., 30101 for Ethereum) **Optional Parameters:** - `--options`: Additional execution options as hex string (default: "0x") **Example with options:** ```bash npx hardhat lz:oapp-read:read \ --target-contract 0x1234567890123456789012345678901234567890 \ --target-eid 30101 \ --options 0x00030100110100000000000000000000000000030d40 ``` The task automatically: - Finds your deployed ReadPublic contract from deployment artifacts - Quotes the exact gas fee needed using `quoteReadFee()` - Sends the read request with proper fee payment - Provides block explorer and LayerZero Scan links for tracking - Shows the transaction details and gas usage **Example output:** ``` ✅ SENT_READ_REQUEST: Successfully sent read request from arbitrum-sepolia to ethereum ✅ TX_HASH: Block explorer link for source chain arbitrum-sepolia: https://sepolia.arbiscan.io/tx/0x... ✅ EXPLORER_LINK: LayerZero Scan link for tracking read request: https://testnet.layerzeroscan.com/tx/0x... 📖 Read request sent! The data will be received and emitted in a DataReceived event. Check the ReadPublic contract for the DataReceived event to see the result. ``` For manual deployment and testing with Foundry, use the following deployment scripts: #### 1. Deploy Target Contract First, deploy the `ExampleContract` that will be read from: ```solidity // script/DeployExampleContract.s.sol forge script script/DeployExampleContract.s.sol:DeployExampleContractScript \ --rpc-url $RPC_URL_TARGET \ --private-key $PRIVATE_KEY \ --broadcast ``` #### 2. Deploy ReadPublic Contract Deploy the lzRead contract on your source chain: ```solidity // script/DeployReadPublic.s.sol forge script script/DeployReadPublic.s.sol:DeployReadPublicScript \ --rpc-url $RPC_URL_SOURCE \ --private-key $PRIVATE_KEY \ --broadcast ``` :::tip Make sure to update the `ENDPOINT_ADDRESS` and `READ_CHANNEL` constants in the script to match your deployment network. ::: #### 3. Configure the Read Contract After deployment, configure your contract with the proper LayerZero settings: ```solidity // script/SetConfig.s.sol forge script script/SetConfig.s.sol:SetConfigScript \ --rpc-url $RPC_URL_SOURCE \ --private-key $PRIVATE_KEY \ --broadcast ``` This script: - Sets the Read Library addresses - Configures DVNs using `ReadLibConfig` struct - Activates the read channel - Sets enforced options for lzRead :::warning The configuration uses `ReadLibConfig` (not `UlnConfig`) with these required fields: - `executor`: Address of the executor - `requiredDVNCount`: Number of required DVNs - `optionalDVNCount`: Number of optional DVNs - `optionalDVNThreshold`: Threshold for optional DVNs - `requiredDVNs`: Array of required DVN addresses - `optionalDVNs`: Array of optional DVN addresses ::: #### 4. Test the Read Functionality Execute a test read to verify everything is working: ```solidity // script/TestRead.s.sol forge script script/TestRead.s.sol:TestReadScript \ --rpc-url $RPC_URL_SOURCE \ --private-key $PRIVATE_KEY \ --broadcast ``` This script will: 1. Quote the fee for the read operation 2. Send the read request with the proper fee 3. Display transaction details and tracking information #### Complete Example Workflow ```bash # 1. Set environment variables export PRIVATE_KEY="your_private_key" export RPC_URL_TARGET="https://rpc.target-chain.com" export RPC_URL_SOURCE="https://rpc.source-chain.com" # 2. Deploy target contract (e.g., on Optimism) forge script script/DeployExampleContract.s.sol:DeployExampleContractScript \ --rpc-url $RPC_URL_TARGET \ --private-key $PRIVATE_KEY \ --broadcast # 3. Add deployed address to .env echo "EXAMPLE_CONTRACT_ADDRESS=0x..." >> .env # 4. Deploy ReadPublic on source chain (e.g., on Arbitrum) forge script script/DeployReadPublic.s.sol:DeployReadPublicScript \ --rpc-url $RPC_URL_SOURCE \ --private-key $PRIVATE_KEY \ --broadcast # 5. Add deployed address to .env echo "READ_PUBLIC_ADDRESS=0x..." >> .env # 6. Configure the ReadPublic contract forge script script/SetConfig.s.sol:SetConfigScript \ --rpc-url $RPC_URL_SOURCE \ --private-key $PRIVATE_KEY \ --broadcast # 7. Test the read functionality forge script script/TestRead.s.sol:TestReadScript \ --rpc-url $RPC_URL_SOURCE \ --private-key $PRIVATE_KEY \ --broadcast ``` ## Advanced Read Contracts lzRead supports several advanced patterns for cross-chain data access. Each pattern addresses different use cases, from simple data retrieval to complex multi-chain aggregation with compute logic. :::tip **Complete examples** are available in the [**LayerZero devtools repository**](https://github.com/LayerZero-Labs/devtools/tree/82285d5c566d6cea4d7d0cc05899c9838b0a4c6c/examples/). These examples provide full contract implementations you can deploy and test. ::: ### Call View/Pure Functions You can use lzRead to call any `view` or `pure` function on a target chain and bring the returned data back to your source chain contract. This is the fundamental lzRead pattern that enables cross-chain function execution without state changes. **Core concept:** Instead of deploying identical contracts on every chain or building complex bridging infrastructure, lzRead lets you call functions on any supported chain and receive the results natively. The target function executes via `eth_call`, ensuring no state modification occurs. **Use cases:** - **Cross-chain calculations**: Call mathematical functions, pricing algorithms, or complex computations - **Remote contract queries**: Access getter functions, view state, or computed values from contracts on other chains - **Protocol integration**: Query external protocols (like AMMs, lending protocols, oracles) without deploying wrappers - **Data aggregation**: Collect information from various chains' contracts for unified processing - **Validation**: Verify conditions or states across multiple chains before executing local logic **Key implementation details:** - Single `EVMCallRequestV1` targeting specific function with parameters - Target function must be `view` or `pure` to ensure no state changes - Raw response data returned directly to `_lzReceive` - no compute processing needed - Function selector and parameter encoding handled via standard ABI encoding - Works with any function signature: simple getters, complex multi-parameter functions, struct returns #### Installation Get started quickly with a pre-built lzRead example for reading view or pure functions: ```bash LZ_ENABLE_READ_EXAMPLE=1 npx create-lz-oapp@latest --example view-pure-read ``` This creates a complete lzRead project with: - Example contracts for reading `view`/`pure` functions - Deploy and configuration scripts - Test suites demonstrating all patterns - Ready-to-use implementations you can customize #### Contract Example ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; // Import necessary interfaces and contracts import { AddressCast } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/AddressCast.sol"; import { MessagingFee, MessagingReceipt } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { OAppRead } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppRead.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { EVMCallRequestV1, ReadCodecV1 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/ReadCodecV1.sol"; /// @title IExampleContract /// @notice Interface for the ExampleContract's `add` function. interface IExampleContract { function add(uint256 a, uint256 b) external pure returns (uint256); } /// @title ReadViewOrPure Example /// @notice An OAppRead contract that calls view/pure functions on target chains and receives results contract ReadViewOrPure is OAppRead, OAppOptionsType3 { /// @notice Emitted when cross-chain function data is successfully received event SumReceived(uint256 sum); /// @notice LayerZero read channel ID for cross-chain data requests uint32 public READ_CHANNEL; /// @notice Message type identifier for read operations uint16 public constant READ_TYPE = 1; /// @notice Target chain's LayerZero Endpoint ID (immutable after deployment) uint32 public immutable targetEid; /// @notice Address of the contract to read from on the target chain address public immutable targetContractAddress; /** * @notice Initialize the cross-chain read contract * @dev Sets up LayerZero connectivity and establishes read channel peer relationship * @param _endpoint LayerZero endpoint address on the source chain * @param _readChannel Read channel ID for this contract's operations * @param _targetEid Destination chain's endpoint ID where target contract lives * @param _targetContractAddress Contract address to read from on target chain */ constructor( address _endpoint, uint32 _readChannel, uint32 _targetEid, address _targetContractAddress ) OAppRead(_endpoint, msg.sender) Ownable(msg.sender) { READ_CHANNEL = _readChannel; targetEid = _targetEid; targetContractAddress = _targetContractAddress; // Establish read channel peer - contract reads from itself via LayerZero _setPeer(READ_CHANNEL, AddressCast.toBytes32(address(this))); } /** * @notice Configure the LayerZero read channel for this contract * @dev Owner-only function to activate/deactivate read channels * @param _channelId Read channel ID to configure * @param _active Whether to activate (true) or deactivate (false) the channel */ function setReadChannel(uint32 _channelId, bool _active) public override onlyOwner { // Set or clear the peer relationship for the read channel _setPeer(_channelId, _active ? AddressCast.toBytes32(address(this)) : bytes32(0)); READ_CHANNEL = _channelId; } /** * @notice Execute a cross-chain read request to call the target function * @dev Builds the read command and sends it via LayerZero messaging * @param _a First parameter for the target function * @param _b Second parameter for the target function * @param _extraOptions Additional execution options (gas, value, etc.) * @return receipt LayerZero messaging receipt containing transaction details */ function readSum( uint256 _a, uint256 _b, bytes calldata _extraOptions ) external payable returns (MessagingReceipt memory) { // 1. Build the read command specifying target function and parameters bytes memory cmd = _getCmd(_a, _b); // 2. Send the read request via LayerZero return _lzSend( READ_CHANNEL, cmd, combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions), MessagingFee(msg.value, 0), payable(msg.sender) ); } /** * @notice Get estimated messaging fee for a cross-chain read operation * @dev Calculates LayerZero fees before sending to avoid transaction failures * @param _a First parameter for the target function * @param _b Second parameter for the target function * @param _extraOptions Additional execution options * @return fee Estimated LayerZero messaging fee structure */ function quoteReadFee( uint256 _a, uint256 _b, bytes calldata _extraOptions ) external view returns (MessagingFee memory fee) { // Build the same command as readSum and quote its cost return _quote(READ_CHANNEL, _getCmd(_a, _b), combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions), false); } /** * @notice Build the LayerZero read command for target function execution * @dev Constructs EVMCallRequestV1 specifying what data to fetch and from where * @param _a First parameter to pass to target function * @param _b Second parameter to pass to target function * @return Encoded read command for LayerZero execution */ function _getCmd(uint256 _a, uint256 _b) internal view returns (bytes memory) { // 1. Build the function call data // Encode the target function selector with parameters bytes memory callData = abi.encodeWithSelector(IExampleContract.add.selector, _a, _b); // 2. Create the read request structure EVMCallRequestV1[] memory readRequests = new EVMCallRequestV1[](1); readRequests[0] = EVMCallRequestV1({ appRequestLabel: 1, // Request identifier for tracking targetEid: targetEid, // Which chain to read from isBlockNum: false, // Use timestamp instead of block number for data freshness blockNumOrTimestamp: uint64(block.timestamp), // Read current state confirmations: 15, // Wait for block finality before executing to: targetContractAddress, // Target contract address callData: callData // The function call to execute }); // 3. Encode the command (no compute logic needed for simple reads) return ReadCodecV1.encode(0, readRequests); } /** * @notice Process the received data from the target chain * @dev Called by LayerZero when the read response is delivered * @param _message Encoded response data from the target function call */ function _lzReceive( Origin calldata /*_origin*/, bytes32 /*_guid*/, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { // 1. Validate response format require(_message.length == 32, "Invalid message length"); // 2. Decode the returned data (matches target function return type) uint256 sum = abi.decode(_message, (uint256)); // 3. Process the result (emit event, update state, trigger logic, etc.) emit SumReceived(sum); } } // Example target contract for demonstration contract ExampleContract { /** * @notice Adds two numbers. * @param a First number. * @param b Second number. * @return sum The sum of a and b. */ function add(uint256 a, uint256 b) external pure returns (uint256 sum) { return a + b; } } ``` **Cross-Chain View/Pure Function Reading:** - Deploy `ReadViewOrPure` on your source network - Call `readSum(5, 10, "0x")` to execute the add function on the target chain - The contract's DVNs fetch the result directly and deliver it to `SumReceived(15)` event - No compute processing - raw response delivered directly to your contract This enables direct access to any view/pure function across supported chains without complex bridging infrastructure. #### Constructor - Pass the Endpoint V2 address, owner address, read channel ID, target chain ID, and target contract address - `OAppRead(_endpoint, msg.sender)` binds your contract to LayerZero and sets the delegate - `Ownable(msg.sender)` makes the deployer the only address that can change configurations - `_setPeer(READ_CHANNEL, AddressCast.toBytes32(address(this)))` establishes the read channel peer relationship #### readSum(...) 1. **Build the read command** - `_getCmd()` constructs the query specifying target function with parameters - Uses `IExampleContract.add.selector` for type-safe function selection 2. **Send the read request** - `_lzSend()` packages and dispatches the read request via LayerZero - `combineOptions()` merges enforced options with caller-provided options - Caller must provide sufficient native fee for cross-chain execution #### \_getCmd(...) 1. **Encode the function call** - Build `callData` using function selector and parameters - Standard ABI encoding for target contract interface 2. **Create the read request** - Single `EVMCallRequestV1` targeting specific chain and contract - `appRequestLabel: 1` for request tracking and identification - Uses current timestamp for fresh data reads 3. **Encode the command** - `ReadCodecV1.encode(0, readRequests)` with no compute logic - AppLabel 0 indicates basic read without additional processing #### \_lzReceive(...) 1. **Endpoint verification** - Only LayerZero Endpoint can invoke this function - Validates sender matches registered read channel peer 2. **Decode the response** - Extract raw data using `abi.decode(_message, (uint256))` - Data format matches target function's return type exactly 3. **Process the result** - Emit `SumReceived` event with the fetched data - Add custom business logic, state updates, or trigger additional operations #### (Optional) quoteReadFee(...) Estimates messaging fees before sending to avoid transaction failures: ```solidity // Get fee estimate first MessagingFee memory fee = readContract.quoteReadFee(5, 10, "0x"); // Then send with estimated fee readContract.readSum{value: fee.nativeFee}(5, 10, "0x"); ``` #### Real-world applications The example above shows a simple mathematical function, but lzRead can call any view/pure function across chains: ```solidity // Query token balances on other chains function getBalance(address user) external view returns (uint256); // Access oracle prices from different chains function getLatestPrice() external view returns (uint256 price, uint256 timestamp); // Check protocol states across deployments function getTotalSupply() external view returns (uint256); function getReserves() external view returns (uint112 reserve0, uint112 reserve1); // Validate conditions before cross-chain actions function isEligibleForRewards(address user) external view returns (bool eligible, uint256 amount); // Query governance states function getProposalState(uint256 proposalId) external view returns (uint8 state); ``` The key advantage is **data locality** - instead of bridging tokens or deploying contracts everywhere, you can query any chain's data directly and use it in your source chain logic. ### Add Compute Logic to Responses Add off-chain data processing to transform, validate, or format response data before it reaches your contract. The compute layer executes between the DVN(s) getting the response data from your target contract and delivering it to your `_lzReceive` function, allowing complex data manipulation without additional gas costs. **Core concept:** After DVNs fetch your requested data, the compute layer can process it off-chain using your custom `lzMap` and `lzReduce` functions. This enables data transformation, validation, aggregation, and formatting without consuming gas on your source chain. **How compute processing works:** 1. **DVNs fetch data** from your target contract using the specified function call 2. **lzMap executes** (if configured) to transform each individual response 3. **lzReduce executes** (if configured) to aggregate all mapped responses into a final result 4. **Final result delivered** to your `_lzReceive` function on the source chain The compute layer acts as a **middleware processing step** that runs off-chain but is cryptographically verified, giving you powerful data manipulation capabilities without gas costs. **Use cases:** - **Data transformation**: Convert complex structs into simpler formats your contract needs - **Response validation**: Filter out invalid responses or apply business logic rules - **Unit conversion**: Convert between different decimal places, currencies, or measurement units - **Data cleaning**: Remove outliers, normalize formats, or standardize responses - **Aggregation prep**: Process individual responses before combining them - **Format standardization**: Ensure all responses follow consistent encoding patterns **Key implementation details:** - Implement `IOAppMapper` for individual response processing and/or `IOAppReducer` for aggregation - Add `EVMCallComputeV1` struct to specify compute configuration - Configure `computeSetting`: `0` = lzMap only, `1` = lzReduce only, `2` = both - Compute functions execute off-chain, reducing gas costs for complex operations - `lzMap` processes each response individually; `lzReduce` combines all mapped responses #### Installation Get started quickly with a pre-built lzRead compute example: ```bash LZ_ENABLE_READ_EXAMPLE=1 npx create-lz-oapp@latest --example view-pure-read ``` The generated project includes the ReadViewOrPureAndCompute contract demonstrating the complete compute pipeline. #### Contract Example ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; // Import necessary interfaces and contracts import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { OAppRead } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppRead.sol"; // highlight-start import { IOAppMapper } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppMapper.sol"; import { IOAppReducer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppReducer.sol"; // highlight-end import { EVMCallRequestV1, EVMCallComputeV1, ReadCodecV1 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/ReadCodecV1.sol"; import { AddressCast } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/AddressCast.sol"; import { MessagingFee, MessagingReceipt, ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; /// @title IExampleContract /// @notice Interface for the ExampleContract's `add` function. interface IExampleContract { function add(uint256 a, uint256 b) external pure returns (uint256); } /// @title ReadViewOrPureAndCompute /// @notice Cross-chain read contract with compute processing for data transformation and aggregation contract ReadViewOrPureAndCompute is OAppRead, IOAppMapper, IOAppReducer, OAppOptionsType3 { /// @notice Emitted when final computed result is received from the compute pipeline event SumReceived(uint256 sum); /// @notice LayerZero read channel ID for cross-chain data requests with compute uint32 public READ_CHANNEL; /// @notice Message type identifier for read operations with compute processing uint16 public constant READ_TYPE = 1; /// @notice Target chain's LayerZero Endpoint ID (immutable after deployment) uint32 public immutable targetEid; /// @notice Address of the contract to read from on the target chain address public immutable targetContractAddress; /** * @notice Initialize the cross-chain read contract with compute capabilities * @dev Sets up LayerZero connectivity, establishes read channel, and enables compute processing * @param _endpoint LayerZero endpoint address on the source chain * @param _readChannel Read channel ID for this contract's operations * @param _targetEid Destination chain's endpoint ID where target contract lives * @param _targetContractAddress Contract address to read from on target chain */ constructor( address _endpoint, uint32 _readChannel, uint32 _targetEid, address _targetContractAddress ) OAppRead(_endpoint, msg.sender) Ownable(msg.sender) { READ_CHANNEL = _readChannel; targetEid = _targetEid; targetContractAddress = _targetContractAddress; // Establish read channel peer - contract processes its own compute functions _setPeer(READ_CHANNEL, AddressCast.toBytes32(address(this))); } /** * @notice Configure the LayerZero read channel for compute-enabled operations * @dev Owner-only function to activate/deactivate read channels with compute processing * @param _channelId Read channel ID to configure * @param _active Whether to activate (true) or deactivate (false) the channel */ function setReadChannel(uint32 _channelId, bool _active) public override onlyOwner { // Set or clear the peer relationship for compute-enabled read operations _setPeer(_channelId, _active ? AddressCast.toBytes32(address(this)) : bytes32(0)); READ_CHANNEL = _channelId; } /** * @notice Execute a cross-chain read request with compute processing pipeline (Step 1) * @dev Builds read command with compute configuration and sends via LayerZero * @param _a First parameter for the target function * @param _b Second parameter for the target function * @param _extraOptions Additional execution options (gas, value, etc.) * @return receipt LayerZero messaging receipt containing transaction details */ function readSum( uint256 _a, uint256 _b, bytes calldata _extraOptions ) external payable returns (MessagingReceipt memory) { // 1. Build the read command with compute configuration bytes memory cmd = _getCmd(_a, _b); // 2. Send the read request with compute processing enabled return _lzSend( READ_CHANNEL, cmd, combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions), MessagingFee(msg.value, 0), payable(msg.sender) ); } /** * @notice Get estimated messaging fee for cross-chain read with compute processing * @dev Calculates LayerZero fees including compute overhead before sending * @param _a First parameter for the target function * @param _b Second parameter for the target function * @param _extraOptions Additional execution options * @return fee Estimated LayerZero messaging fee structure */ function quoteReadFee( uint256 _a, uint256 _b, bytes calldata _extraOptions ) external view returns (MessagingFee memory fee) { // Build the same command as readSum (including compute config) and quote its cost return _quote(READ_CHANNEL, _getCmd(_a, _b), combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions), false); } /** * @notice Build the LayerZero read command with compute processing configuration * @dev Constructs both the target function call AND the compute pipeline setup * @param _a First parameter to pass to target function * @param _b Second parameter to pass to target function * @return Encoded read command with compute configuration for LayerZero execution */ function _getCmd(uint256 _a, uint256 _b) internal view returns (bytes memory) { // 1. Build the target function call data (same as basic read) bytes memory callData = abi.encodeWithSelector(IExampleContract.add.selector, _a, _b); // 2. Create the read request structure EVMCallRequestV1[] memory readRequests = new EVMCallRequestV1[](1); readRequests[0] = EVMCallRequestV1({ appRequestLabel: 1, // Request identifier for tracking through compute pipeline targetEid: targetEid, // Which chain to read from isBlockNum: false, // Use timestamp for data freshness blockNumOrTimestamp: uint64(block.timestamp), // Read current state confirmations: 15, // Wait for block finality before processing to: targetContractAddress, // Target contract address callData: callData // The function call to execute }); // highlight-start // 3. Configure the compute processing pipeline - THIS IS THE KEY DIFFERENCE EVMCallComputeV1 memory computeRequest = EVMCallComputeV1({ computeSetting: 2, // 0=lzMap only, 1=lzReduce only, 2=both lzMap and lzReduce targetEid: ILayerZeroEndpointV2(endpoint).eid(), // Execute compute on source chain (this chain) isBlockNum: false, // Use timestamp for compute execution timing blockNumOrTimestamp: uint64(block.timestamp), // When to execute compute functions confirmations: 15, // Confirmations needed before compute processing begins to: address(this) // Contract address containing lzMap/lzReduce implementations }); // 4. Encode the complete command (read requests + compute configuration) return ReadCodecV1.encode(0, readRequests, computeRequest); // highlight-end } // highlight-start /** * @notice Transform individual read responses during compute processing (Step 2 of compute pipeline) * @dev Called by LayerZero's compute layer for each raw response from target chains * @param _request Original request data (unused in this example, but available for context) * @param _response Raw response data from the target chain function call * @return Processed response data to pass to lzReduce (or final result if no reduce step) */ function lzMap( bytes calldata /*_request*/, bytes calldata _response ) external pure override returns (bytes memory) { // 1. Decode the raw response from target function (uint256 from add function) uint256 sum = abi.decode(_response, (uint256)); // 2. Apply transformation logic (example: increment by 1) // This could be: unit conversion, validation, filtering, formatting, etc. sum += 1; // 3. Re-encode for lzReduce or final delivery return abi.encode(sum); } /** * @notice Aggregate all mapped responses into final result (Step 3 of compute pipeline) * @dev Called after all lzMap operations complete, receives array of mapped responses * @param _cmd Original command data (unused in this example, but available for context) * @param _responses Array of processed responses from lzMap function * @return Final aggregated data to deliver to _lzReceive */ function lzReduce( bytes calldata /*_cmd*/, bytes[] calldata _responses ) external pure override returns (bytes memory) { uint256 totalSum = 0; // Process each mapped response and aggregate them for (uint256 i = 0; i < _responses.length; i++) { // 1. Validate each response format require(_responses[i].length == 32, "Invalid response length"); // 2. Decode the mapped response uint256 sum = abi.decode(_responses[i], (uint256)); // 3. Apply aggregation logic (example: sum all responses) totalSum += sum; } // 4. Return final aggregated result return abi.encode(totalSum); } // highlight-end /** * @notice Process the final computed result from the compute pipeline (Step 4) * @dev Called by LayerZero when compute processing is complete and result is delivered * @param _message Final processed data from the compute pipeline (lzReduce output) */ function _lzReceive( Origin calldata /*_origin*/, bytes32 /*_guid*/, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { // 1. Validate final result format require(_message.length == 32, "Invalid message length"); // 2. Decode the final computed result uint256 sum = abi.decode(_message, (uint256)); // 3. Process the result (emit event, update state, trigger logic, etc.) emit SumReceived(sum); } } ``` **Cross-Chain Reading with Compute Processing:** - Deploy `ReadViewOrPureAndCompute` on your source network - Call `readSum(5, 10, "0x")` to execute the add function on the target chain with compute processing - The contract's DVNs fetch the result, lzMap transforms it (+1), lzReduce aggregates each response (by default only 1 response, so unchanged), and the final computed result is delivered to `SumReceived(16)` event This enables sophisticated data processing pipelines where raw cross-chain data is transformed and aggregated off-chain before reaching your contract. #### Constructor - Initialize the contract with compute capabilities enabled via `IOAppMapper` and `IOAppReducer` interfaces - Sets up LayerZero connectivity and establishes read channel peer relationship for compute operations - The contract becomes both the read requester and the compute processor (via `address(this)` in compute configuration) #### readSum(...) **Step 1 of compute pipeline:** Dispatch read request with compute command 1. **Build the compute command** - `_getCmd()` constructs both the read request AND the compute configuration - Specifies which compute functions to use (`lzMap`, `lzReduce`, or both) 2. **Send the read request** - `_lzSend()` packages and dispatches the read request with compute processing enabled - Higher fees due to compute overhead compared to basic reads #### \_getCmd(...) **Key difference from basic reads:** Includes `EVMCallComputeV1` configuration - **Read request structure:** Same as basic pattern - specifies target function and parameters - **Compute configuration:** Defines the processing pipeline that will execute after data retrieval - `computeSetting: 2` enables both `lzMap` and `lzReduce` processing - `to: address(this)` specifies this contract contains the compute function implementations #### lzMap(...) **Step 2 of compute pipeline:** Individual response transformation 1. **Decode raw response** - Extract data from target chain function call result 2. **Apply transformation logic** - Convert formats, validate data, apply business rules - Example: increment by 1, but could be unit conversion, filtering, etc. 3. **Re-encode for next step** - Prepare data for `lzReduce` or final delivery to `_lzReceive` #### lzReduce(...) **Step 3 of compute pipeline:** Response aggregation 1. **Process mapped responses** - Receive array of all `lzMap` outputs - Validate each response format and content 2. **Apply aggregation logic** - Combine responses using your business logic - Example: sum all values, but could be averaging, min/max, weighted calculations 3. **Return final result** - Single aggregated value to deliver to `_lzReceive` #### \_lzReceive(...) **Step 4 of compute pipeline:** Final result processing 1. **Receive computed result** - Data has already been through `lzMap` and `lzReduce` processing - Final result is delivered, not raw target chain response 2. **Process final data** - Emit events, update state, trigger additional logic - Result represents the fully processed and aggregated data #### (Optional) quoteReadFee(...) Fee estimation includes compute processing overhead. Costs are higher than basic reads due to: - Additional compute execution processing - Data transformation and aggregation operations - Multiple processing steps in the pipeline **Example usage:** ```solidity // Get fee estimate for read with compute MessagingFee memory fee = readContract.quoteReadFee(5, 10, "0x"); // Send with computed processing readContract.readSum{value: fee.nativeFee}(5, 10, "0x"); ``` ### Call Non-View Functions lzRead can also query functions that aren't marked `view` or `pure`, but still return valuable data without modifying state. This pattern leverages `eth_call` to safely execute functions that would normally require gas, enabling access to sophisticated on-chain computations. **Core concept:** Many useful functions (especially in DeFi) aren't marked `view` because they rely on calling other non-view functions internally, even though they don't modify state. lzRead uses `eth_call` to execute these functions safely, capturing their return values without gas costs or state changes. **Use cases:** - **DEX price quotations**: Uniswap V3's `quoteExactInputSingle` simulates swaps to calculate output amounts - **Lending protocol queries**: Calculate borrow rates, collateral requirements, or liquidation thresholds - **Yield farming calculations**: Determine pending rewards, APR calculations, or harvest amounts - **Options pricing**: Complex mathematical models for derivative pricing - **Arbitrage detection**: Calculate profit opportunities across different protocols - **Liquidation analysis**: Determine if positions are liquidatable and expected returns **Why these functions aren't `view`:** - They call other non-view functions internally (like Uniswap's swap simulation) - They use try-catch blocks or other constructs that prevent `view` designation - They access external contracts that may not be `view`-compatible - They perform complex state reads that the compiler can't verify as non-modifying **Key implementation details:** - Functions must not revert during execution - test parameters thoroughly - Use proper struct encoding for complex parameters (like Uniswap's `QuoteExactInputSingleParams`) - Handle multi-return-value responses with correct ABI decoding - Target functions execute via `eth_call`, so no actual state changes or gas consumption occur - DVNs verify these calls can execute successfully before returning data #### Installation Get started quickly with a pre-built Uniswap V3 quote reader example: ```bash LZ_ENABLE_READ_EXAMPLE=1 npx create-lz-oapp@latest --example uniswap-read ``` This creates a complete project with: - Uniswap V3 QuoterV2 integration contracts - Non-view function calling examples - Multi-chain price aggregation patterns - Ready-to-deploy implementations for major chains #### Contract Example ```solidity // contracts/UniswapV3QuoteDemo.sol // // ────────────────────────────────────────────────────────────────────────────── // 1b. Read Command Construction // ────────────────────────────────────────────────────────────────────────────── /// @notice Constructs the read command to fetch Uniswap V3 quotes from each configured chain. /// @return cmd Encoded command for cross-chain price queries. function getCmd() public view returns (bytes memory cmd) { uint256 count = targetEids.length; EVMCallRequestV1[] memory requests = new EVMCallRequestV1[](count); for (uint256 i = 0; i < count; ++i) { uint32 eid = targetEids[i]; ChainConfig memory cfg = chainConfigs[eid]; bytes memory data = abi.encodeWithSelector( IQuoterV2.quoteExactInputSingle.selector, IQuoterV2.QuoteExactInputSingleParams({ tokenIn: cfg.tokenInAddress, tokenOut: cfg.tokenOutAddress, amountIn: 1 ether, fee: cfg.fee, sqrtPriceLimitX96: 0 }) ); requests[i] = EVMCallRequestV1({ appRequestLabel: uint16(i + 1), targetEid: eid, isBlockNum: false, blockNumOrTimestamp: uint64(block.timestamp), confirmations: cfg.confirmations, to: cfg.quoterAddress, callData: data }); } EVMCallComputeV1 memory compute = EVMCallComputeV1({ computeSetting: 2, targetEid: ILayerZeroEndpointV2(endpoint).eid(), isBlockNum: false, blockNumOrTimestamp: uint64(block.timestamp), confirmations: 15, to: address(this) }); return ReadCodecV1.encode(0, requests, compute); } // ────────────────────────────────────────────────────────────────────────────── // 2. Map & Reduce Logic // ────────────────────────────────────────────────────────────────────────────── /// @notice Maps individual Uniswap quote responses to encoded amounts. /// @param _response Raw response bytes from the quoted call. /// @return Encoded amountOut for a single chain. function lzMap(bytes calldata, bytes calldata _response) external pure returns (bytes memory) { require(_response.length >= 32, "Invalid response length"); (uint256 amountOut,,,) = abi.decode(_response, (uint256, uint160, uint32, uint256)); return abi.encode(amountOut); } /// @notice Reduces multiple mapped responses to a single average value. /// @param _responses Array of encoded amountOut responses from each chain. /// @return Encoded average of all responses. function lzReduce(bytes calldata, bytes[] calldata _responses) external pure returns (bytes memory) { require(_responses.length > 0, "No responses"); uint256 sum; for (uint i = 0; i < _responses.length; i++) { sum += abi.decode(_responses[i], (uint256)); } uint256 avg = sum / _responses.length; return abi.encode(avg); } // ────────────────────────────────────────────────────────────────────────────── // 3. Receive Business Logic // ────────────────────────────────────────────────────────────────────────────── /// @notice Handles the final averaged quote from LayerZero and emits the result. /// @dev _origin LayerZero origin metadata (unused). /// @dev _guid Unique message identifier (unused). /// @param _message Encoded average price bytes. function _lzReceive( Origin calldata /*_origin*/, bytes32 /*_guid*/, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { uint256 averagePrice = abi.decode(_message, (uint256)); emit AggregatedPrice(averagePrice); } ``` **Cross-Chain Price Aggregation Example:** - Deploy `UniswapV3QuoteDemo` on your source network (configured for Ethereum, Base, and Optimism) - Call `readAverageUniswapPrice("0x")` to query WETH/USDC prices across all three chains simultaneously - The contract's DVNs fetch prices from each chain's Uniswap V3 deployment - `lzMap` extracts the `amountOut` from each chain's complex response - `lzReduce` calculates the average price across all chains - Final averaged price is delivered to `AggregatedPrice(averagePrice)` event This enables sophisticated cross-chain price feeds, governance aggregation, and multi-chain protocol monitoring in a single transaction. #### Constructor Pre-configures three major chains with their respective Uniswap V3 deployments using hardcoded constants: - **Ethereum Mainnet**: EID 30101 with WETH/USDC addresses and QuoterV2 contract - **Base Mainnet**: EID 30184 with chain-specific token addresses - **Optimism Mainnet**: EID 30111 with chain-specific token addresses - Sets up LayerZero connectivity and establishes read channel peer relationship - **Key advantage:** Ready-to-deploy with major chains pre-configured #### readAverageUniswapPrice(...) 1. **Build multi-chain command** - `getCmd()` constructs read requests for ALL three configured chains - Each request queries `quoteExactInputSingle` with 1 WETH input amount 2. **Send aggregated request** - Single `_lzSend()` operation handles all three chains simultaneously - More cost-effective than separate requests per chain - Includes compute configuration for price averaging #### getCmd(...) **Multi-chain request construction with compute:** 1. **Iterate through target chains** - Build `EVMCallRequestV1` for Ethereum, Base, and Optimism - Use unique `appRequestLabel` (1, 2, 3) to track responses during compute processing 2. **Chain-specific parameters** - Each request uses that chain's specific QuoterV2, WETH, and USDC addresses - Maintains consistent `amountIn: 1 ether` across all chains for comparable results - Uses chain-specific confirmation requirements (5 blocks each) 3. **Compute configuration** - `computeSetting: 2` enables both `lzMap` and `lzReduce` for response processing - `targetEid` points to source chain for compute execution - `to: address(this)` specifies this contract contains the compute functions #### lzMap(...) - Price Extraction **Individual chain response processing:** ```solidity // Uniswap returns: (amountOut, sqrtPriceX96After, initializedTicksCrossed, gasEstimate) (uint256 amountOut,,,) = abi.decode(_response, (uint256, uint160, uint32, uint256)); return abi.encode(amountOut); // Extract only the price data we need ``` **Purpose:** Extract `amountOut` (USDC amount for 1 WETH) from Uniswap's complex 4-value response, normalizing all chains to simple price values. #### lzReduce(...) - Price Averaging **Cross-chain aggregation logic:** ```solidity uint256 sum; for (uint i = 0; i < _responses.length; i++) { sum += abi.decode(_responses[i], (uint256)); } uint256 avg = sum / _responses.length; // Simple average across 3 chains ``` **Current implementation:** Simple arithmetic mean of all three chain prices. **Potential enhancements:** - **Weighted averaging:** Weight by liquidity, volume, or chain importance - **Outlier filtering:** Remove prices that deviate significantly from median - **Confidence scoring:** Account for different chain finality requirements #### \_lzReceive(...) - Final Price Delivery Receives the final aggregated price representing the cross-chain average WETH/USDC price: - **Event emission:** Emits `AggregatedPrice(averagePrice)` with the computed average - **Result format:** Single `uint256` representing average USDC amount for 1 WETH across all three chains **Use cases for the aggregated price:** - **Cross-chain arbitrage detection:** Compare with local prices to find opportunities - **Multi-chain price oracles:** Provide robust price feeds aggregating multiple sources - **Risk management:** Monitor price discrepancies across deployments - **Liquidity routing:** Direct users to chains with optimal pricing #### Architecture Benefits **Single Transaction Efficiency:** - One read request handles 3+ chains instead of separate transactions - Reduced gas costs and complexity compared to multiple individual requests **Atomic Consistency:** - All chain data fetched and processed together - No timing discrepancies between separate async requests **Failure Resilience:** - Built-in retry logic across all target chains - Graceful handling of individual chain failures without affecting others **Why These Functions Aren't View:** - **Internal non-view calls:** Uniswap's quoter calls other non-view functions internally during swap simulation - **Try-catch blocks:** Error handling constructs prevent `view` designation even when no state changes occur - **Compiler restrictions:** Complex state reads that the compiler can't verify as non-modifying **DVN Verification:** DVNs use `eth_call` to execute these functions, ensuring: - No actual state changes occur on the target chain - No gas consumption on the target chain - Results are cryptographically verified and delivered to your source chain ### Multi-Chain Aggregation Execute identical or related queries across multiple chains simultaneously and combine the results into a single, meaningful response. This is lzRead's most powerful pattern, enabling true cross-chain data synthesis and decision-making. **Core concept:** Instead of making separate read requests to different chains and manually combining results, multi-chain aggregation fetches data from multiple networks in a single lzRead command. The compute layer processes and combines all responses before delivering the final result to your source chain. **Use cases:** - **Cross-chain price feeds**: Get token prices from major DEXes on different chains and calculate weighted averages - **Multi-chain governance**: Aggregate voting results across different network deployments of your protocol - **Liquidity analysis**: Compare pool depths, trading volumes, and rates across chains to find optimal routing - **Risk assessment**: Analyze protocol health by checking reserves, utilization rates, and other metrics across deployments - **Arbitrage detection**: Find price discrepancies and calculate potential profits across multiple networks - **Portfolio valuation**: Calculate total holdings by querying balances and prices across user's multi-chain positions - **Protocol synchronization**: Monitor and compare state across different chain deployments **Architecture benefits:** - **Single transaction cost**: One read request handles multiple chains instead of separate transactions - **Atomic aggregation**: All chain data is processed together, ensuring consistency - **Reduced complexity**: No need to manage multiple async requests or coordinate responses - **Gas efficiency**: Compute processing happens off-chain, minimizing source chain gas usage - **Failure handling**: Built-in retry and error handling across all target chains **Key implementation details:** - Array of `EVMCallRequestV1` structs, each targeting different chains/contracts - Unique `appRequestLabel` for each request to track responses during compute processing - `lzMap` processes each chain's response individually (normalization, validation) - `lzReduce` combines all mapped responses into final aggregated result - DVNs must support all target chains specified in your requests #### Contract Example Refer to the same `UniswapV3QuoteDemo` contract under [Non-View Functions](#call-non-view-functions) #### Architecture Benefits **Single Transaction Efficiency:** - One read request handles 3+ chains instead of separate transactions - Reduced gas costs and complexity compared to multiple individual requests **Atomic Consistency:** - All chain data fetched and processed together - No timing discrepancies between separate async requests **Failure Resilience:** - Built-in retry logic across all target chains - Graceful handling of individual chain failures without affecting others ### Hybrid Messaging + Read For applications that need both messaging and read capabilities: ```solidity contract HybridApp is OAppRead { uint32 constant READ_CHANNEL_THRESHOLD = 4294965694; function _lzReceive( Origin calldata _origin, bytes32 _guid, bytes calldata _message, address _executor, bytes calldata _extraData ) internal override { if (_origin.srcEid > READ_CHANNEL_THRESHOLD) { // Handle read responses _handleReadResponse(_message); } else { // Handle regular messages _handleMessage(_origin, _message); } } function _handleReadResponse(bytes calldata _message) internal { // Process read response data uint256 price = abi.decode(_message, (uint256)); // Update application state with fetched data } function _handleMessage(Origin calldata _origin, bytes calldata _message) internal { // Process regular cross-chain messages string memory data = abi.decode(_message, (string)); // Handle standard messaging logic } } ``` ## Debugging lzRead introduces unique challenges compared to standard LayerZero messaging. This comprehensive debugging guide covers common pitfalls, specific error scenarios, and practical solutions to help you troubleshoot lzRead implementations effectively. #### 1. Incorrect Execution Options Type **❌ Problem:** Using standard messaging options instead of lzRead-specific options causes transaction reverts. **Root Cause:** lzRead requires `addExecutorLzReadOption` with calldata size estimation, not `addExecutorLzReceiveOption`. ```solidity // ❌ WRONG - Standard messaging options OptionsBuilder.newOptions().addExecutorLzReceiveOption(100000, 0); // ✅ CORRECT - lzRead options with size estimation OptionsBuilder.newOptions().addExecutorLzReadOption(100000, 64, 0); // gas size value ``` **Solution:** Generate the correct option for use in your `enforcedOptions` or `extraOptions`: ```solidity import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; function getReadOptions(uint256 responseSize) internal pure returns (bytes memory) { return OptionsBuilder .newOptions() .addExecutorLzReadOption( 200000, // Gas limit for response processing responseSize, // Expected response data size in bytes 0 // Native value (usually 0 for reads) ); } ``` #### 2. Target Function Reverts (DVN Fulfillment Failure) **❌ Problem:** When the target function reverts during execution, DVNs cannot fulfill the request, causing the entire read operation to fail. **Root Cause:** DVNs use `eth_call` to execute target functions. If the function reverts with the provided parameters, verification cannot complete. **Common Revert Scenarios:** - Invalid parameters passed to target function - Target contract state changes between request and execution - Insufficient target chain block confirmations - Target function has built-in parameter validation that fails ```solidity // ❌ PROBLEMATIC - Function may revert with certain parameters function riskyRead() external payable { bytes memory callData = abi.encodeWithSelector( IToken.balanceOf.selector, address(0) // Zero address may cause revert in some implementations ); // ... rest of read logic } // ✅ SAFE - Validate parameters and use defensive programming function safeRead(address tokenHolder) external payable { require(tokenHolder != address(0), "Invalid holder address"); require(tokenHolder.code.length > 0, "Not a contract"); // If targeting contracts bytes memory callData = abi.encodeWithSelector( IToken.balanceOf.selector, tokenHolder ); // ... rest of read logic } ``` **Debug Strategy:** ```solidity // Test target function locally before using in lzRead function testTargetFunction(address target, bytes memory callData) external view returns (bool success, bytes memory result) { (success, result) = target.staticcall(callData); if (!success) { // Log the revert reason for debugging if (result.length > 0) { assembly { let returndata_size := mload(result) revert(add(32, result), returndata_size) } } } } ``` **Prevention Checklist:** - ✅ Test target function calls with your exact parameters on target chain - ✅ Ensure target contract exists at the specified address - ✅ Verify function selector matches target contract interface - ✅ Check that target function doesn't have restrictive access controls - ✅ Test with realistic parameter ranges and edge cases #### 3. Nonce Ordering Issues (Sequential Verification Failure) **❌ Problem:** If the first nonce in a sequence fails, all subsequent nonces are blocked because LayerZero verification is ordered. **Root Cause:** LayerZero processes nonces sequentially. A failed or stuck nonce prevents processing of later nonces until resolved. ```solidity // ❌ PROBLEMATIC - Multiple rapid requests without nonce management function multipleReads() external payable { // These requests will be processed sequentially by nonce readData(target1, eid1); // Nonce N readData(target2, eid2); // Nonce N+1 - blocked if N fails readData(target3, eid3); // Nonce N+2 - blocked if N or N+1 fails } ``` **Error Indicators:** - Later transactions succeed but never receive responses - LayerZero scan shows "Verified" but not "Delivered" for subsequent messages - Nonce gaps in your application's message history **Recovery Strategy:** 1. **Identify the failed nonce** causing the blockage using nonce status checks 2. **Use `endpoint.skip()`** to bypass the failed nonce and unblock subsequent processing 3. **Ensure subsequent requests** are properly formatted and verifiable before sending new reads 4. **Implement prevention** by validating all parameters before sending future requests See the [Endpoint - Skip](../troubleshooting/debugging-messages.md#skipping-nonce) section to see how to skip a nonce. #### 4. Calldata Size Estimation Errors **❌ Problem:** Underestimating response size in `addExecutorLzReadOption` causes executor delivery failures. **Root Cause:** Executors pre-allocate gas based on your size estimate. If the actual response exceeds this size, automatic delivery fails. ```solidity // ❌ UNDERESTIMATED - Will fail if response is larger than 32 bytes OptionsBuilder.newOptions().addExecutorLzReadOption(100000, 32, 0); // But target function returns: (uint256, address, string) ≈ 100+ bytes function complexTargetFunction() external view returns (uint256, address, string memory); ``` **Size Calculation Guide:** ```solidity contract SizeEstimator { // Calculate response sizes for common types function estimateResponseSize(bytes memory sampleResponse) external pure returns (uint256) { return sampleResponse.length; } // Common type sizes (for reference): // uint256: 32 bytes // address: 32 bytes (padded) // bool: 32 bytes (padded) // bytes32: 32 bytes // string: 32 + length + padding // dynamic array: 32 + (element_size * length) // tuple: sum of all element sizes } // ✅ PROPER SIZE ESTIMATION function getOptionsWithCorrectSize(uint256 expectedStringLength) internal pure returns (bytes memory) { uint256 estimatedSize = 32 + // uint256 32 + // address 32 + // string length expectedStringLength + // string content 32; // padding buffer return OptionsBuilder .newOptions() .addExecutorLzReadOption(200000, estimatedSize, 0); } ``` #### 5. Block Number vs Timestamp on L2 Chains **❌ Problem:** Using `block.number` on L2 chains often references L1 block numbers, causing timing and finality issues. **Root Cause:** Many L2s inherit block numbers from their L1 parent chain, making `block.number` unsuitable for timing-sensitive operations. **Affected Chains:** - **Arbitrum**: `block.number` returns L1 block number, not L2 sequence numbers - **Optimism**: Similar L1 block number inheritance in some contexts ```solidity // ❌ PROBLEMATIC on L2s - May reference L1 blocks EVMCallRequestV1({ // ... isBlockNum: true, blockNumOrTimestamp: uint64(block.number), // This is L1 block number on Arbitrum! // ... }); // ✅ RECOMMENDED - Use timestamps for universal compatibility EVMCallRequestV1({ // ... isBlockNum: false, blockNumOrTimestamp: uint64(block.timestamp), // Works consistently across all chains // ... }); ``` ## Further Reading - [Read Standard Overview](../../../concepts/applications/read-standard.md) - Conceptual information - [Read Paths & DVNs](../../../deployments/read-contracts.md) - Available chains and DVNs - [Execution Options](../configuration/options.md) - Options configuration --- --- 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** enables fungible tokens to exist across multiple blockchains while maintaining a unified supply. The OFT standard works by **debiting** an amount of tokens from a sender on the source chain and **crediting** the same amount of tokens to a receiver on the destination chain. #### OFT The `_debit` function in `OFT.sol` burns an amount of an ERC20 token, while `_credit` mints ERC20 tokens on the destination chain. ![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` and inherits `ERC20`, providing both cross-chain messaging and standard token functionality: ![OFT Inheritance](/img/oft-inheritance-light.svg#gh-light-mode-only) ![OFT Inheritance](/img/oft-inheritance-dark.svg#gh-dark-mode-only) #### OFT Adapter `OFTAdapter.sol` can be used for already deployed ERC20 tokens who lack mint capabilities, so that the `_debit` function calls `safeERC20.transferFrom` from a sender, while `_credit` calls `safeERC20.transfer` to a receiver. ![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` provides token bridging without modifying the original ERC20 token contract: ![OFT Adapter Inheritance](/img/oft-adapter-inheritance-light.svg#gh-light-mode-only) ![OFT Adapter Inheritance](/img/oft-adapter-inheritance-dark.svg#gh-dark-mode-only) :::tip If your use case involves cross-chain messaging beyond token transfers, consider using the [**OApp Standard**](../oapp/overview.md) for maximum flexibility. ::: :::info For detailed technical information about transfer flows, decimal handling, and architecture patterns, see the [**OFT Technical Reference**](../../../concepts/technical-reference/oft-reference.md). ::: ## Installation Below, you can find instructions for installing the OFT contract: ### OFT in a new project To start using LayerZero OFT contracts in a new project, use the LayerZero CLI tool, [**create-lz-oapp**](../../../get-started/create-lz-oapp/start.md). The CLI tool 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 --example oft ``` This will create an example repository containing both the Hardhat and Foundry frameworks, LayerZero development utilities, as well as the **OFT contract package** pre-installed. ### OFT in an existing project To use LayerZero contracts in an existing project, you can install the **OFT package** directly: ```bash npm install @layerzerolabs/oft-evm ``` ```bash yarn add @layerzerolabs/oft-evm ``` ```bash pnpm add @layerzerolabs/oft-evm ``` ```bash forge init ``` ```bash forge install layerzero-labs/devtools forge install layerzero-labs/LayerZero-v2 forge install OpenZeppelin/openzeppelin-contracts git submodule add https://github.com/GNSPS/solidity-bytes-utils.git lib/solidity-bytes-utils ``` Then add to your `foundry.toml` under `[profile.default]`: ```toml [profile.default] src = "src" out = "out" libs = ["lib"] remappings = [ '@layerzerolabs/oft-evm/=lib/devtools/packages/oft-evm/', '@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/', '@layerzerolabs/lz-evm-protocol-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/protocol', '@layerzerolabs/lz-evm-messagelib-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/messagelib', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', 'solidity-bytes-utils/=lib/solidity-bytes-utils/', ] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options ``` :::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", } ``` ::: ## Custom OFT Contract To build your own omnichain token contract, inherit from `OFT.sol` or `OFTAdapter.sol` depending on whether you're creating a new token or bridging an existing one. Below is a complete example showing the key pieces you need to implement: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol"; /// @notice OFT is an ERC-20 token that extends the OFTCore contract. contract MyOFT is OFT { constructor( string memory _name, string memory _symbol, address _lzEndpoint, address _owner ) OFT(_name, _symbol, _lzEndpoint, _owner) Ownable(_owner) {} } ``` :::tip Remember to add the ERC20 `_mint` method either in the constructor or as a protected `mint` function before deploying. ::: This contract provides a complete omnichain ERC20 implementation. The OFT automatically handles: - **Burning tokens** on the source chain when sending - **Minting tokens** on the destination chain when receiving - **Decimal precision** conversion between different chains - **Unified supply** management across all networks ```solidity // 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 lockbox in your omnichain deployment.** Multiple adapters break unified liquidity and can cause permanent token loss due to insufficient destination supply. ::: The OFT Adapter enables existing ERC20 tokens to become omnichain without code changes. The adapter: - **Locks tokens** in the adapter contract when sending - **Unlocks tokens** from the adapter when receiving - **Requires approval** of the underlying token for transfers - **Maintains the original token** contract unchanged ### Constructor - Pass the Endpoint V2 address and owner address into the base contracts. - `OFT(_name, _symbol, _lzEndpoint, _owner)` binds your contract to the local LayerZero Endpoint V2 and registers the delegate - `Ownable(_owner)` makes `_owner` the only address that can change configurations (such as peers, enforced options, and delegate) - After deployment, the owner can call: - `setConfig(...)` to adjust library or DVN parameters - `setSendLibrary(...)` and `setReceiveLibrary(...)` to override default libraries - `setPeer(...)` to whitelist remote OFT addresses - `setDelegate(...)` to assign a different delegate address - `setEnforcedOptions(...)` to set mandatory execution options ## Deployment and Wiring After you finish writing and testing your `MyOFT` contract, follow these steps to deploy it on each network and wire up the messaging stack. :::tip We **strongly recommend** using the LayerZero CLI tool to manage your configurations. Our config generator simplifies access to all available deployments across networks and is the preferred method for cross-chain messaging. See the [**CLI Guide**](../../../get-started/create-lz-oapp/start.md) for examples and how to use it in your project. ::: ### 1. Deploy Your OFT Contract Deploy `MyOFT` on each chain using either the LayerZero CLI (recommended) or manual deployment scripts. After running `pnpm compile` at the root level of your example repo, you can deploy your contracts. #### Network Configuration Before using the CLI, you'll need to configure your networks in `hardhat.config.ts` with LayerZero [Endpoint IDs (EIDs)](/v2/concepts/glossary#endpoint-id) and declare an RPC URL in your `.env` or directly in the config file: ```typescript // hardhat.config.ts import { EndpointId } from '@layerzerolabs/lz-definitions' // ... rest of hardhat config omitted for brevity networks: { 'optimism-sepolia-testnet': { // highlight-next-line eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, 'arbitrum-sepolia-testnet': { // highlight-next-line eid: EndpointId.ARBSEP_V2_TESTNET, url: process.env.RPC_URL_ARB_SEPOLIA || 'https://arbitrum-sepolia.gateway.tenderly.co', accounts, }, } ``` :::info The key addition to a standard `hardhat.config.ts` is the inclusion of LayerZero Endpoint IDs (`eid`) for each network. Check the [Deployments](../../../deployments/deployed-contracts.md) section for all available endpoint IDs. ::: The LayerZero CLI provides automated deployment with built-in endpoint detection based on your `hardhat.config.ts` networks object: ```bash # Deploy using interactive prompts npx hardhat lz:deploy ``` The CLI will prompt you to: 1. **Select chains to deploy to:** ```bash ? Which networks would you like to deploy? › ◉ fuji ◉ amoy ◉ sepolia ``` 2. **Choose deploy script tags:** ```bash ? Which deploy script tags would you like to use? › MyOFT ``` 3. **Confirm deployment:** ```bash ✔ Do you want to continue? … yes Network: amoy Deployer: 0x0000000000000000000000000000000000000000 Network: sepolia Deployer: 0x0000000000000000000000000000000000000000 Deployed contract: MyOApp, network: amoy, address: 0x0000000000000000000000000000000000000000 Deployed contract: MyOApp, network: sepolia, address: 0x0000000000000000000000000000000000000000 ``` The CLI automatically: - Detects the correct LayerZero Endpoint V2 address for each chain - Deploys your OApp contract with proper constructor arguments - Generates deployment artifacts in `./deployments/` folder - Creates network-specific deployment files (e.g., `deployments/sepolia/MyOApp.json`) For manual deployment using Foundry, create a deployment script that handles endpoint addresses: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { MyOApp } from "../contracts/MyOApp.sol"; contract DeployOApp is Script { function run() external { // Replace these env vars with your own values address endpoint = vm.envAddress("ENDPOINT_ADDRESS"); address owner = vm.envAddress("OWNER_ADDRESS"); vm.startBroadcast(vm.envUint("PRIVATE_KEY")); MyOApp oapp = new MyOApp(endpoint, owner); vm.stopBroadcast(); console.log("MyOApp deployed to:", address(oapp)); } } ``` Run the deployment script: ```bash # Deploy to testnet forge script script/DeployOApp.s.sol --rpc-url $RPC_URL --broadcast --verify # Deploy to multiple chains forge script script/DeployOApp.s.sol --rpc-url $ETHEREUM_RPC --broadcast --verify forge script script/DeployOApp.s.sol --rpc-url $POLYGON_RPC --broadcast --verify ``` You'll need to set the correct LayerZero Endpoint V2 addresses for each chain in your environment variables. Check the [Deployments](../../../deployments/deployed-contracts.md) section for endpoint addresses. ### 2. Wire Messaging Libraries and Configurations Once your contracts are on-chain, you must set up send/receive libraries and DVN/Executor settings so cross-chain messages flow correctly. The LayerZero CLI automatically handles all wiring via a single configuration file and command: #### Configuration File In your project root, you can find a `layerzero.config.ts` file: ```typescript import {EndpointId} from '@layerzerolabs/lz-definitions'; import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; import {OAppEnforcedOption, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; // This contract object defines the OApp deployment on Optimism Sepolia testnet // The config references the contract deployment from your ./deployments folder const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', }; const arbitrumContract: OmniPointHardhat = { eid: EndpointId.ARBSEP_V2_TESTNET, contractName: 'MyOFT', }; // For this example's simplicity, we will use the same enforced options values for sending to all chains // For production, you should ensure `gas` is set to the correct value through profiling the gas usage of calling OApp._lzReceive(...) on the destination chain // To learn more, read https://docs.layerzero.network/v2/concepts/applications/oapp-standard#execution-options-and-enforced-settings const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 80000, value: 0, }, ]; // To connect all the above chains to each other, we need the following pathways: // Optimism <-> Arbitrum // With the config generator, pathways declared are automatically bidirectional // i.e. if you declare A,B there's no need to declare B,A const pathways: TwoWayConfig[] = [ [ optimismContract, // Chain A contract arbitrumContract, // Chain C contract [['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [1, 1], // [A to B confirmations, B to A confirmations] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain C enforcedOptions, Chain A enforcedOptions ], ]; export default async function () { // Generate the connections config based on the pathways const connections = await generateConnectionsConfig(pathways); return { contracts: [{contract: optimismContract}, {contract: arbitrumContract}], connections, }; } ``` Make sure your contract object's `contractName` matches the named deployment file for the network under `./deployments/`. #### Wire Everything Run a single command to configure all pathways: ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` This automatically handles: - Fetching the necessary contract addresses for each network from metadata - Setting send and receive libraries - Configuring DVNs and Executors - Setting up peers between contracts - Applying enforced options - All bidirectional pathways in your config For manual configuration using Foundry scripts, follow these steps: #### Environment Setup Here's a comprehensive `.env.example` file showing all the environment variables needed for the different configuration scripts: ```bash # Common variables used across scripts ENDPOINT_ADDRESS=0x... # LayerZero Endpoint V2 address OAPP_ADDRESS=0x... # Your OApp contract address SIGNER=0x... # Address with permissions to configure/send # Library Configuration (SetLibraries.s.sol) SEND_LIB_ADDRESS=0x... # SendUln302 address RECEIVE_LIB_ADDRESS=0x... # ReceiveUln302 address DST_EID=30101 # Destination chain EID SRC_EID=30110 # Source chain EID GRACE_PERIOD=0 # Grace period for library switch (0 for immediate) # Send Config (SetSendConfig.s.sol) SOURCE_ENDPOINT_ADDRESS=0x... # Chain A Endpoint address SENDER_OAPP_ADDRESS=0x... # OApp on Chain A REMOTE_EID=30101 # Endpoint ID for Chain B # Peer Configuration (SetPeers.s.sol) CHAIN1_EID=30101 # First chain EID CHAIN1_PEER=0x... # OApp address on first chain CHAIN2_EID=30110 # Second chain EID CHAIN2_PEER=0x... # OApp address on second chain CHAIN3_EID=30111 # Third chain EID CHAIN3_PEER=0x... # OApp address on third chain # Message Sending (SendMessage.s.sol) MESSAGE="Hello World" # Message to send cross-chain ``` #### 2.1 Set Send and Receive Libraries 1. **Choose your libraries** (addresses of deployed MessageLib contracts). For standard cross-chain messaging, you should use `SendUln302.sol` for `setSendLibrary(...)` and `ReceiveUln302.sol` for `setReceiveLibrary(...)`. You can find the deployments for these contracts under the [Deployments](../../../deployments/deployed-contracts.md) section. 2. Call `setSendLibrary(oappAddress, dstEid, sendLibAddress)` on the Endpoint. 3. Call `setReceiveLibrary(oappAddress, srcEid, receiveLibAddress, gracePeriod)` on the Endpoint. ```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"; /// @title LayerZero Library Configuration Script /// @notice Sets up send and receive libraries for OApp messaging contract SetLibraries is Script { function run() external { // Load environment variables address endpoint = vm.envAddress("ENDPOINT_ADDRESS"); // LayerZero Endpoint address address oapp = vm.envAddress("OAPP_ADDRESS"); // Your OApp contract address address signer = vm.envAddress("SIGNER"); // Address with permissions to configure // Library addresses address sendLib = vm.envAddress("SEND_LIB_ADDRESS"); // SendUln302 address address receiveLib = vm.envAddress("RECEIVE_LIB_ADDRESS"); // ReceiveUln302 address // Chain configurations uint32 dstEid = uint32(vm.envUint("DST_EID")); // Destination chain EID uint32 srcEid = uint32(vm.envUint("SRC_EID")); // Source chain EID uint32 gracePeriod = uint32(vm.envUint("GRACE_PERIOD")); // Grace period for library switch vm.startBroadcast(signer); // Set send library for outbound messages ILayerZeroEndpointV2(endpoint).setSendLibrary( oapp, // OApp address dstEid, // Destination chain EID sendLib // SendUln302 address ); // Set receive library for inbound messages ILayerZeroEndpointV2(endpoint).setReceiveLibrary( oapp, // OApp address srcEid, // Source chain EID receiveLib, // ReceiveUln302 address gracePeriod // Grace period for library switch ); vm.stopBroadcast(); } } ``` You would need to set up your `.env` file with the appropriate values: ```env ENDPOINT_ADDRESS=0x... OAPP_ADDRESS=0x... SIGNER=0x... SEND_LIB_ADDRESS=0x... # SendUln302 address RECEIVE_LIB_ADDRESS=0x... # ReceiveUln302 address DST_EID=30101 SRC_EID=30110 GRACE_PERIOD=0 # Set to 0 for immediate switch, or block number for gradual migration ``` #### 2.2 Set Send Config and Receive Config If you need non-default DVN or Executor settings (block confirmations, required DVNs, max message size, etc.), call `setConfig(...)` next. To see defaults, use `getConfig(...)`. **Send Config (A → B):** The send config is set on the source chain (Chain A) and applies to messages being sent from Chain A to Chain B. This config determines the DVN and Executor settings for outbound messages leaving Chain A and destined for Chain B. You must call `setConfig` on the Endpoint contract on Chain A, specifying the remote Endpoint ID for Chain B and the appropriate SendLib address for the A → B pathway. ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { ILayerZeroEndpointV2, SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; import { ExecutorConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBase.sol"; /// @title LayerZero Send Configuration Script (A → B) /// @notice Defines and applies ULN (DVN) + Executor configs for cross‑chain messages sent from Chain A to Chain B via LayerZero Endpoint V2. contract SetSendConfig is Script { uint32 constant EXECUTOR_CONFIG_TYPE = 1; uint32 constant ULN_CONFIG_TYPE = 2; /// @notice Broadcasts transactions to set both Send ULN and Executor configurations for messages sent from Chain A to Chain B function run() external { address endpoint = vm.envAddress("SOURCE_ENDPOINT_ADDRESS"); // Chain A Endpoint address oapp = vm.envAddress("SENDER_OAPP_ADDRESS"); // OApp on Chain A uint32 eid = uint32(vm.envUint("REMOTE_EID")); // Endpoint ID for Chain B address sendLib = vm.envAddress("SEND_LIB_ADDRESS"); // SendLib for A → B address signer = vm.envAddress("SIGNER"); /// @notice ULNConfig defines security parameters (DVNs + confirmation threshold) for A → B /// @notice Send config requests these settings to be applied to the DVNs and Executor for messages sent from A to B /// @dev 0 values will be interpretted as defaults, so to apply NIL settings, use: /// @dev uint8 internal constant NIL_DVN_COUNT = type(uint8).max; /// @dev uint64 internal constant NIL_CONFIRMATIONS = type(uint64).max; UlnConfig memory uln = UlnConfig({ confirmations: 15, // minimum block confirmations required on A before sending to B requiredDVNCount: 2, // number of DVNs required optionalDVNCount: type(uint8).max, // optional DVNs count, uint8 optionalDVNThreshold: 0, // optional DVN threshold requiredDVNs: [address(0x1111...), address(0x2222...)], // sorted list of required DVN addresses optionalDVNs: [] // sorted list of optional DVNs }); /// @notice ExecutorConfig sets message size limit + fee‑paying executor for A → B ExecutorConfig memory exec = ExecutorConfig({ maxMessageSize: 10000, // max bytes per cross-chain message executor: address(0x3333...) // address that pays destination execution fees on B }); bytes memory encodedUln = abi.encode(uln); bytes memory encodedExec = abi.encode(exec); SetConfigParam[] memory params = new SetConfigParam[](2); params[0] = SetConfigParam(eid, EXECUTOR_CONFIG_TYPE, encodedExec); params[1] = SetConfigParam(eid, ULN_CONFIG_TYPE, encodedUln); vm.startBroadcast(signer); ILayerZeroEndpointV2(endpoint).setConfig(oapp, sendLib, params); // Set config for messages sent from A to B vm.stopBroadcast(); } } ``` **Receive Config (B ← A):** The receive config is set on the destination chain (Chain B) and applies to messages being received on Chain B from Chain A. This config determines the DVN settings for inbound messages arriving from Chain A. You must call `setConfig` on the Endpoint contract on Chain B, specifying the remote Endpoint ID for Chain A and the appropriate ReceiveLib address for the B ← A pathway. ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { ILayerZeroEndpointV2, SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { UlnConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; /// @title LayerZero Receive Configuration Script (B ← A) /// @notice Defines and applies ULN (DVN) config for inbound message verification on Chain B for messages received from Chain A via LayerZero Endpoint V2. contract SetReceiveConfig is Script { uint32 constant RECEIVE_CONFIG_TYPE = 2; function run() external { address endpoint = vm.envAddress("ENDPOINT_ADDRESS"); // Chain B Endpoint address oapp = vm.envAddress("OAPP_ADDRESS"); // OApp on Chain B uint32 eid = uint32(vm.envUint("REMOTE_EID")); // Endpoint ID for Chain A address receiveLib= vm.envAddress("RECEIVE_LIB_ADDRESS"); // ReceiveLib for B ← A address signer = vm.envAddress("SIGNER"); /// @notice UlnConfig controls verification threshold for incoming messages from A to B /// @notice Receive config enforces these settings have been applied to the DVNs for messages received from A /// @dev 0 values will be interpretted as defaults, so to apply NIL settings, use: /// @dev uint8 internal constant NIL_DVN_COUNT = type(uint8).max; /// @dev uint64 internal constant NIL_CONFIRMATIONS = type(uint64).max; UlnConfig memory uln = UlnConfig({ confirmations: 15, // min block confirmations from source (A) requiredDVNCount: 2, // required DVNs for message acceptance optionalDVNCount: type(uint8).max, // optional DVNs count optionalDVNThreshold: 0, // optional DVN threshold requiredDVNs: [address(0x1111...), address(0x2222...)], // sorted required DVNs optionalDVNs: [] // no optional DVNs }); bytes memory encodedUln = abi.encode(uln); SetConfigParam[] memory params = new SetConfigParam[](1); params[0] = SetConfigParam(eid, RECEIVE_CONFIG_TYPE, encodedUln); vm.startBroadcast(signer); ILayerZeroEndpointV2(endpoint).setConfig(oapp, receiveLib, params); // Set config for messages received on B from A vm.stopBroadcast(); } } ``` #### 2.3 Set Peers Once you've finished your **OApp Configuration** you can open the messaging channel and connect your OApp deployments by calling `setPeer`. A peer is required to be set for each EID (or network). Ideally an OApp (or OFT) will have multiple peers set where one and only one peer exists for one EID. The function takes 2 arguments: `_eid`, the destination endpoint ID for the chain our other OApp contract lives on, and `_peer`, the destination OApp contract address in `bytes32` format. ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { MyOApp } from "../contracts/MyOApp.sol"; /// @title LayerZero OApp Peer Configuration Script /// @notice Sets up peer connections between OApp deployments on different chains contract SetPeers is Script { function run() external { // Load environment variables address oapp = vm.envAddress("OAPP_ADDRESS"); // Your OApp contract address address signer = vm.envAddress("SIGNER"); // Address with owner permissions // Example: Set peers for different chains // Format: (chain EID, peer address in bytes32) (uint32 eid1, bytes32 peer1) = (uint32(vm.envUint("CHAIN1_EID")), bytes32(uint256(uint160(vm.envAddress("CHAIN1_PEER"))))); (uint32 eid2, bytes32 peer2) = (uint32(vm.envUint("CHAIN2_EID")), bytes32(uint256(uint160(vm.envAddress("CHAIN2_PEER"))))); (uint32 eid3, bytes32 peer3) = (uint32(vm.envUint("CHAIN3_EID")), bytes32(uint256(uint160(vm.envAddress("CHAIN3_PEER"))))); vm.startBroadcast(signer); // Set peers for each chain MyOApp(oapp).setPeer(eid1, peer1); MyOApp(oapp).setPeer(eid2, peer2); MyOApp(oapp).setPeer(eid3, peer3); vm.stopBroadcast(); } } ``` :::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. ::: #### 2.4 Set Enforced Options Enforced options allow the OApp owner to set mandatory execution parameters that will be applied to all messages of a specific type sent to a destination chain. These options are automatically combined with any caller-provided options when using `OAppOptionsType3`. **Why use enforced options?** - Ensure sufficient gas is always allocated for message execution on the destination - Enforce payment for additional services like PreCrime verification - Set consistent execution parameters across all users of your OApp - Prevent failed deliveries due to insufficient gas ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { MyOApp } from "../contracts/MyOApp.sol"; import { EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; /// @title LayerZero OApp Enforced Options Configuration Script /// @notice Sets enforced execution options for specific message types and destinations contract SetEnforcedOptions is Script { using OptionsBuilder for bytes; function run() external { // Load environment variables address oapp = vm.envAddress("OAPP_ADDRESS"); // Your OApp contract address address signer = vm.envAddress("SIGNER"); // Address with owner permissions // Destination chain configurations uint32 dstEid1 = uint32(vm.envUint("DST_EID_1")); // First destination EID uint32 dstEid2 = uint32(vm.envUint("DST_EID_2")); // Second destination EID // Message type (should match your contract's constant) uint16 SEND = 1; // Message type for sendString function // Build options using OptionsBuilder bytes memory options1 = OptionsBuilder.newOptions().addExecutorLzReceiveOption(80000, 0); bytes memory options2 = OptionsBuilder.newOptions().addExecutorLzReceiveOption(100000, 0); // Create enforced options array EnforcedOptionParam[] memory enforcedOptions = new EnforcedOptionParam[](2); // Set enforced options for first destination enforcedOptions[0] = EnforcedOptionParam({ eid: dstEid1, msgType: SEND, options: options1 }); // Set enforced options for second destination enforcedOptions[1] = EnforcedOptionParam({ eid: dstEid2, msgType: SEND, options: options2 }); vm.startBroadcast(signer); // Set enforced options on the OApp MyOApp(oapp).setEnforcedOptions(enforcedOptions); vm.stopBroadcast(); console.log("Enforced options set successfully!"); console.log("Destination 1 EID:", dstEid1, "Gas:", 80000); console.log("Destination 2 EID:", dstEid2, "Gas:", 100000); } } ``` **Environment variables needed:** ```env OAPP_ADDRESS=0x... # Your deployed MyOApp address SIGNER=0x... # Address with owner permissions DST_EID_1=30101 # First destination endpoint ID DST_EID_2=30110 # Second destination endpoint ID ``` **Run the script:** ```bash forge script script/SetEnforcedOptions.s.sol --rpc-url $RPC_URL --broadcast ``` Once set, these enforced options will be automatically applied when using `combineOptions()` in your send functions, ensuring consistent execution parameters across all messages.

## Usage Once deployed and wired, you can begin sending tokens across chains. ### Send tokens The LayerZero CLI provides a convenient task for sending OFT tokens that automatically handles fee estimation and transaction execution. #### Using the Send Task The CLI includes a built-in `lz:oft:send` task that: 1. Finds your deployed OFT contract automatically 2. Quotes the gas cost using your OFT's `quoteSend()` function 3. Sends the tokens with the correct fee 4. Provides tracking links for the transaction **Basic usage:** ```bash npx hardhat lz:oft:send --src-eid 40232 --dst-eid 40231 --amount 1.5 --to 0x1234567890123456789012345678901234567890 ``` **Required Parameters:** - `--src-eid`: Source endpoint ID (e.g., 40232 for Optimism Sepolia) - `--dst-eid`: Destination endpoint ID (e.g., 40231 for Arbitrum Sepolia) - `--amount`: Amount to send in human readable units (e.g., "1.5") - `--to`: Recipient address (20-byte hex for EVM) **Optional Parameters:** - `--min-amount`: Minimum amount to receive for slippage protection (e.g., "1.4") - `--extra-options`: Additional gas units for lzReceive, lzCompose, or receiver address - `--compose-msg`: Arbitrary bytes message to deliver alongside the OFT - `--oft-address`: Override the source OFT address (if not using deployment artifacts) **Example with optional parameters:** ```bash npx hardhat lz:oft:send \ --src-eid 40232 \ --dst-eid 40231 \ --amount 10.0 \ --to 0x1234567890123456789012345678901234567890 \ --min-amount 9.5 \ --extra-options 0x00030100110100000000000000000000000000030d40 ``` The task automatically: - Finds your deployed OFT contract from deployment artifacts - Handles token approvals (for OFTAdapter) - Quotes the exact gas fee needed - Provides block explorer and LayerZero Scan links for tracking Remember to generate a fee estimate using `quoteSend` first, then pass the returned native gas amount as your `msg.value` If using the base `OFTAdapter.sol`, you will want to approve the adapter contract to spend your ERC20 tokens: ```solidity ERC20(tokenAddress).approve(adapterAddress, amount); ``` For manual token sending using Foundry, create a script that handles fee estimation and token transfer: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import "forge-std/Script.sol"; import { MyOFT } from "../contracts/MyOFT.sol"; import { SendParam } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; import { MessagingFee } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; contract SendOFT is Script { using OptionsBuilder for bytes; function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } function run() external { // Load environment variables address oftAddress = vm.envAddress("OFT_ADDRESS"); address toAddress = vm.envAddress("TO_ADDRESS"); uint256 tokensToSend = vm.envUint("TOKENS_TO_SEND"); uint32 dstEid = uint32(vm.envUint("DST_EID")); uint256 privateKey = vm.envUint("PRIVATE_KEY"); vm.startBroadcast(privateKey); MyOFT oft = MyOFT(oftAddress); // Build send parameters bytes memory extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0); SendParam memory sendParam = SendParam({ dstEid: dstEid, to: addressToBytes32(toAddress), amountLD: tokensToSend, minAmountLD: tokensToSend * 95 / 100, // 5% slippage tolerance extraOptions: extraOptions, composeMsg: "", oftCmd: "" }); // Get fee quote MessagingFee memory fee = oft.quoteSend(sendParam, false); console.log("Sending tokens..."); console.log("Fee amount:", fee.nativeFee); // Send tokens oft.send{value: fee.nativeFee}(sendParam, fee, msg.sender); vm.stopBroadcast(); } } ``` **Environment variables needed:** ```env OFT_ADDRESS=0x... # Your deployed OFT address TO_ADDRESS=0x... # Recipient address TOKENS_TO_SEND=1000000000000000000 # Amount in wei (18 decimals) DST_EID=30101 # Destination endpoint ID PRIVATE_KEY=0x... # Private key for sending ``` **Run the script:** ```bash forge script script/SendOFT.s.sol --rpc-url $RPC_URL --broadcast ``` ### Send tokens + call composer **Horizontal composability** allows your OFT to trigger additional actions on the destination chain through separate, containerized message packets. Unlike vertical composability (multiple calls in a single transaction), horizontal composability processes operations independently, providing better fault isolation and gas efficiency. ![Composed Light](/img/learn/Composed-Light.svg#gh-light-mode-only) ![Composed Dark](/img/learn/Composed-Dark.svg#gh-dark-mode-only) #### Benefits of Horizontal Composability - **Fault Isolation**: If a composed call fails, it doesn't revert the main token transfer - **Gas Efficiency**: Each step can have independent gas limits and execution options - **Flexible Workflows**: Complex multi-step operations can be broken into manageable pieces - **Non-Critical Operations**: Secondary actions (like swaps or staking) can fail without affecting token delivery #### Workflow Overview 1. **Token Transfer**: OFT processes the token transfer in `_lzReceive()` and credits tokens to the recipient 2. **Compose Message**: OFT calls `endpoint.sendCompose()` to queue a separate composed message 3. **Composer Execution**: The composer contract receives the message via `lzCompose()` and executes custom logic #### Sending with ComposeMsg When sending tokens with composed actions, set the `to` address to your composer contract and include your custom `composeMsg`: ```solidity SendParam memory sendParam = SendParam({ dstEid: dstEid, to: addressToBytes32(composerAddress), // Composer contract address, NOT end recipient amountLD: tokensToSend, minAmountLD: tokensToSend * 95 / 100, // highlight-start extraOptions: extraOptions, composeMsg: abi.encode(finalRecipient, swapParams), // Data for composer logic // highlight-end oftCmd: "" }); ``` :::tip **Message Encoding**: The OFT automatically includes additional context in the composed message: - Original sender address (`msg.sender` from source chain) - Token amount transferred (`amountLD`) - Your custom `composeMsg` payload - Message nonce and source endpoint ID Your composer should decode this full context using `OFTComposeMsgCodec` helper functions. ::: #### Execution Options for Composed Messages Composed messages require gas for **two separate executions**: 1. **Token Transfer (`lzReceive`)**: Credits tokens and queues the composed message 2. **Composer Call (`lzCompose`)**: Executes your custom logic in the composer contract ```solidity bytes memory options = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(65000, 0) // Token transfer + compose queuing .addExecutorLzComposeOption(0, 50000, 0); // Composer contract execution ``` :::caution **Two-Phase Gas Requirements**: - **`lzReceiveOption`**: Gas for token crediting + `endpoint.sendCompose()` call (varies with `composeMsg` size) - **`lzComposeOption`**: Gas for your composer contract's business logic (depends on complexity) Always test your composed implementation to determine adequate gas limits for both phases. If either phase runs out of gas, you'll need to manually retry the failed execution. ::: #### Using the CLI with Composed Messages The `lz:oft:send` task supports composed messages via the `--compose-msg` and `--extra-options` parameters: ```bash npx hardhat lz:oft:send \ --src-eid 40232 \ --dst-eid 40231 \ --amount 5 \ --to 0x1234567890123456789012345678901234567890 \ --compose-msg 0x000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd \ --extra-options 0x00030100110100000000000000000000000000fdfe00030200010000000000000000000000000000c350 ``` :::tip **Encoding Compose Messages**: The `--compose-msg` parameter expects hex-encoded bytes. You can encode data using: - **Online tools**: Use ethers.js playground or similar tools to encode your data - **Cast command**: `cast abi-encode "function_signature" param1 param2` - **Hardhat console**: `ethers.utils.defaultAbiCoder.encode(['address'], ['0x...'])` **Extra Options**: The `--extra-options` above includes both `lzReceiveOption` (gas: 65534) and `lzComposeOption` (index: 0, gas: 50000) for composed messages. ::: #### Implementing a Composer Contract The composer contract must implement `IOAppComposer` to handle composed messages. Here's a comprehensive example: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import { IOAppComposer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol"; import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; /** * @title TokenSwapper * @notice Receives OFT tokens and automatically swaps them for another token */ contract TokenSwapper is IOAppComposer { using SafeERC20 for IERC20; /// @notice LayerZero endpoint address address public immutable endpoint; /// @notice Trusted OFT that can send composed messages address public immutable trustedOFT; /// @notice Token to swap to IERC20 public immutable targetToken; event TokenSwapped( address indexed originalSender, address indexed recipient, uint256 amountIn, uint256 amountOut ); constructor(address _endpoint, address _trustedOFT, address _targetToken) { endpoint = _endpoint; trustedOFT = _trustedOFT; targetToken = IERC20(_targetToken); } /** * @notice Handles composed messages from the OFT * @param _oApp Address of the originating OApp (must be trusted OFT) * @param _guid Unique identifier for this message * @param _message Encoded message containing compose data */ function lzCompose( address _oApp, bytes32 _guid, bytes calldata _message, address /*_executor*/, bytes calldata /*_extraData*/ ) external payable override { // Security: Verify the message source require(msg.sender == endpoint, "TokenSwapper: unauthorized sender"); require(_oApp == trustedOFT, "TokenSwapper: untrusted OApp"); // Decode the full composed message context uint64 nonce = OFTComposeMsgCodec.nonce(_message); uint32 srcEid = OFTComposeMsgCodec.srcEid(_message); uint256 amountLD = OFTComposeMsgCodec.amountLD(_message); // Get original sender (who initiated the OFT transfer) bytes32 composeFromBytes = OFTComposeMsgCodec.composeFrom(_message); address originalSender = OFTComposeMsgCodec.bytes32ToAddress(composeFromBytes); // Decode your custom compose message bytes memory composeMsg = OFTComposeMsgCodec.composeMsg(_message); (address recipient, uint256 minAmountOut) = abi.decode(composeMsg, (address, uint256)); // Execute the swap logic uint256 amountOut = _performSwap(amountLD, minAmountOut); // Transfer swapped tokens to recipient targetToken.safeTransfer(recipient, amountOut); emit TokenSwapped(originalSender, recipient, amountLD, amountOut); } function _performSwap(uint256 amountIn, uint256 minAmountOut) internal returns (uint256 amountOut) { // Your swap logic here (DEX integration, etc.) // This is a simplified example amountOut = amountIn * 95 / 100; // Simulate 5% slippage require(amountOut >= minAmountOut, "TokenSwapper: insufficient output"); } } ``` #### Key Security Considerations - **Endpoint Verification**: Always verify `msg.sender == endpoint` - **OApp Authentication**: Only accept messages from trusted OApps - **Message Validation**: Validate all decoded parameters before execution - **Reentrancy Protection**: Consider using `ReentrancyGuard` for complex operations :::tip **Token Availability**: The OFT automatically credits tokens to the composer address before calling `lzCompose`, so your composer can immediately use the received tokens. The tokens are already available in the composer's balance when `lzCompose` executes. ::: ## Extensions The OFT Standard can be extended to support several different use cases, similar to the ERC20 token standard. Since OFT inherits from the base OApp contract, all OApp extensions and patterns are also available to OFT implementations, providing maximum flexibility for cross-chain token applications. Below you can find relevant patterns and extensions: ### Rate Limiting The `RateLimiter` pattern controls the number of tokens that can be transferred cross-chain within a specific time window. This is particularly valuable for OFTs to prevent abuse and ensure controlled token flow across chains. #### Why Use Rate Limiting for OFTs? - **Prevent Token Drain Attacks**: Protects against malicious actors attempting to rapidly drain tokens from a chain - **Regulatory Compliance**: Helps meet compliance requirements for controlled cross-blockchain token transfers - **Supply Management**: Maintains balanced token distribution across chains by limiting transfer velocity - **Risk Management**: Reduces exposure to smart contract vulnerabilities or bridge exploits #### Implementation Inherit from both `OFT` and `RateLimiter` in your contract: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol"; import { RateLimiter } from "@layerzerolabs/oapp-evm/contracts/oapp/utils/RateLimiter.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; contract MyRateLimitedOFT is OFT, RateLimiter { constructor( string memory _name, string memory _symbol, address _lzEndpoint, address _owner, RateLimitConfig[] memory _rateLimitConfigs ) OFT(_name, _symbol, _lzEndpoint, _owner) Ownable(_owner) { _setRateLimits(_rateLimitConfigs); } // Override _debit to enforce rate limits on token transfers function _debit( address _from, uint256 _amountLD, uint256 _minAmountLD, uint32 _dstEid ) internal override returns (uint256 amountSentLD, uint256 amountReceivedLD) { // Check rate limit before allowing the transfer _outflow(_dstEid, _amountLD); // Proceed with normal OFT debit logic return super._debit(_amountLD, _minAmountLD, _dstEid); } } ``` #### Configuration Set up rate limits per destination chain during deployment: ```solidity // Example: Allow max 1000 tokens per hour to Ethereum, 500 per hour to Polygon RateLimitConfig[] memory configs = new RateLimitConfig[](2); configs[0] = RateLimitConfig({ dstEid: 30101, // Ethereum endpoint ID limit: 1000 ether, // 1000 tokens (18 decimals) window: 3600 // 1 hour window }); configs[1] = RateLimitConfig({ dstEid: 30109, // Polygon endpoint ID limit: 500 ether, // 500 tokens (18 decimals) window: 3600 // 1 hour window }); ``` #### Dynamic Rate Limit Management Add functions to update rate limits post-deployment: ```solidity function setRateLimits( RateLimitConfig[] calldata _rateLimitConfigs ) external onlyOwner { _setRateLimits(_rateLimitConfigs); } function getRateLimit(uint32 _dstEid) external view returns (RateLimit memory) { return rateLimits[_dstEid]; } ``` #### Rate Limit Behavior When a transfer exceeds the rate limit: - The transaction reverts with a rate limit error - Users must wait for the time window to reset - The limit resets based on a sliding window mechanism :::tip Consider implementing different rate limits for different user tiers (e.g., higher limits for verified institutions) by overriding the rate limit check logic. ::: :::caution Rate limiting may not be suitable for all OFT applications. High-frequency trading or time-sensitive applications might be negatively impacted by rate limits. ::: ### Mint & Burn OFT Adapter The `MintBurnOFTAdapter` is a specialized adapter for existing ERC20 tokens that have exposed mint and burn functions. Unlike the standard `OFTAdapter` which locks/unlocks tokens, this adapter burns tokens on the source chain and mints them on the destination chain. #### Key Differences from Standard OFTAdapter | Feature | Standard OFTAdapter | MintBurnOFTAdapter | | ------------------------ | ----------------------------------- | ------------------------------ | | **Token Supply** | Locks/unlocks existing tokens | Burns/mints tokens dynamically | | **Multiple Deployments** | Only one adapter per token globally | Multiple adapters can exist | | **Approval Required** | Yes, users must approve adapter | No, uses mint/burn privileges | | **Token Mechanism** | Escrow (locks tokens) | Non-escrow (burns/mints) | #### When to Use MintBurnOFTAdapter - **Tokens with mint/burn capabilities**: Your ERC20 already has `mint()` and `burn()` functions - **Dynamic supply management**: You prefer burning/minting over locking mechanisms - **Reduced custody risk**: Eliminate the risk of locked token supply running dry when using multiple adapters #### Installation To get started with a MintBurnOFTAdapter example, use the LayerZero CLI tool to create a new project: ```bash npx create-lz-oapp@latest --example mint-burn-oft-adapter ``` This creates a complete project with: - Example `MintBurnOFTAdapter` contracts - Sample `ElevatedMinterBurner` implementation - Deployment and configuration scripts - Cross-chain unit tests The example includes both the adapter contract and the underlying token with mint/burn capabilities, showing the complete integration pattern. #### Implementation Create your mint/burn adapter by inheriting from `MintBurnOFTAdapter`: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { MintBurnOFTAdapter } from "@layerzerolabs/oft-evm/contracts/MintBurnOFTAdapter.sol"; import { IMintableBurnable } from "@layerzerolabs/oft-evm/contracts/interfaces/IMintableBurnable.sol"; contract MyMintBurnOFTAdapter is MintBurnOFTAdapter { constructor( address _token, // Your existing ERC20 token with mint/burn exposed IMintableBurnable _minterBurner, // Contract with mint/burn privileges address _lzEndpoint, // Local LayerZero endpoint address _owner // Contract owner ) MintBurnOFTAdapter(_token, _minterBurner, _lzEndpoint, _owner) Ownable(_owner) {} } ``` #### Token Requirements You need a contract that implements the `IMintableBurnable` interface. This can be either: **Option 1: Token directly implements the interface** ```solidity interface IMintableBurnable { function burn(address _from, uint256 _amount) external returns (bool success); function mint(address _to, uint256 _amount) external returns (bool success); } ``` **Option 2: Elevated minter/burner contract (Recommended)** For existing tokens that already have mint/burn capabilities but don't implement `IMintableBurnable`, use an intermediary contract: ```solidity contract ElevatedMinterBurner is IMintableBurnable, Ownable { IMintableBurnable public immutable token; mapping(address => bool) public operators; modifier onlyOperators() { require(operators[msg.sender] || msg.sender == owner(), "Not authorized"); _; } constructor(IMintableBurnable _token, address _owner) Ownable(_owner) { token = _token; } function setOperator(address _operator, bool _status) external onlyOwner { operators[_operator] = _status; } function burn(address _from, uint256 _amount) external override onlyOperators returns (bool) { return token.burn(_from, _amount); } function mint(address _to, uint256 _amount) external override onlyOperators returns (bool) { return token.mint(_to, _amount); } } ``` The elevated contract approach allows you to: - Use existing tokens without modification - Control which contracts can mint/burn through operator management - Maintain existing token governance while adding bridge functionality #### Usage Flow 1. **Sending tokens**: - User calls `send()` on the MintBurnOFTAdapter - Adapter burns tokens from user's balance - LayerZero message sent to destination 2. **Receiving tokens**: - Destination adapter receives LayerZero message - Adapter mints new tokens to recipient's address #### Security Considerations The `MintBurnOFTAdapter` requires careful access control since it can mint tokens: ```solidity // Example: Ensure only the adapter can mint/burn contract SecureMintBurner is IMintableBurnable, Ownable { IERC20Mintable public token; address public adapter; modifier onlyAdapter() { require(msg.sender == adapter, "Only adapter can mint/burn"); _; } function mint(address _to, uint256 _amount) external onlyAdapter returns (bool) { token.mint(_to, _amount); return true; } function burn(address _from, uint256 _amount) external onlyAdapter returns (bool) { token.burnFrom(_from, _amount); return true; } } ``` :::warning **Transfer Fee Tokens**: The default implementation assumes lossless transfers (1 token in = 1 token out). If your token has transfer fees, you must override `_debit` and `_credit` functions to handle the actual amounts transferred. ::: :::info Unlike standard OFTAdapter, you can deploy multiple MintBurnOFTAdapters for the same omnichain mesh. ::: ### OFT Alt When the native gas token cannot be used to pay LayerZero fees, you can use `OFTAlt` which supports payment in an alternative ERC20 token. #### Installation To get started with an OFTAlt example, use the LayerZero CLI tool to create a new project: ```bash LZ_ENABLE_ALT_EXAMPLE=1 npx create-lz-oapp@latest --example oft-alt ``` This creates a complete project with: - Example `OFTAlt` contracts with alternative fee payment - EndpointV2Alt integration setup - Alternative fee token configuration - Deployment and configuration scripts - Cross-chain unit tests with ERC20 fee payments The example includes both the OFT Alt contract and the necessary setup for using alternative fee tokens, showing the complete integration pattern. #### Implementation ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { OFTAlt } from "@layerzerolabs/oft-evm/contracts/OFTAlt.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; contract MyOFTAlt is OFTAlt { constructor( string memory _name, string memory _symbol, address _lzEndpointAlt, address _owner ) OFTAlt(_name, _symbol, _lzEndpointAlt, _owner) Ownable(_owner) {} } ``` #### Key Differences 1. **Fee Payment**: Uses ERC20 tokens instead of native gas 2. **Approval Required**: You must approve the OFT contract to spend your fee tokens 3. **Endpoint**: Must use `EndpointV2Alt` instead of standard `EndpointV2` #### Using OFT Alt Before sending messages, approve the fee token: ```solidity // Approve the OFT to spend fee tokens IERC20(feeToken).approve(oftAltAddress, feeAmount); // Then send normally oft.send{value: 0}(sendParam, fee, refundAddress); // No native value needed ``` :::info OFT Alt is designed for chains where native gas tokens are not suitable for LayerZero fees, such as certain L2s or sidechains with alternative fee mechanisms. ::: ### Further Reading For more advanced patterns and detailed implementations, see: - [OApp Design Patterns](../oapp/message-design-patterns.md) - Additional messaging patterns - [Message Execution Options](../configuration/options.md) - Detailed options configuration - [OFT Technical Reference](../../../concepts/technical-reference/oft-reference.md) - Deep dive into OFT mechanics ### Tracing and Troubleshooting You can follow your testnet and mainnet transaction statuses using [LayerZero Scan](https://layerzeroscan.com/). Refer to [Debugging Messages](../troubleshooting/debugging-messages.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 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 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. ```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`](../../../get-started/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 layerzero-labs/devtools forge install layerzero-labs/LayerZero-v2 forge install OpenZeppelin/openzeppelin-contracts git submodule add https://github.com/GNSPS/solidity-bytes-utils.git lib/solidity-bytes-utils ``` Then add to your `foundry.toml` under `[profile.default]`: ```toml [profile.default] src = "src" out = "out" libs = ["lib"] remappings = [ '@layerzerolabs/onft-evm/=lib/devtools/packages/onft-evm/', '@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/', '@layerzerolabs/lz-evm-protocol-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/protocol', '@layerzerolabs/lz-evm-messagelib-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/messagelib', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', 'solidity-bytes-utils/=lib/solidity-bytes-utils/', ] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options ``` LayerZero contracts work with both [**OpenZeppelin V5**](https://docs.openzeppelin.com/contracts/5.x/erc721) and V4 contracts. Specify your desired version in your project's package.json: ```json "resolutions": { "@openzeppelin/contracts": "^5.0.1", } ``` To create an ONFT, you should decide which implementation is appropriate for your use case: 1. Use `ONFT721` when you're creating a new NFT collection that will exist on multiple chains. 2. Use `ONFT721Adapter` when you need to make an existing NFT collection 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 Composers sidebar_label: Omnichain Composers 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 layerzero-labs/devtools --no-commit ``` ```bash forge install layerzero-labs/LayerZero-v2 --no-commit ``` ```bash forge install OpenZeppelin/openzeppelin-contracts --no-commit ``` ```bash git submodule add https://github.com/GNSPS/solidity-bytes-utils.git lib/solidity-bytes-utils ``` Then add to your `foundry.toml` under `[profile.default]`: ```toml [profile.default] src = "src" out = "out" libs = ["lib"] remappings = [ '@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/', '@layerzerolabs/lz-evm-protocol-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/protocol', '@layerzerolabs/lz-evm-messagelib-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/messagelib', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', 'solidity-bytes-utils/=lib/solidity-bytes-utils/', ] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options ``` :::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: OVault EVM Implementation sidebar_label: Omnichain Vaults (OVault) description: Deploy cross-chain ERC-4626 vaults with omnichain shares and seamless user experience toc_min_heading_level: 2 toc_max_heading_level: 4 --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import Mermaid from '@theme/Mermaid'; import ZoomableMermaidV2 from '@site/src/components/ZoomableMermaidV2'; # OVault EVM Implementation Create an **omnichain ERC-4626 vault** that enable users to deposit assets or redeem shares on any blockchain network through a single transaction. ![OVault Comparison](/img/ovault-light-comparison.svg#gh-light-mode-only) ![OVault Comparison](/img/ovault-dark-comparison.svg#gh-dark-mode-only) ## Prerequisites Before implementing OVault, you should understand: 1. [OFT Standard](../oft/quickstart.md): How **Omnichain Fungible Tokens** work and what the typical deployment looks like 2. [Composer Pattern](../composer/overview.md): Understanding of `composeMsg` encoding and cross-chain message workflows 3. [ERC-4626 Vaults](https://eips.ethereum.org/EIPS/eip-4626): How the tokenized vault standard interface works for `deposit`/`redeem` operations An **Omnichain Vault (OVault)** takes a new or existing ERC4626 vault, and connects the underlying asset or share to many blockchain networks using the **Omnichain Fungible Token (OFT) standard**. An OVault implementation requires **five** core contracts to be deployed: - an `OFT` asset - an `ERC4626` vault - an `OFTAdapter` to transform the vault's share into an omnichain token - a `VaultComposerSync` to orchestrate omnichain deposits and redemptions between the asset and share - an `OFT` to represent the shares on spoke chains You can review the implementation of these contracts under [Contracts Overview](#contracts-overview). ## Step 1. Project Installation To start using LayerZero OVault contracts in a new project, use the LayerZero CLI tool, [**create-lz-oapp**](../../../get-started/create-lz-oapp/start.md). The CLI tool allows developers to create any omnichain application in <4 minutes! Get started by running the following from your command line: ```bash LZ_ENABLE_OVAULT_EXAMPLE=1 npx create-lz-oapp@latest --example ovault-evm ``` After running, select the directory for the scaffold project to be cloned into: ```bash ✔ Where do you want to start your project? … ./ ? Which example would you like to use as a starting point? › - Use arrow-keys. Return to submit. ❯ OVault EVM OApp OFT OFTAdapter ONFT721 ``` After the installer completes, copy and paste the `.env.example` in the project root, add your `PRIVATE_KEY`, `RPC_URL` you will be working with, and rename the file to `.env`: ```bash cp .env.example .env ``` You can find the sample codebase in [devtools/examples/ovault-evm](https://github.com/LayerZero-Labs/devtools/tree/main/examples/ovault-evm). ## Step 2. Network Configuration Update `hardhat.config.ts` to include your desired networks. Modify your `.env` file or the URL directly to change network RPCs: ```typescript const config: HardhatUserConfig = { networks: { base: { eid: EndpointId.BASESEP_V2_TESTNET, url: process.env.RPC_URL_BASESEP_TESTNET || 'https://base-sepolia.gateway.tenderly.co', accounts, }, arbitrum: { eid: EndpointId.ARBSEP_V2_TESTNET, url: process.env.RPC_URL_ARBSEP_TESTNET || 'https://arbitrum-sepolia.gateway.tenderly.co', accounts, }, optimism: { eid: EndpointId.OPTSEP_V2_TESTNET, url: process.env.RPC_URL_OPTSEP_TESTNET || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, }, // ... rest of config }; ``` ## Step 3. Deployment Configuration Configure your vault deployment in `devtools/deployConfig.ts`. This file controls which contracts to deploy and to what chains. The `deployConfig` supports several modes, depending on what contracts already have been deployed on the hub chain. If your `asset` token is already an `OFT`, you do not need to deploy a new `OFT` contract mesh. Both [Stargate Hydra](https://docs.stargate.finance/primitives/routes/stargateV2#supported-assets-hydra) assets (e.g., `USDC.e`) and [standard OFTs](../oft/quickstart.md) (e.g., `USDT0`) can be used as the asset inside the `ERC4626` vault. To see a list of existing OFT-compatible assets, review the [LayerZero OFT API](../../../tools/api/oft-reference.mdx). Pick the setup section that best aligns with your deployment needs: - [3.1a Existing AssetOFT](#31a-existing-assetoft) - [3.1b Existing AssetOFT and Vault](#31b-existing-assetoft-and-vault) - [3.1c Existing AssetOFT, Vault, and ShareOFTAdapter](#31c-existing-assetoft-vault-and-shareoft) For a completely fresh deployment of the **AssetOFT**, **OVault**, and **ShareOFT**: - [3.1d New AssetOFT, Vault, and ShareOFTAdapter](#31d-new-assetoft-vault-and-shareoft) ### 3.1a Existing AssetOFT The only requirement is that your `assetOFT` is deployed on the `hub` chain defined in the `deployConfig` file. - Update the `_hubEid` and the `_spokeEids` for the networks you plan to deploy to accordingly. - Add the `assetOFTAddress` contract for the `_hubEid` network under your `vault` config. - Add any changes necessary to your `Vault` and `ShareOFT` config `contract` or `metadata`. ```typescript // highlight-start // Hub network where ERC4626 lives const _hubEid = EndpointId.ARBSEP_V2_TESTNET; // Spoke networks where ShareOFT lives (excluding hub) const _spokeEids = [EndpointId.OPTSEP_V2_TESTNET, EndpointId.BASESEP_V2_TESTNET]; // highlight-end // ============================================ // Deployment Export // ============================================ // devtools/deployConfig.ts export const DEPLOYMENT_CONFIG: DeploymentConfig = { // highlight-start vault: { contracts: { vault: 'MyERC4626', shareAdapter: 'MyShareOFTAdapter', composer: 'MyOVaultComposer', }, deploymentEid: _hubEid, vaultAddress: undefined, // Existing ERC4626 vault assetOFTAddress: '', // Existing AssetOFT shareOFTAdapterAddress: undefined, // Deploy ShareOFTAdapter }, // highlight-end // highlight-start // Share OFT configuration (only on spoke chains) shareOFT: { contract: 'MyShareOFT', metadata: { name: 'MyShareOFT', symbol: 'SHARE', }, deploymentEids: _spokeEids, }, // highlight-end // Asset OFT configuration (deployed on specified chains) assetOFT: { contract: 'MyAssetOFT', metadata: { name: 'MyAssetOFT', symbol: 'ASSET', }, deploymentEids: [_hubEid, ..._spokeEids], }, } as const; ``` :::tip `assetOFT` and `shareOFT` networks do not need to perfectly overlap, as long as both contain deployments on the `_hubEid`. Configure based on your deployment requirements. ::: ### 3.1b Existing AssetOFT and Vault If your `assetOFT` and `ERC4626` contracts are already deployed, you only need to deploy the `ShareOFTAdapter` and `Composer`. - Update the `_hubEid` and the `_spokeEids` for the networks you plan to deploy to accordingly. - Add the `vaultAddress` and `assetOFTAddress` for the `_hubEid` network under your `vault` config. - Add any changes necessary to your `ShareOFT` config `contract` or `metadata`. ```typescript // devtools/deployConfig.ts // highlight-start // Hub network where ERC4626 lives const _hubEid = EndpointId.ARBSEP_V2_TESTNET; // Spoke networks where ShareOFT lives (excluding hub) const _spokeEids = [EndpointId.OPTSEP_V2_TESTNET, EndpointId.BASESEP_V2_TESTNET]; // highlight-end // ============================================ // Deployment Export // ============================================ export const DEPLOYMENT_CONFIG: DeploymentConfig = { vault: { contracts: { vault: 'MyERC4626', shareAdapter: 'MyShareOFTAdapter', composer: 'MyOVaultComposer', }, deploymentEid: _hubEid, // highlight-start vaultAddress: '', // Existing ERC4626 vault assetOFTAddress: '', // Existing AssetOFT token shareOFTAdapterAddress: undefined, // Deploy ShareOFTAdapter // highlight-end }, // highlight-start // Share OFT configuration (only on spoke chains) shareOFT: { contract: 'MyShareOFT', metadata: { name: 'MyShareOFT', symbol: 'SHARE', }, deploymentEids: _spokeEids, }, // highlight-end // Asset OFT configuration (deployed on specified chains) assetOFT: { contract: 'MyAssetOFT', metadata: { name: 'MyAssetOFT', symbol: 'ASSET', }, deploymentEids: [_hubEid, ..._spokeEids], }, } as const; ``` :::info This configuration will skip deploying the `AssetOFT` and `ERC4626 Vault` contracts, deploying only the `ShareOFTAdapter` and `Composer`. ::: ### 3.1c Existing AssetOFT, Vault, and ShareOFT If your `assetOFT`, `ERC4626`, and `ShareOFTAdapter` have already been deployed, you only need to deploy the `Composer`. - Update the `_hubEid` and the `_spokeEids` for the networks you plan to deploy to accordingly. - Add the `vaultAddress`, `assetOFTAddress`, and `shareOFTAdapterAddress` for the `_hubEid` network under your `vault` config. - Add any changes necessary to your `composer` config under `vault`. The `Vault`, `ShareOFT`, and `AssetOFT` configs and deployments will be **skipped**. ```typescript // devtools/deployConfig.ts // highlight-start // Hub network where ERC4626 lives const _hubEid = EndpointId.ARBSEP_V2_TESTNET; // Spoke networks where ShareOFT lives (excluding hub) const _spokeEids = [EndpointId.OPTSEP_V2_TESTNET, EndpointId.BASESEP_V2_TESTNET]; // highlight-end // ============================================ // Deployment Export // ============================================ export const DEPLOYMENT_CONFIG: DeploymentConfig = { vault: { contracts: { vault: 'MyERC4626', shareAdapter: 'MyShareOFTAdapter', // highlight-start composer: 'MyOVaultComposer', // highlight-end }, deploymentEid: _hubEid, // highlight-start vaultAddress: '', // Existing ERC4626 vault assetOFTAddress: '', // Existing AssetOFT shareOFTAdapterAddress: <'YOUR_SHARE_OFT_ADAPTER_ADDRESS'>, // Existing ShareOFTAdapter // highlight-end }, // Share OFT configuration (only on spoke chains) shareOFT: { contract: 'MyShareOFT', metadata: { name: 'MyShareOFT', symbol: 'SHARE', }, deploymentEids: _spokeEids, }, // Asset OFT configuration (deployed on specified chains OR use existing address) assetOFT: { contract: 'MyAssetOFT', metadata: { name: 'MyAssetOFT', symbol: 'ASSET', }, deploymentEids: [_hubEid, ..._spokeEids], }, } as const; ``` :::caution This configuration will skip deployment of the `AssetOFT`, `ERC4626` vault, and `ShareOFT` contracts. Only the `Composer` will be deployed. ::: ### 3.1d New AssetOFT, Vault, and ShareOFT If you have **no existing** OVault contracts, this configuration will deploy `AssetOFT`, `ERC4626` vault, `ShareOFTAdapter`, and the `Composer`. ```typescript const _hubEid = EndpointId.ARBSEP_V2_TESTNET; const _spokeEids = [EndpointId.OPTSEP_V2_TESTNET, EndpointId.BASESEP_V2_TESTNET]; // ============================================ // Deployment Export // ============================================ export const DEPLOYMENT_CONFIG: DeploymentConfig = { vault: { contracts: { vault: 'MyERC4626', shareAdapter: 'MyShareOFTAdapter', composer: 'MyOVaultComposer', }, deploymentEid: _hubEid, // highlight-start vaultAddress: undefined, assetOFTAddress: undefined, shareOFTAdapterAddress: undefined, // highlight-end }, // Share OFT configuration (only on spoke chains) ShareOFT: { contract: 'MyShareOFT', metadata: { name: 'MyShareOFT', symbol: 'SHARE', }, deploymentEids: _spokeEids, }, // Asset OFT configuration (deployed on specified chains) AssetOFT: { contract: 'MyAssetOFT', metadata: { name: 'MyAssetOFT', symbol: 'ASSET', }, deploymentEids: [_hubEid, ..._spokeEids], }, } as const; ``` :::info This configuration will deploy all core OVault contracts for a full fresh setup. ::: ### 3.2 Build Compile your contracts: ```bash pnpm compile ``` :::info If you're deploying the asset OFT from scratch for testing purposes, you'll need to mint an initial supply. Uncomment the `_mint` line in the `MyAssetOFT` constructor to provide initial liquidity. This ensures you have tokens to test deposit and cross-chain transfer functionality. ::: :::caution Do NOT mint share tokens directly in `MyShareOFT`. Share tokens must only be minted by the vault contract during deposits to maintain the correct share-to-asset ratio. Manually minting share tokens breaks the vault's accounting and can lead to incorrect redemption values. The mint line in `MyShareOFT` should only be uncommented for UI/integration testing, never in production. ::: ### 3.3 Deploy Deploy all vault contracts across all configured chains: ```bash pnpm hardhat lz:deploy --tags ovault ``` Based on your `deployConfig.ts`, this single command will begin deploying the defined contracts on your target `_hubEid` and `_spokeEids`. The deployment scripts automatically skip existing deployments, so you can safely run this command when expanding to new chains. Simply add the new chain endpoints to your `deployConfig.ts` and run the deploy command again. > **Tip**: To deploy to specific networks only, use the `--networks` flag: > > ```bash > pnpm hardhat lz:deploy --tags ovault --networks arbitrum,optimism > ``` ## Step 4. Wiring New Mesh This establishes the peer relationships between each OFT deployment, enabling cross-chain token transfers. See the [OFT Wiring Step](../oft/quickstart.md#2-wire-messaging-libraries-and-configurations) for more information. Depending on your deployment configuration in [Step 3](#step-3-deployment-configuration), you will have to wire either your newly deployed `ShareOFT`, `AssetOFT`, or **both**. ### 4.1 Existing Asset After modifying your `layerzero.share.config.ts`: ```bash pnpm hardhat lz:oapp:wire --oapp-config layerzero.share.config.ts ``` ### 4.2 Existing Asset & Share No action needed. ### 4.3 New Asset & Share After modifying your `layerzero.asset.config.ts` and `layerzero.share.config.ts`: ```bash # Configure LayerZero connections pnpm hardhat lz:oapp:wire --oapp-config layerzero.asset.config.ts pnpm hardhat lz:oapp:wire --oapp-config layerzero.share.config.ts ``` ## Step 5: Usage OVault enables two main operation patterns: **deposits** and **redemptions**. Each uses the standard `OFT.send()` interface with the `composer` handling vault operations automatically. The provided project scaffold demonstrates how to create send calls in [devtools/examples/ovault-evm/tasks/sendOVaultComposer.ts](https://github.com/LayerZero-Labs/devtools/blob/main/examples/ovault-evm/tasks/sendOVaultComposer.ts). ### Deposit Assets → Receive Shares **Scenario**: Deposit `asset` from a `_spokeEid`, receive vault `shares` on **the same** `_spokeEid` ```bash # Using the CLI task (recommended) npx hardhat lz:ovault:send \ --src-eid 30110 --dst-eid 30110 \ --amount 100.0 --to 0xRecipient \ --token-type asset ``` **Flow**: >A_SRC: send(asset, dstEid, composer, composeMsg) A_SRC->>A_HUB: LayerZero transfer A_HUB->>C: lzReceive() → lzCompose() Note over C: Detects asset deposit operation C->>V: deposit(assets) V-->>C: shares minted C->>S_HUB: send(shares, arbitrum, recipient) S_HUB->>A_SRC: LayerZero transfer (shares) A_SRC->>U: shares delivered Note over U,S_HUB: Single transaction: Assets → Shares (same chain)`} />

**Scenario**: Deposit `asset` from a `_spokeEid`, receive vault `shares` on **a different** `_spokeEid` ```bash # Using the CLI task (recommended) npx hardhat lz:ovault:send \ --src-eid 30110 --dst-eid 30111 \ --amount 100.0 --to 0xRecipient \ --token-type asset ``` **Flow**: >A_SRC: send(asset, dstEid, composer, composeMsg) A_SRC->>A_HUB: LayerZero transfer A_HUB->>C: lzReceive() → lzCompose() Note over C: Detects asset deposit operation C->>V: deposit(assets) V-->>C: shares minted C->>S_HUB: send(shares, optimism, recipient) S_HUB->>S_DST: LayerZero transfer S_DST->>R: shares delivered Note over U,R: Single transaction: Assets(Arbitrum) → Shares(Optimism)`} />

**Scenario**: Deposit `asset` from `_spokeEid`, receive vault `shares` on the `_hubEid` chain ```bash npx hardhat lz:ovault:send \ --src-eid 30110 --dst-eid 30184 \ --amount 100.0 --to 0xRecipient \ --token-type asset ``` **Flow**: >A_SRC: send(asset, dstEid, composer, composeMsg) A_SRC->>A_HUB: LayerZero transfer A_HUB->>C: lzReceive() → lzCompose() Note over C: Detects asset deposit operation C->>V: deposit(assets) V-->>C: shares minted Note over C: dstEid == hubEid (local delivery) C->>R: Direct ERC20 transfer (shares) Note over U,R: Single transaction: Assets(Arbitrum) → Shares(Hub)`} /> ### Redeem Shares → Receive Assets **Scenario**: Redeem vault `shares` from `_spokeEid`, receive `asset` on different `_spokeEid` ```bash npx hardhat lz:ovault:send \ --src-eid 30111 --dst-eid 30110 \ --amount 50.0 --to 0xRecipient \ --token-type share ``` **Flow**: >S_SRC: send(shares, dstEid, composer, composeMsg) S_SRC->>S_HUB: LayerZero transfer S_HUB->>C: lzReceive() → lzCompose() Note over C: Detects share redeem operation C->>V: redeem(shares) V-->>C: assets returned C->>A_HUB: send(assets, arbitrum, recipient) A_HUB->>A_DST: LayerZero transfer A_DST->>R: assets delivered Note over U,R: Single transaction: Shares(Optimism) → Assets(Arbitrum)`} />

**Scenario**: Redeem `vault` shares from `_spokeEid`, receive `asset` on the `_hubEid` chain ```bash npx hardhat lz:ovault:send \ --src-eid 30111 --dst-eid 30184 \ --amount 50.0 --to 0xRecipient \ --token-type share ``` **Flow**: >S_SRC: send(shares, dstEid, composer, composeMsg) S_SRC->>S_HUB: LayerZero transfer S_HUB->>C: lzReceive() → lzCompose() Note over C: Detects share redeem operation C->>V: redeem(shares) V-->>C: assets returned Note over C: dstEid == hubEid (local delivery) C->>R: Direct ERC20 transfer (assets) Note over U,R: Single transaction: Shares(Optimism) → Assets(Hub)`} /> ### SDK Integration For programmatic integration, use the official SDK [`@layerzerolabs/ovault-evm/src`](https://github.com/LayerZero-Labs/devtools/tree/main/packages/ovault-evm/src) which simplifies OVault operations by using [viem](https://viem.sh/) to generate the necessary calldata for calling `OFT.send()` with the proper `composeMsg` for the hub composer. The SDK's `OVaultMessageBuilder.generateOVaultInputs()` method handles all the complex message encoding and returns ready-to-use transaction parameters for `viem` wallet clients. ```typescript const input = { srcEid: 40245, // eid for base-sepolia hubEid: 40231, // eid for arbitrum-sepolia dstEid: 40245, // eid for base-sepolia // Optional. If dstAddress is not specified it will default to the walletAddress on the dst chain dstAddress: '0x0000000000000000000000000000000000000000', walletAddress: '0x0000000000000000000000000000000000000000', vaultAddress: '0x0000000000000000000000000000000000000000', // Address of the OVault Composer on the Hub Chain. Should implement IVaultComposerSync composerAddress: '0x0000000000000000000000000000000000000000', // Supply the Viem Chain Definitions for the hub and source chain. This is so the sdk can // quote fees and perform read operations hubChain: arbitrumSepolia, sourceChain: baseSepolia, operation: OVaultOperations.DEPOSIT, amount: 100000000000000000n, slippage: 0.01, // 1% slippage // Address of the token/oft. The token is an ERC20. They can be the same address. // If tokenAddress isn't specified it defaults to the oftAddress tokenAddress: '0x0000000000000000000000000000000000000000', oftAddress: '0x0000000000000000000000000000000000000000', } as const; const inputs = await OVaultMessageBuilder.generateOVaultInputs(input); ``` For complete usage examples, API reference, and advanced configuration, see the [SDK repository](https://github.com/LayerZero-Labs/devtools/blob/main/packages/ovault-evm/README.md). For manual integration and advanced usage, see the [Technical Reference](#technical-reference) section below. ## Technical Reference ### Contracts Overview An **Omnichain Vault (OVault)** takes a new or existing ERC4626 vault, and connects the underlying asset or share to many blockchain networks using the **Omnichain Fungible Token (OFT) standard**. An OVault implementation requires **five** core contracts to be deployed: - an `OFT` asset - an `ERC4626` vault - an `OFTAdapter` to transform the vault's share into an omnichain token - a `VaultComposerSync` to orchestrate omnichain deposits and redemptions between the asset and share - an `OFT` to represent the shares on spoke chains OVault uses a **hub-and-spoke model**: - **Hub Chain**: Hosts the `OFT` asset, `ERC4626` vault, the `VaultComposerSync`, and the share's `OFTAdapter` (lockbox) - **Spoke Chains**: Host `OFT` assets and `OFT` shares that connect to the hub implementations These connections enable a user to transfer an amount of the asset or share `OFT` from a **source** blockchain, **deposit** or **redeem** the token amount in the `ERC4626` vault, and receive the corresponding output token amount back on the **source** network. >A_SRC: send(asset, dstEid, composer, composeMsg) A_SRC->>A_HUB: LayerZero transfer A_HUB->>C: lzReceive() → lzCompose() Note over C: Detects asset deposit operation C->>V: deposit(assets) V-->>C: shares minted C->>S_HUB: send(shares, arbitrum, recipient) S_HUB->>A_SRC: LayerZero transfer (shares) A_SRC->>U: shares delivered Note over U,S_HUB: Single transaction: Assets → Shares (source chain)`} />

If you have an existing `assetOFT`, `vault`, or `ShareOFT` implementation, you may only need to deploy some of the contracts provided in the `ovault-evm` example repo: ##### Asset OFT (All Chains) ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.20; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol"; /** * @title MyAssetOFT * @notice ERC20 representation of the vault's asset token on a spoke chain for cross-chain functionality * @dev This contract represents the vault's underlying asset on spoke chains. It inherits from * LayerZero's OFT (Omnichain Fungible Token) to enable seamless cross-chain transfers of the * vault's asset tokens between the hub chain and spoke chains. * * The asset OFT acts as a bridgeable ERC20 representation of the vault's collateral asset, allowing * users to move their assets across supported chains while maintaining fungibility. */ contract MyAssetOFT is OFT { /** * @notice Constructs the Asset OFT contract * @dev Initializes the OFT with LayerZero endpoint and sets up ownership * @param _name The name of the asset token * @param _symbol The symbol of the asset token * @param _lzEndpoint The address of the LayerZero endpoint on this chain * @param _delegate The address that will have owner privileges */ constructor( string memory _name, string memory _symbol, address _lzEndpoint, address _delegate ) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) { // NOTE: Uncomment the line below if you need to mint initial supply // This can be useful for testing or if the asset needs initial liquidity // _mint(msg.sender, 1 ether); } } ``` `MyAssetOFT` is an example of a standard `OFT` and `ERC20` token that will be the `asset` inside the `ERC4626` vault. The `asset` token must be deployed on at least the `hub` and one `spoke` chain. :::info If your intended vault `asset` is already an `OFT` (e.g., [USDT0](https://docs.usdt0.to/technical-documentation/developer), [USDe](https://docs.ethena.fi/solution-overview/usde-overview)), you do not need to deploy this contract. if your vault `asset` is not an `OFT` (e.g., USDC via [CCTP](https://developers.circle.com/cctp)), you will need to convert the `asset` into an `OFT` compatible asset (e.g., USDC via [Stargate Hydra](https://docs.stargate.finance/primitives/routes/stargateV2#the-hydra-mechanism), [OFTAdapter](../oft/quickstart.md)). See the [OFT API `/list`](../../../tools/api/oft-reference.mdx) endpoint for a detailed list of all known tokens using the OFT standard. ::: ##### Vault + Share Adapter (Hub Chain) ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ERC4626 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; import { OFTAdapter } from "@layerzerolabs/oft-evm/contracts/OFTAdapter.sol"; /** * @title MyERC4626 * @notice ERC4626 tokenized vault implementation for cross-chain vault operations * @dev SECURITY CONSIDERATIONS: * - Donation/inflation attacks on empty or low-liquidity vaults * - Share price manipulation via large donations before first deposit * - Slippage during deposit/redeem operations in low-liquidity conditions * - First depositor advantage scenarios * * See OpenZeppelin ERC4626 documentation for full risk analysis: * https://docs.openzeppelin.com/contracts/4.x/erc4626#inflation-attack * * MITIGATIONS: * - OpenZeppelin v4.9+ includes virtual assets/shares to mitigate inflation attacks * - Deployers should consider initial deposits to prevent manipulation */ contract MyERC4626 is ERC4626 { /** * @notice Creates a new ERC4626 vault * @dev Initializes the vault with virtual assets/shares protection against inflation attacks * @param _name The name of the vault token * @param _symbol The symbol of the vault token * @param _asset The underlying asset that the vault accepts */ constructor(string memory _name, string memory _symbol, IERC20 _asset) ERC20(_name, _symbol) ERC4626(_asset) {} } /** * @title MyShareOFTAdapter * @notice OFT adapter for vault shares enabling cross-chain transfers * @dev The share token MUST be an OFT adapter (lockbox). * @dev A mint-burn adapter would not work since it transforms `ShareERC20::totalSupply()` */ contract MyShareOFTAdapter is OFTAdapter { /** * @notice Creates a new OFT adapter for vault shares * @dev Sets up cross-chain token transfer capabilities for vault shares * @param _token The vault share token to adapt for cross-chain transfers * @param _lzEndpoint The LayerZero endpoint for this chain * @param _delegate The account with administrative privileges */ constructor( address _token, address _lzEndpoint, address _delegate ) OFTAdapter(_token, _lzEndpoint, _delegate) Ownable(_delegate) {} } ``` `MyERC4626` is the standard tokenized vault contract. Given an `_asset` address for a valid `ERC20` contract in the constructor, the vault will create a corresponding `share` token using the vanilla `ERC4626` implementation. This `share` must then be transformed into an **Omnichain Fungible Token** using `MyShareOFTAdapter`. :::info If you have an existing `ERC4626` vault deployed, you will only need to deploy `MyShareOFTAdapter` using the `share` token address as the `address _token` argument in the constructor. ::: ##### Composer (Hub Chain) ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import { VaultComposerSync } from "@layerzerolabs/ovault-evm/contracts/VaultComposerSync.sol"; /** * @title MyOVaultComposer * @notice Cross-chain vault composer enabling omnichain vault operations via LayerZero */ contract MyOVaultComposer is VaultComposerSync { /** * @notice Creates a new cross-chain vault composer * @dev Initializes the composer with vault and OFT contracts for omnichain operations * @param _vault The vault contract implementing ERC4626 for deposit/redeem operations * @param _assetOFT The OFT contract for cross-chain asset transfers * @param _shareOFT The OFT contract for cross-chain share transfers */ constructor( address _vault, address _assetOFT, address _shareOFT ) VaultComposerSync(_vault, _assetOFT, _shareOFT) {} } ``` `VaultComposerSync` is the orchestrator contract that enables cross-chain vault operations between the **OFT standard** and **ERC-4626 vaults**, automatically handling deposits and redemptions based on incoming token transfers. The "Sync" in `VaultComposerSync` refers to **synchronous vault operations** - meaning the vault must support immediate, single-transaction deposits and redemptions without delays or waiting periods. :::caution **For Asynchronous Vaults** For asynchronous vaults that require multi-transaction redemptions, you will need to modify the `MyOVaultComposer` contract. ::: ##### Share OFT (Spoke Chains) ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.20; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol"; /** * @title MyShareOFT * @notice ERC20 representation of the vault's share token on a spoke chain for cross-chain functionality * @dev This contract represents the vault's share tokens on spoke chains. It inherits from * LayerZero's OFT (Omnichain Fungible Token) to enable seamless cross-chain transfers of * vault shares between the hub chain and spoke chains. This contract is designed to work * with ERC4626-compliant vaults, enabling standardized cross-chain vault interactions. * * Share tokens represent ownership in the vault and can be redeemed for the underlying * asset on the hub chain. The OFT mechanism ensures that shares maintain their value and can be freely * moved across supported chains while preserving the vault's accounting integrity. */ contract MyShareOFT is OFT { /** * @notice Constructs the Share OFT contract * @dev Initializes the OFT with LayerZero endpoint and sets up ownership * @param _name The name of the share token * @param _symbol The symbol of the share token * @param _lzEndpoint The address of the LayerZero endpoint on this chain * @param _delegate The address that will have owner privileges */ constructor( string memory _name, string memory _symbol, address _lzEndpoint, address _delegate ) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) { // WARNING: Do NOT mint share tokens directly as this breaks the vault's share-to-asset ratio // Share tokens should only be minted by the vault contract during deposits to maintain // the correct relationship between shares and underlying assets // _mint(msg.sender, 1 ether); // ONLY uncomment for testing UI/integration, never in production } } ``` Similar to `MyAssetOFT`, `MyShareOFT` is a standard `OFT` representation of the `share` token from the `ERC4626` vault to be used on other spoke chains. This contract requires `MyShareOFTAdapter` to be deployed on the hub chain using the `share` address as the `_token` argument. :::info If your intended vault `share` is already an `OFT` (e.g., [sUSDe](https://docs.ethena.fi/solution-overview/usde-overview)), you do not need to deploy this contract, and will only need to deploy `MyOVaultComposer`. ::: :::caution You should **NEVER** implement `_mint()` in the constructor or externally in `share` tokens. Since `shares` can be redeemed for `assets` on the hub chain, minting new supply breaks the conversion rate inside the `ERC4626` vault. ::: OVault operations follow a **two-phase architecture** where failures and slippage protection occur in distinct stages: ### Two-Phase Operation Flow **Phase 1: Source → Hub (Standard OFT)** - User calls `OFT.send()` targeting the hub composer - Standard LayerZero transfer with compose message - Reliable transfer with minimal failure modes **Phase 2: Hub Operations + Output Routing** - Composer executes vault operations (`deposit`/`redeem`) - **Critical slippage point**: Vault conversion rates may have changed - Output tokens routed to final destination (local or cross-chain) #### Operation Detection The composer automatically determines the vault operation based on which OFT sent the tokens: - **AssetOFT caller** → Triggers `deposit` operation (`assets` → `shares`) - **ShareOFT caller** → Triggers `redeem` operation (`shares` → `assets`) #### Slippage Protection Strategy Since the real slippage occurs during vault operations on the hub, the `composeMsg` contains the critical slippage parameters: - **Phase 1** `minAmountLD`: Set for source token (not critical for vault rates) - **Phase 2** `minAmountLD`: Set in `composeMsg` for vault output (critical protection) #### 1. Standard OFT Transfer Initiation Users call the standard OFT interface with compose instructions: ```solidity // Standard OFT send with compose message assetOFT.send( SendParam({ dstEid: hubEid, // Always send to hub first to: bytes32(composer), // VaultComposerSync address amountLD: depositAmount, minAmountLD: minDepositAmount, // Slippage protection extraOptions: "...", // Gas for compose + second hop composeMsg: composeMsg, // Second SendParam + minMsgValue oftCmd: "" }), MessagingFee(msg.value, 0), refundAddress ); ``` #### 2a. Composer Message Reception When tokens arrive at the hub via `lzReceive()`, the composer is triggered via `lzCompose()`: ```solidity function lzCompose( address _composeCaller, // Either ASSET_OFT or SHARE_OFT bytes32 _guid, bytes calldata _message, // Contains routing instructions address _executor, bytes calldata _extraData ) external payable ``` The compose message contains the **Phase 2 routing instructions** with critical slippage protection: ```solidity // Decoded in handleCompose() - this controls Phase 2 behavior (SendParam memory sendParam, uint256 minMsgValue) = abi.decode( _composeMsg, (SendParam, uint256) ); // SendParam for vault output routing: // - dstEid: Target chain for output tokens // - to: Final recipient address // - amountLD: Updated by composer to actual vault output // - minAmountLD: CRITICAL - protects against vault rate slippage // - extraOptions: Gas settings for destination transfer // - composeMsg: Empty (no nested compose) // - oftCmd: Empty (no OFT commands) ``` #### 2b. Operation Detection & Execution The composer automatically determines the vault operation based on which OFT sent the tokens: **Asset Deposit Flow (AssetOFT → Composer):** ```solidity function _depositAndSend() { // 1. Deposit assets into vault uint256 shareAmount = VAULT.deposit(_assetAmount, address(this)); // 2. Verify slippage protection _assertSlippage(shareAmount, _sendParam.minAmountLD); // 3. Route shares to final destination _send(SHARE_OFT, shareAmount, _refundAddress); } ``` **Share Redemption Flow (ShareOFT → Composer):** ```solidity function _redeemAndSend() { // 1. Redeem shares from vault uint256 assetAmount = VAULT.redeem(_shareAmount, address(this), address(this)); // 2. Verify slippage protection _assertSlippage(assetAmount, _sendParam.minAmountLD); // 3. Route assets to final destination _send(ASSET_OFT, assetAmount, _refundAddress); } ``` #### 2c. Smart Output Routing The `_send()` function handles both local and cross-chain delivery: ```solidity function _send(address _oft, SendParam memory _sendParam, address _refundAddress) { if (_sendParam.dstEid == VAULT_EID) { // Same chain: Direct ERC20 transfer (no LayerZero fees) address erc20 = _oft == ASSET_OFT ? ASSET_ERC20 : SHARE_ERC20; IERC20(erc20).safeTransfer(_sendParam.to.bytes32ToAddress(), _sendParam.amountLD); } else { // Cross-chain: Standard OFT send IOFT(_oft).send{ value: msg.value }(_sendParam, MessagingFee(msg.value, 0), _refundAddress); } } ``` #### Key Implementation Tips 1. **Start Simple**: Deploy a basic vault first, add yield strategies later 2. **Test Thoroughly**: Each operation type has different gas requirements 3. **Monitor Closely**: Set up alerts for failed compose messages 4. **Plan Recovery**: Document procedures for each failure scenario 5. **Optimize Gas**: Use the task's automatic optimization, adjust as needed ## Troubleshooting OVault operations have only **two possible final outcomes**: `Success` or `Failed` (but Refunded). Understanding the failure flow helps determine appropriate recovery actions. with composeMsg]) --> OFTTransfer[LayerZero OFT transfer
Source → Hub chain] OFTTransfer --> LayerZeroCheck{LayerZero transfer
successful?} LayerZeroCheck -->|Yes| LzReceive[LZ Executor calls lzReceive
OFT credits tokens to composer] LayerZeroCheck -->|No| TxRevert[Transaction reverts
No tokens moved] LzReceive --> SendCompose[OFT calls endpoint.sendCompose
Stores composeMsg for execution] SendCompose --> LzCompose[LZ Executor calls lzCompose
on VaultComposerSync] LzCompose --> TryCatch[try-catch around handleCompose
Protects against composer failures] TryCatch --> MsgValueCheck{msg.value sufficient
for destination delivery?} MsgValueCheck -->|Yes| VaultExecution[Vault operation executes
vault.deposit or vault.redeem] MsgValueCheck -->|No| AutoRefund[InsufficientMsgValue revert
try-catch triggers _refund] VaultExecution --> ActualAmount[actualAmount from vault output
shares or assets received] ActualAmount --> SlippageCheck{actualAmount >= minAmountLD
from composeMsg?} SlippageCheck -->|Yes| UpdateParam[Update SendParam.amountLD
Reset minAmountLD to zero] SlippageCheck -->|No| SlippageRevert[revert SlippageExceeded
try-catch triggers _refund] UpdateParam --> OutputDelivery[_send executes
Hub → Destination or local transfer] OutputDelivery --> Success[✓ Operation complete
Tokens delivered to recipient] TxRevert --> RetryState[User retries with
correct gas/fees] AutoRefund --> RetryState SlippageRevert --> RetryState[User retries with
adjusted slippage tolerance] style Start fill:#A77DFF,stroke:#7A3DFC,stroke-width:2px,color:#fff style Success fill:#8AE06C,stroke:#5FA049,stroke-width:2px,color:#000 style RetryState fill:#F1DF38,stroke:#BFB02D,stroke-width:2px,color:#000 style OFTTransfer fill:#2B2B2B,stroke:#757575,stroke-width:2px,color:#F2F2F2 style LzReceive fill:#6CADF5,stroke:#3986DC,stroke-width:2px,color:#fff style SendCompose fill:#6CADF5,stroke:#3986DC,stroke-width:2px,color:#fff style LzCompose fill:#6CADF5,stroke:#3986DC,stroke-width:2px,color:#fff style TryCatch fill:#F1DF38,stroke:#BFB02D,stroke-width:2px,color:#000 style VaultExecution fill:#2B2B2B,stroke:#757575,stroke-width:2px,color:#F2F2F2 style ActualAmount fill:#6CADF5,stroke:#3986DC,stroke-width:2px,color:#fff style UpdateParam fill:#6CADF5,stroke:#3986DC,stroke-width:2px,color:#fff style OutputDelivery fill:#2B2B2B,stroke:#757575,stroke-width:2px,color:#F2F2F2 style TxRevert fill:#F56868,stroke:#D84C4C,stroke-width:2px,color:#fff style AutoRefund fill:#F56868,stroke:#D84C4C,stroke-width:2px,color:#fff style SlippageRevert fill:#F56868,stroke:#D84C4C,stroke-width:2px,color:#fff style LayerZeroCheck fill:#1a1a1a,stroke:#757575,stroke-width:2px,color:#ffffff style MsgValueCheck fill:#1a1a1a,stroke:#757575,stroke-width:2px,color:#ffffff style SlippageCheck fill:#1a1a1a,stroke:#757575,stroke-width:2px,color:#ffffff`} /> ### Refund Scenarios and Recovery The `VaultComposerSync` uses a try-catch pattern around `handleCompose()` to ensure robust error handling: ```solidity try this.handleCompose{ value: msg.value }(/*...*/) { emit Sent(_guid); } catch (bytes memory _err) { // Automatic refund for any handleCompose failures _refund(_composeCaller, _message, amount, tx.origin); emit Refunded(_guid); } ``` **Common scenarios caught by try-catch**: - `InsufficientMsgValue` - insufficient gas for destination delivery → Auto refund - `SlippageExceeded` - vault output below minimum → Manual refund available - Vault operational errors (paused, insufficient liquidity) → Manual refund available ##### Transaction Revert: Gas or Fee Issues **What happens**: OFT transfer fails on source chain before any tokens move **Common causes**: - Insufficient native tokens for LayerZero fees - Invalid destination endpoint configuration - Gas estimation errors **User experience**: Transaction reverts immediately, no tokens transferred **Recovery**: User can retry immediately after fixing the issue - Use `quoteSend()` to get accurate fee estimation - Verify destination chain configuration - Ensure sufficient native tokens for cross-chain fees ##### Automatic Refund: Insufficient msg.value for Second Hop **What happens**: LayerZero completes lzReceive and lzCompose successfully, but insufficient gas for destination delivery **Technical flow**: 1. LZ Executor calls `lzReceive()` - tokens credited to composer ✓ 2. OFT calls `endpoint.sendCompose()` - composeMsg stored ✓ 3. LZ Executor calls `lzCompose()` on VaultComposerSync ✓ 4. Try-catch around `handleCompose()` catches `InsufficientMsgValue` revert 5. Automatic `_refund()` triggered back to source chain **Common causes**: - Underestimated gas for second hop during `quoteSend()` - Gas price fluctuations between quote and execution - Complex destination chain operations requiring more gas **User experience**: - Cross-chain transfer appears successful initially - Composer automatically triggers refund to source chain - Original tokens returned within minutes **Recovery**: Automatic - no user action required - Monitor source chain for refunded tokens - Retry with higher gas estimate from `quoteSend()` ##### Manual Refund: Vault Operation Issues **What happens**: LayerZero flow completes successfully, but vault operation fails slippage check **Technical flow**: 1. LZ Executor calls `lzReceive()` - tokens credited to composer ✓ 2. OFT calls `endpoint.sendCompose()` - composeMsg stored ✓ 3. LZ Executor calls `lzCompose()` on VaultComposerSync ✓ 4. Try-catch around `handleCompose()` executes vault operation ✓ 5. Vault returns `actualAmount` (shares or assets) 6. Slippage check: `actualAmount >= minAmountLD` **FAILS** 7. `SlippageExceeded` revert caught by try-catch 8. Manual `_refund()` available (user must trigger) **Common causes**: - Vault share/asset price changed during cross-chain transfer - Vault hit deposit/withdrawal limits between quote and execution - `minAmountLD` set too high based on stale `previewDeposit/previewRedeem` data **User experience**: - Cross-chain transfer succeeds - Vault operation fails on hub with slippage error - Tokens held by composer awaiting user action **Recovery**: User must manually trigger refund from hub chain 1. Switch wallet to hub chain network 2. Call composer refund function 3. Original tokens returned to source chain 4. Retry operation with adjusted slippage tolerance **Prevention**: - Use wider slippage tolerance (2-5% for volatile vaults) - Check vault limits: `vault.maxDeposit()`, `vault.maxRedeem()` - Monitor vault state with `vault.previewDeposit()` before large operations - Account for time delays in cross-chain operations when setting `minAmountLD` --- --- 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: LayerZero V2 EVM Protocol Overview sidebar_label: Protocol Contracts 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: 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: 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**](../../../get-started/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) {#custom-configuration} 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**](../../../tools/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](../../../concepts/glossary#dead-dvn) This configuration includes a **Dead DVN**: | SendUlnConfig (A to B) | ReceiveUlnConfig (B to A) | | ----------------------------------- | --------------------------------------- | | confirmations: 15 | confirmations: 15 | | optionalDVNCount: 0 | optionalDVNCount: 0 | | optionalDVNThreshold: 0 | optionalDVNThreshold: 0 | | optionalDVNs: Array(0) | optionalDVNs: Array(0) | | **requiredDVNCount: 2** | **requiredDVNCount: 2** | | **requiredDVNs: Array(DVN1, DVN2)** | **requiredDVNs: Array(DVN1, DVN_DEAD)** | :::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: 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: 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: Debugging Messages --- import InteractiveContract from '@site/src/components/InteractiveContract'; import EndpointV2ABI from '@site/node_modules/@layerzerolabs/lz-evm-sdk-v2/artifacts/contracts/EndpointV2.sol/EndpointV2.json'; 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. General debugging steps can be found [here](../../../concepts/troubleshooting/debugging-messages). ## Message Execution When your message is successfully delivered to the destination chain, the protocol attempts to execute the message with the execution parameters defined by the sender. Message execution can result in two possible states: - **Success**: If the execution is successful, an event (`PacketReceived`) is emitted. - **Failure**: If the execution fails, the contract reverses the clearing of the payload (re-inserts the payload) and emits an event (`LzReceiveAlert`) to signal the failure. - **Out of Gas**: The message fails because the transaction that contains the message doesn't provide enough gas for execution. The [Message Execution Options](../../../tools/sdks/options) applied to a message can be viewed on LayerZero Scan. There are several ways to determine the optimal gas values for these options. See [Determining Gas Costs](../../../tools/sdks/options#determining-gas-costs) for more details. - **Logic Error**: There's an error in either the contract code or the message parameters passed that prevents the message from being executed correctly. ### Retry Message Because LayerZero separates the verification of a message from its execution, if a message fails to execute due to either of the reasons above, the message can be retried without having to resend it from the origin chain. This is possible because the message has already been confirmed by the DVNs as a valid message packet, meaning execution can be retried at anytime, by anyone. Here's how an OApp contract owner or user can retry a message: - **Using LayerZero Scan**: For users that want a simple frontend interface to interact with, LayerZero Scan provides both message failure detection and in-browser message retrying. - **Calling `lzReceive` Directly**: If message execution fails, any user can retry the call on the Endpoint's `lzReceive` function via the block explorer or any popular library for interacting with the blockchain like [ethers](https://docs.ethers.org/v5/), [viem](https://viem.sh/docs/getting-started.html), etc. #### lzReceive() - Receive Messages Note: In the event of an `lzCompose` failure, the resolution process is similar. Any user can simply retry the call by invoking the Endpoint’s `lzCompose` function. #### lzCompose() - Execute Compose Messages ### Skipping Nonce Occasionally, an [OApp delegate](../oapp/overview.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: #### skip() **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. ::: #### clear() - Clear Stored Message **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() - Mark Message as Nil The `nilify` function is designed to transform a non-executed payload hash into NIL value (0xFFFFFF...). This transformation enables the resubmission of these NIL packets via the MessageLib back into the endpoint, providing a recovery mechanism from disruptions caused by malicious DVNs. #### burn() - Permanently Block Message The `burn` function operates similarly to the `clear` function with two key distinctions: 1. The OApp is not required to be aware of the original payload 2. The nonce designated for burning must be less than the `lazyInboundNonce` This function exists to avoid malicious DVNs from hiding the original payload to avoid the message from being cleared. --- --- title: 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 title: 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)**: an extension of `OApp` built for handling and supporting omnichain SPL Token transfers. - **Omnichain Application (OApp)**: the base program utilities for omnichain messaging and configuration. Each of these programs standards implement common functions for **sending** and **receiving** omnichain messages. ## Differences from the Ethereum Virtual Machine The full differences between Solidity and Solana are outside the scope of this overview (e.g., see [A Complete Guide to Solana Development for Ethereum Developers](https://solana.com/developers/evm-to-svm/complete-guide) or [60 Days of Solana by RareSkills](https://www.rareskills.io/solana-tutorial). :::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 state 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: Interactive Solana Program Playground sidebar_label: Program Playground --- import InteractiveSolanaInstruction from '@site/src/components/InteractiveSolanaInstruction'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Interactive Solana Program Playground Test LayerZero Solana programs directly from your browser. Build and send instructions without writing code. Explore key program instructions for message fee calculation, sending, receiving, and configuration management. For production use, please use the official SDKs which provides proper type-safe instruction builders: - [@layerzerolabs/lz-solana-sdk-v2](https://www.npmjs.com/package/@layerzerolabs/lz-solana-sdk-v2) - [@layerzerolabs/oft-v2-solana-sdk](https://www.npmjs.com/package/@layerzerolabs/oft-v2-solana-sdk) :::tip Real On-Chain Methods All instructions shown in this playground are **real methods** available in the LayerZero Solana programs today: - **Endpoint Program**: [Source Code](https://github.com/LayerZero-Labs/LayerZero-v2/tree/main/packages/layerzero-v2/solana/programs/programs/endpoint/src) - **OFT Program**: [Source Code](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/programs/oft/src) We only document OApp-relevant instructions, excluding admin-only functions. State variables are clearly marked as direct account data reads, not instructions. ::: ## LayerZero EndpointV2 The main entry point for all cross-chain messaging operations on Solana. This program handles message routing, fee calculation, and configuration management. ### Message Routing Core functions for sending and receiving messages between smart contracts. #### quote() - Get Fee Estimates #### send() - Send Messages #### clear() - Clear Payload :::info Why is clear() under Message Routing instead of Message Recovery? On Solana, the `clear()` instruction placement differs from EVM: - **EVM**: The Endpoint handles clearing payloads directly during message delivery - **Solana**: The OApp must explicitly call `clear()` via CPI in its `lzReceive` implementation This architectural difference means Solana developers need to implement the CPI call to `clear()` within their OApp's message handling logic, giving them more control over the message lifecycle but requiring explicit implementation. See the architectural note at the top of this page for the complete message flow. ::: #### sendCompose() - Send Compose Messages #### clearCompose() - Clear Compose Message :::info Architectural Difference: lzReceive If you're looking for `lzReceive` or `lzCompose` instructions in the Endpoint (as you might expect from EVM), note that on Solana these are implemented directly in the OApp (e.g., OFT program) due to architectural differences. **On Solana:** - The executor calls `lzReceive` directly on the OApp/OFT program - The OApp then makes a CPI (Cross-Program Invocation) call to the Endpoint's `clear` instruction to validate and clear the payload - After validation, the OApp continues with its business logic (e.g., minting tokens) This is different from EVM where the Endpoint calls back to the OApp's `_lzReceive` function. On Solana, the flow is inverted with the OApp calling into the Endpoint. See the [OFT lzReceive implementation](https://github.com/LayerZero-Labs/devtools/blob/73f732acf683ca18b74cd6c6adb3d656d2e0f36a/examples/oft-solana/programs/oft/src/instructions/lz_receive.rs#L72-L94) for an example. ::: ### Configuration Management Functions for setting custom verification, execution, and pathway management. #### registerOapp() - Register OApp #### setDelegate() - Set Delegate Address #### setSendLibrary() - Configure Send Library #### setReceiveLibrary() - Configure Receive Library #### setConfig() - Set Configuration Parameters #### initNonce() - Initialize Nonce #### initVerify() - Initialize Verification #### initSendLibrary() - Initialize Send Library #### initReceiveLibrary() - Initialize Receive Library #### initConfig() - Initialize Configuration #### setReceiveLibraryTimeout() - Set Library Timeout ### Message Recovery & Security Functions for handling message exceptions, security threats, and recovery scenarios. #### burn() - Permanently Block Message #### skip() - Skip Inbound Nonce #### nilify() - Mark Message as Nil #### withdrawRent() - Withdraw Rent ### Status Checks Functions for querying current configuration settings, library assignments, nonce tracking, and message states. #### eid() - Get Endpoint ID #### endpointAdmin() - Get Endpoint Admin #### oappDelegate() - Get OApp Delegate #### outboundNonce() - Get Outbound Nonce #### inboundNonce() - Get Inbound Nonce ## Omnichain Fungible Token (OFT) Omnichain Fungible Token (OFT) enables seamless cross-chain token transfers. Deploy once and bridge your SPL tokens to any supported blockchain. Since Solana uses a rent-based storage model rather than EVM's gas-per-bytecode deployment costs, and has no restrictive contract size limits (like EVM's 24KB limit), we can include all of these extensions in the same program. While some OFT instances may not utilize all features (like fees or rate limits), having them built-in provides maximum flexibility without the contract splitting requirements common in EVM development. :::info Default OFT Program For Solana Mainnet, we use **PENGU OFT** as the default example: - **Program**: `EfRMrTJWU2CYm52kHmRYozQNdF8RH5aTi3xyeSuLAX2Y` - **OFT Store**: `qMNo1RFo11J9ZLGuq7dVmWAssuCZaNsSamk8g2q4UZA` You can replace these with your own OFT deployment addresses. ::: :::info lzReceive Implementation The `lzReceive` instruction is implemented here in the OFT program (not in the Endpoint). This is a key architectural difference from EVM: - On Solana, executors call `lzReceive` directly on the OApp/OFT - The OFT program then makes a CPI call to the Endpoint's `clear` instruction to validate the message - After successful validation, the OFT continues with its logic (minting tokens, updating balances, etc.) The flow is: Executor → OFT.lzReceive → Endpoint.clear (via CPI) → Continue OFT logic ::: ### Send Tokens #### quoteSend() - Get Transfer Fees #### quoteOft() - Get Detailed Transfer Quote #### send() - Transfer Tokens #### lzReceiveTypes() - Get Receive Account Types #### lzReceive() - Receive Tokens ### Token Details The **OFT Store** account is a [Program Derived Address (PDA)](https://solana.com/docs/core/pda), not the token mint itself. This account stores essential OFT related state variables. #### oftAdmin() - Get OFT Admin #### tokenMint() - Get Token Mint #### tokenEscrow() - Get Token Escrow #### sharedDecimals() - Get Shared Decimals #### decimalConversionRate() - Get Decimal Conversion Rate #### oftVersion() - Get OFT Version #### tvlLd() - Get Total Value Locked #### isPaused() - Check Pause State #### defaultFeeBps() - Get Default Fee ### Peer Configuration These functions read peer-specific configuration from PeerConfig accounts: #### peerAddress() - Get Peer Address #### enforcedOptions() - Check Enforced Options #### peerFeeBps() - Get Peer Fee #### outboundRateLimiter() - Check Outbound Rate Limiter #### inboundRateLimiter() - Check Inbound Rate Limiter :::tip Finding PeerConfig Account The PeerConfig account is a PDA (Program Derived Address) derived from: - OFT program ID - Seeds: `[b"peer_config", oft_store.key().as_ref(), &dst_eid.to_be_bytes()]` You'll need to derive this address using the OFT store account and the destination chain's endpoint ID. ::: ### Management Functions #### initOft() - Initialize OFT #### setPeerConfig() - Configure Remote Peer #### setOftConfig() - Update OFT Settings #### setPause() - Pause/Unpause OFT #### withdrawFee() - Withdraw Collected Fees ## Events and Errors ### Endpoint Events Key events emitted by the Endpoint program during cross-chain operations. #### PacketSentEvent - Message Sent Emitted when a packet is sent through the endpoint. Contains the encoded packet data and execution options for tracking cross-chain messages. #### PacketVerifiedEvent - Message Verified Emitted when an inbound message has been verified by the DVNs and is ready for execution. Indicates the message passed all security checks. #### PacketDeliveredEvent - Message Delivered Emitted when a message is successfully delivered to the destination OApp. This confirms the cross-chain transaction completed. #### ComposeSentEvent - Compose Message Queued Emitted when a compose message is queued for sequential execution after the primary message. Used for complex multi-step operations. #### ComposeDeliveredEvent - Compose Message Executed Emitted when a compose message is successfully executed. Indicates the secondary operation completed successfully. #### DelegateSetEvent - Delegate Updated Emitted when an OApp delegate is set or changed. The delegate can configure settings on behalf of the OApp. #### SendLibrarySetEvent - Send Library Configured Emitted when the send library is configured for a specific destination. Tracks library changes for outbound messages. #### ReceiveLibrarySetEvent - Receive Library Configured Emitted when the receive library is configured for a specific source. Tracks library changes for inbound messages. #### OAppRegisteredEvent - OApp Registration Emitted when a new OApp is registered with the endpoint. This establishes the OApp's ability to send and receive messages. ### Endpoint Errors Common errors returned by the Endpoint program. #### Unauthorized - Permission Denied Thrown when the caller lacks required permissions for the operation. Only authorized addresses can perform certain actions. #### InvalidNonce - Nonce Mismatch Thrown when processing a message with an invalid nonce. Ensures messages are processed in the correct sequential order. #### InvalidSender - Unauthorized Sender Thrown when receiving a message from an unauthorized sender. Only configured peers can send messages to the OApp. #### InvalidReceiver - Invalid Destination Thrown when the specified receiver address is invalid or not configured properly for the destination chain. #### LzTokenUnavailable - LayerZero Token Error Thrown when LayerZero token operations fail or tokens are unavailable for fee payment. ### OFT Events Key events emitted by the OFT program during token operations. ([Source](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/programs/oft/src/events.rs)) #### OFTSent - Tokens Sent Cross-Chain Emitted when tokens are sent to another chain. Contains the message GUID, destination chain ID, sender and recipient addresses, and the amount sent in both shared and local decimals. #### OFTReceived - Tokens Received Emitted when tokens are received from another chain. Contains the source chain ID, sender address, and the amount received after decimal conversion. ### OFT Errors Common errors returned by the OFT program. ([Source](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/programs/oft/src/errors.rs)) #### Paused - OFT Operations Paused Thrown when attempting operations while the OFT is paused. No transfers can be initiated until unpaused by the designated unpauser. #### SlippageExceeded - Insufficient Received Amount Thrown when the received amount after cross-chain transfer falls below the minimum acceptable amount due to decimal conversions or fees. #### RateLimitExceeded - Transfer Rate Limit Hit Thrown when a transfer exceeds the configured rate limits for the peer connection. Wait for the rate limit window to reset. #### InvalidOptions - Invalid Message Options Thrown when the provided LayerZero message options are invalid or incompatible with the OFT configuration. #### InvalidAmount - Invalid Transfer Amount Thrown when the transfer amount is zero, exceeds limits, or is otherwise invalid for the operation. #### InvalidPeer - Peer Not Configured Thrown when attempting to interact with a destination chain where no peer OFT has been configured. Use setPeerConfig() first. ## Usage Tips ### Getting Started 1. **Connect Your Wallet**: Click "Connect Phantom Wallet" to connect your Solana wallet 2. **Select Network**: Choose between Solana Mainnet and Devnet 3. **Custom RPC (Optional)**: If you encounter rate limits (403 errors), add a custom RPC URL: - [Helius](https://helius.dev) - Generous free tier - [QuickNode](https://quicknode.com) - Free tier available - [Alchemy](https://alchemy.com) - Professional services ### Common Workflows #### Sending Tokens Cross-Chain (OFT) 1. Initialize your OFT with `initOft()` 2. Configure peers with `setPeerConfig()` 3. Get a quote with `quoteOft()` or `quoteSend()` 4. Send tokens with `send()` #### Setting Up Messaging (Endpoint) 1. Register your OApp with `registerOapp()` 2. Initialize nonce tracking with `initNonce()` 3. Set up libraries with `initSendLibrary()` and `initReceiveLibrary()` 4. Configure DVNs/executors with `setConfig()` 5. Get quotes with `quote()` and send messages with `send()` ### Troubleshooting - **403 Errors**: Use a custom RPC URL instead of public endpoints - **"Account does not exist"**: Ensure all required accounts have been initialized - **"Invalid arguments"**: Check that byte arrays are properly formatted (0x prefix) - **Simulation failures**: This playground uses simplified encoding - use official SDKs for production For production applications, always use the official LayerZero SDKs which provide proper type safety and encoding. --- --- title: LayerZero V2 Solana OApp Reference sidebar_label: Solana OApp audience: Solana smart-contract engineers who already understand Anchor basics and the LayerZero V2 protocol on EVM. goal: Show every moving piece you must implement to make a Solana program behave like an **OApp** (Omnichain Application) under LayerZero’s Executor / DVN flow. --- import ZoomableMermaidV2 from '@site/src/components/ZoomableMermaidV2'; The OApp Standard provides developers with a _generic message passing interface_ to **send** and **receive** arbitrary pieces of data between contracts existing on different blockchain networks. How exactly the data is interpreted and what other actions they trigger, depend on the specific OApp implementation. ## Developing Solana OApps vs EVM OApps Due to VM and programming paradigm differences, developing OApps on Solana works differently from doing the same on EVM chains. On EVM chains, OApps can simply inherit the OApp contract standard to unlock cross-chain functionality. On Solana, there is no similar inheritance. The table below outlines the main differences when developing: | EVM | Solana | | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | OApp contracts inherit base contracts such as `OAppSender` and `OAppReceiver` which hides all endpoint calls. | No inheritance. Your OApp program **must** directly include the code that [CPIs](/concepts/glossary.md#cpi-cross-program-invocation) the endpoint. | | Dynamic dispatch ⇒ `endpoint.lzReceive()` can `delegatecall` back into your contract without pre-knowing storage slots. | **All accounts required** for execution of `lz_receive()` must be listed up-front. This is done via the Executor calling the `lz_receive_types` instruction. | | The OApp contract's address is used as the OApp address | The OApp program's address is not used as the OApp address. Instead, a [PDA](/concepts/glossary.md#pda-program-derived-address) owned by the OApp program is used as the OApp address. For example, for OFTs, the OApp address is the OFT Store's address, which is owned by the OFT program. | | Pathway configs are set in the storage of the Endpoint contract. | Pathway configs are stored in [PDAs](/concepts/glossary.md#pda-program-derived-address) owned by the Endpoint program or the Send/Library program. | ## High-level message flow ``` ┌────────┐ (1) msg packet to receiver PDA EVM │ Src │ ──────────────────────────────────────────► Solana Chain │ OApp │ │ └────────┘ ▼ ┌─────────────────────────┐ │ Executor program │ └─────────────────────────┘ │ (2) CPI: `lz_receive_types` ──────┘ ← returns Vec │ (accounts required for `lz_receive`) (3) CPI: `lz_receive` (your code) | - expects full list of required accounts │ – runs business logic │ – CPIs back into Endpoint (`Endpoint::clear()`) │ │ ▼ ┌────────┐ │ Dst │ │ OApp │ └────────┘ (4) Endpoint and OApp state updated ``` ## Required PDAs The following are the [PDAs](/concepts/glossary.md#pda-program-derived-address) that are required for a Solana OApp. | Component | What it is | Seed / Derivation | Why it matters | | ------------------------ | ------------------------------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------ | | **OApp Store PDA** | Zero-copy account holding program state (admin, bump, endpoint id, user data …) | `[b"Store"]` (customizable) | Acts as _receiver address_ and signer seed for Endpoint [CPIs](/concepts/glossary.md#cpi-cross-program-invocation) | | **Peer PDA(s)** | One per remote chain; stores the peer address allowed to send | `[b"Peer", store, src_eid]` | Used to authenticate `params.sender` inside `lz_receive` | | **lz_receive_types PDA** | Flat list of _static_ accounts you’ll echo back | `[b"LzReceiveTypes", store]` | Queried off-chain; never touched on-chain | | **lz_compose_types PDA** | Same pattern but for _compose_ messages | `[b"LzComposeTypes", store]` | Only needed if you compose sub-messages | Note that except for the Store PDA, the PDA seeds are not customizable. ## Running the example Solana OApp You can install the example Solana OApp using the `create-lzoapp` CLI: ```bash LZ_ENABLE_SOLANA_OAPP_EXAMPLE=1 npx create-lz-oapp@latest --example oapp-solana ``` The CLI will set up a string-passing OApp project involving EVM and Solana. Follow the included README for the deployment instructions. The following sections will highlight the several code excerpts of the Solana OApp that are essential to it functioning. ## Initialize the OApp PDA This `init_store` instruction initializes 3 PDAs: OApp Store's, lz_receive_types and lz_compose_types. ```rust impl InitStore<'_> { pub fn apply(ctx: &mut Context, params: &InitStoreParams) -> Result<()> { ctx.accounts.store.admin = params.admin; ctx.accounts.store.bump = ctx.bumps.store; ctx.accounts.store.endpoint_program = params.endpoint; ctx.accounts.store.string = "Nothing received yet.".to_string(); // set up the “types” PDAs so the SDK can find them ctx.accounts.lz_receive_types_accounts.store = ctx.accounts.store.key(); ctx.accounts.lz_compose_types_accounts.store = ctx.accounts.store.key(); // note that the current Solana OApp example does not yet fully implement lzCompose // calling endpoint cpi let register_params = RegisterOAppParams { delegate: ctx.accounts.store.admin }; let seeds: &[&[u8]] = &[STORE_SEED, &[ctx.accounts.store.bump]]; // ▸ Register with the Endpoint so the Executor can call us later oapp::endpoint_cpi::register_oapp( ENDPOINT_ID, ctx.accounts.store.key(), ctx.remaining_accounts, seeds, register_params, )?; Ok(()) } } ``` **Key points** - You must call `oapp::endpoint_cpi::register_oapp` to register your OApp - Registration is **one-time**; afterwards the Executor knows this Store PDA = OApp. - You **do not** pass a “trusted remote” mapping here—that’s what the `Peer` PDAs enforce. - The `string` field is specific to the string-passing OApp example - You can extend your OApp's store PDA with other fields required by your use case. ## `lz_receive_types` — tell the Executor which accounts are needed by `lz_receive` When the Executor calls `lz_receive` on any OApp program, due to how Solana works, it needs to provide a list of all the accounts that are read or written to. The Solana OApp standard does this via the `lz_receive_types` instruction, shown in the example excerpt below. ```rust pub fn apply( ctx: &Context, params: &LzReceiveParams, ) -> Result> { // 1. your writable state let store = ctx.accounts.store.key(); // 2. the peer that sent the message (read-only) let peer_seeds = [PEER_SEED, &store.to_bytes(), ¶ms.src_eid.to_be_bytes()]; let (peer, _) = Pubkey::find_program_address(&peer_seeds, ctx.program_id); let mut accs = vec![ LzAccount { pubkey: store, is_signer: false, is_writable: true }, LzAccount { pubkey: peer, is_signer: false, is_writable: false }, ]; // 3. Accounts specifically required for calling Endpoint::clear() accs.extend(get_accounts_for_clear( ENDPOINT_ID, &store, params.src_eid, ¶ms.sender, params.nonce, )); // 4. (optional) If the message itself is a *compose* payload, // also append the accounts the Endpoint expects for send_compose() if msg_codec::msg_type(¶ms.message) == msg_codec::COMPOSED_TYPE { accs.extend(get_accounts_for_send_compose( ENDPOINT_ID, &store, // payer = this PDA &store, // receiver (self-compose) ¶ms.guid, 0, // fee = 0, Executor pre-funds ¶ms.message, )); } Ok(accs) } ``` **Rules of thumb** 1. **Exact order matters**—`LzAccount[0]` in the Vec becomes account #0 in the eventual tx. 2. **Signer placeholders:** If some downstream [CPI](/concepts/glossary.md#cpi-cross-program-invocation) (e.g. ATA init) needs a signer, pass `pubkey = Pubkey::default(), is_signer = true`. 3. Include **every** account `lz_receive` and\_ your Endpoint [CPIs](/concepts/glossary.md#cpi-cross-program-invocation) will require. ### Accounts for `lz_receive_types` checklist For a minimal string-passing OApp, these are the accounts that must be returned by `lz_receive_types`: ``` 0 store (w) – PDA signer via seeds [b"Store"] 1 peer (r) – verifies src sender 2 endpoint_program – oapp::endpoint (ID) 3 system_program – for rent in clear() 4 rent sysvar 5 … 10 – (six replay-protection PDAs that get_accounts_for_clear puts at the end) ``` Your **`lz_receive_types` must return these exact pubkeys in this exact order** or the Executor will splat with “AccountNotWritable”, “InvalidProgramId”, etc. :::info Note that the accounts that `lz_receive_types` return will differ by program. The accounts listed above are specific to the string-passing OApp program. The `lz_receive_types` of the OFT program will require a different list of accounts. ::: --- ## `lz_receive` — business logic + `Endpoint::clear` The `lz_receive` instruction is where your OApp's business logic is defined and also where the call to `Endpoint::clear` is made. Additionally, it is also where compose messages are handled, if implemented. ```rust #[derive(Accounts)] #[instruction(params: LzReceiveParams)] pub struct LzReceive<'info> { #[account(mut, seeds = [STORE_SEED], bump = store.bump)] pub store: Account<'info, Store>, #[account( seeds = [PEER_SEED, &store.key().to_bytes(), ¶ms.src_eid.to_be_bytes()], bump = peer.bump, constraint = params.sender == peer.peer_address )] pub peer: Account<'info, PeerConfig> } pub fn apply(ctx: &mut Context, params: &LzReceiveParams) -> Result<()> { // 1. replay-protection (handled inside clear) let seeds = &[STORE_SEED, &[ctx.accounts.store.bump]]; // The first Clear::MIN_ACCOUNTS_LEN remaining accounts are exactly what // get_accounts_for_clear() returned earlier. let clear_accounts = &ctx.remaining_accounts[..Clear::MIN_ACCOUNTS_LEN]; oapp::endpoint_cpi::clear( ENDPOINT_ID, ctx.accounts.store.key(), // payer (seeds above) clear_accounts, seeds, ClearParams { receiver: ctx.accounts.store.key(), src_eid: params.src_eid, sender: params.sender, nonce: params.nonce, guid: params.guid, message: params.message.clone(), }, )?; // You should have app-specific logic to determine whether a message is a compose message // e.g. you can have part of the payload be a u8 where 1 = regular message, 2 = compose message // or, especially if your regular payload has a fixed length, determine the presence of a compose message based on presence of data after an offset (example: https://github.com/LayerZero-Labs/devtools/blob/main/examples/lzapp-migration/programs/oft202/src/msg_codec.rs#L60) oapp::endpoint_cpi::send_compose( ENDPOINT_ID, ctx.accounts.store.key(), &ctx.remaining_accounts[Clear::MIN_ACCOUNTS_LEN..], seeds, SendComposeParams { to: ctx.accounts.store.key(), // self guid: params.guid, index: 0, message: params.message.clone(), }, )?; // 2. Your app-specific logic let new_string = msg_codec::decode(¶ms.message); ctx.accounts.store.string = new_string; Ok(()) } ``` **Rules of thumb** - Call `clear()` **before** touching any user state—this burns the nonce and prevents re-entry. - Use `ctx.remaining_accounts` instead of hard-wiring anything—keeps `lz_receive_types` and `lz_receive` perfectly in sync. - Don’t forget `is_signer: true` zero-pubkey placeholders for ATA init or rent payer. **Security Reminders** - Validate the `Peer` account first (`constraint = params.sender == peer.address`). - **Store the Endpoint ID inside state** (`store.endpoint_program`) and **assert** it every CPI. ## OApp-Specific Message Codec Since at its core, the OApp Standard simply gives you the interface for generic message passing (raw bytes), you need to implement for yourself how the raw bytes are interpreted. In the Solana OApp example and also in the [OFT implementation](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/programs/oft/src/msg_codec.rs), this logic is encapusalated in a `msg_codec.rs` file. ```rust use anchor_lang::prelude::error_code; use std::str; // Just like OFT, we don't need an explicit MSG_TYPE param // Instead, we'll check whether there's data after the string ends pub const LENGTH_OFFSET: usize = 0; pub const STRING_OFFSET: usize = 32; #[error_code] pub enum MsgCodecError { /// Buffer too short to even contain the 32‐byte length header InvalidLength, /// Header says "string is N bytes" but buffer < 32+N BodyTooShort, /// Payload bytes aren’t valid UTF-8 InvalidUtf8, } fn decode_string_len(buf: &[u8]) -> Result { if buf.len() < STRING_OFFSET { return Err(MsgCodecError::InvalidLength); } let mut string_len_bytes = [0u8;32]; string_len_bytes.copy_from_slice(&buf[LENGTH_OFFSET..LENGTH_OFFSET+32]); Ok(u32::from_be_bytes(string_len_bytes[28..32].try_into().unwrap()) as usize) } pub fn encode(string: &str) -> Vec { let string_bytes = string.as_bytes(); let mut msg = Vec::with_capacity( STRING_OFFSET + // length word (fixed) string_bytes.len() // string length ); // 4-byte length msg.extend(std::iter::repeat(0).take(28)); // padding msg.extend_from_slice(&(string_bytes.len() as u32).to_be_bytes()); // string msg.extend_from_slice(string_bytes); msg } pub fn decode(message: &[u8]) -> Result { // Read the declared payload length from the header let string_len = decode_string_len(message)?; let start = STRING_OFFSET; // Safely compute end index and check for overflow let end = start .checked_add(string_len) .ok_or(MsgCodecError::InvalidLength)?; // Ensure the buffer actually contains the full payload if end > message.len() { return Err(MsgCodecError::BodyTooShort); } // Slice out the payload bytes let payload = &message[start..end]; // Attempt to convert to &str, returning an error if invalid UTF-8 match str::from_utf8(payload) { Ok(s) => Ok(s.to_string()), Err(_) => Err(MsgCodecError::InvalidUtf8), } } ``` **Key points** - Every OApp would have its own Message Codec implementation - The above Message Codec example involves an OApp that expects the message to contain only a `length` and the actual `string` - If sending across VMs, ensure the codec on the other VM matches. ## Gotchas & common errors | Error | Usual cause | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | | `AccountNotSigner` on slot N | You omitted a signer placeholder or swapped two accounts. | | `InvalidProgramId` (Endpoint) | Wrong Endpoint ID; check you passed the same constant everywhere. | | Transaction > 1232 bytes | Too many accounts in your Vec → trim until ALTs support arrives (see [Address Lookup Tables](#roadmap-address-lookup-tables-q3-2025) ). | | Executor halts at `lz_receive` | Your `lz_receive_types` returned fewer accounts than `lz_receive` expects. | ## `lz_receive_types_v2` For simpler use cases, `lz_receive_types` (v1) is sufficient. However, for more complex use cases, `lz_receive_types` (v1) has several limitations. ### Limitations of `lz_receive_types` (v1) #### 1. Return Data Size Constraint Solana’s runtime restricts the return data of [CPI](../../../concepts/glossary#cpi-cross-program-invocation) calls to a maximum of 1024 bytes. This constrains the number of accounts that `lz_receive_types`(v1) can return to roughly 30—insufficient for complex messaging patterns such as the ABA messaging pattern. #### 2. Lack of Address Lookup Tables Support `lz_receive_types` (v1) does not leverage [Solana’s Address Lookup Tables (ALTs)](https://solana.com/developers/guides/advanced/lookup-tables), which extend the maximum number of accounts in a transaction from 32 to 128. This restricts functionality of state-heavy applications. #### 3. Lack of ABA Messaging Pattern Support `lz_receive_types` (v1) does not support the [ABA messaging pattern](../../evm/oapp/message-design-patterns) (where the destination OApp sends a new LayerZero message during `lz_receive`). This limitation is due to: - No `lz_send` account discovery: OApps are forced to freeze send-related configurations and declare all associated accounts within lz_receive_types(). - CPI depth limit: `lz_receive` executed via [Cross-Program Invocation (CPI)](../../../concepts/glossary#cpi-cross-program-invocation), encounters a limitation due to [Solana's maximum CPI depth of 4](../oft/overview#cross-program-invocation-into-the-oft-program-cpi-depth-limitation). This depth is fully utilized by [ULN](../../../concepts/glossary#uln-ultra-light-node)-based `lz_send` operations, making ABA messaging infeasible. #### 4. No support for Multiple Instructions `lz_receive_types` (v1) supports only a single-instruction execution model, but on Solana composing program behavior through multiple instructions is a common pattern, especially to work around the CPI depth limit of 4. #### 5. No Support for EOA Account Initialization The [Executor](../../../concepts/glossary#executor) provides only a single signer as the payer for account rent. `lz_receive_types` (v1) does not support passing in additional EOAs as signers to create arbitrary writable data accounts on demand, which limits account creation and flexibility in state management. #### 6. OApp-Unaware Executor Fee Limit While the Executor enforces an internal limit on how much SOL can be spent from its signer account, the OApp has no visibility into this threshold. This lack of transparency makes it difficult for OApps to reason about safe spending behavior, increasing the risk of unintentionally exceeding the limit and triggering transaction failure. ### What `lz_receive_types_v2` introduces `lz_receive_types_v2` addresses the limitations of `lz_receive_types` (v1) through the following key features: - **Support for multiple ALTs**: Expands the capacity for account inclusion by allowing multiple Address Lookup Tables, increasing the number of accounts accessible within a transaction. - **Compact and flexible account reference model**: Implements the **AddressLocator**, enabling OApps to reference accounts with a leaner, more efficient structure while maintaining adaptability for future upgrades. - **Context account from the Executor**: Provides runtime metadata at execution, ensuring that OApps have contextual awareness without requiring redundant account fetching or manual setup. - **Explicit support for multiple EOA signers**: Enables dynamic data account initialization by allowing multiple externally owned accounts (EOAs) to participate in signing and setup. This improves flexibility for multi-party or multi-step workflows. - **Multi-instruction execution model**: Empowers OApps to compose complex workflows within a single atomic transaction, combining several instructions while preserving consistency and rollback guarantees. ### How `lz_receive_types_v2` works Overall, `lz_receive_types_v2` has the following execution flow: ``` (1) lz_receive_types_info | | LzReceiveTypesV2Accounts v (2) lz_receive_types_v2 | | Full list of instructions for lz_receive + ALTs v (3) build and submit transaction (including lz_receive) ``` 1. `lz_receive_types_info` - this instruction only requires the `lz_receive_types_account` PDA account, and returns `(version, versioned_data)`: - `version: u8` — A protocol-defined version identifier for the `LzReceiveType` logic and return type, starting from 2. - `versioned_data: Any` — A value of type `Any`, representing a version-specific structure. The Executor decodes this payload based on the version and uses it to construct the full set of accounts needed to invoke `lz_receive_types` (`LzReceiveTypesV2Accounts`). 2. `lz_receive_types_v2` - this instruction is called with `LzReceiveTypesV2Accounts` as the supplied accounts and returns: - The full list of instructions for `lz_receive` - ALTS used (if any) 3. `build and submit transaction` - now the Executor can prepare the full transaction and submit it based on what was returned by `lz_receive_types_v2`. Before submitting, the Executor will also prepend and append instructions to prepare context and ensure safety. :::info All the instructions above are called by the Executor. As the OApp developer, you only need to ensure that you implement the `lz_receive_types_v2` interface in your OApp program. ::: ### Implementing `lz_receive_types_v2` Define `LzReceiveTypesAccounts` anywhere in your state module tree: ```rust /// LzReceiveTypesAccounts includes accounts that are used in the LzReceiveTypes instruction. #[account] #[derive(InitSpace)] pub struct LzReceiveTypesAccounts { pub store: Pubkey, // Note: This is used as your OApp address. pub alt: Pubkey, // Note: in this example, we store a single ALT. You can modify this to store a Vec of Pubkeys too. pub bump: u8, // Note: you may add more account Pubkeys into this struct, per your use case. } ``` Ensure that you init the `LzReceiveTypesAccounts` PDA in your `init` instruction: ```rust use crate::{ state::{LzReceiveTypesAccounts}, STORE_SEED, }; use anchor_lang::prelude::*; use anchor_lang::solana_program::address_lookup_table::program::ID as ALT_PROGRAM_ID; use oapp::{ endpoint::{instructions::RegisterOAppParams, ID as ENDPOINT_ID}, LZ_RECEIVE_TYPES_SEED, }; #[derive(Accounts)] pub struct Init<'info> { #[account(mut)] pub payer: Signer<'info>, #[account( init, payer = payer, space = 8 + Store::INIT_SPACE, seeds = [STORE_SEED], bump )] pub store: Account<'info, Store>, #[account( init, payer = payer, space = 8 + LzReceiveTypesAccounts::INIT_SPACE, seeds = [LZ_RECEIVE_TYPES_SEED, store.key().as_ref()], bump )] pub lz_receive_types_accounts: Account<'info, LzReceiveTypesAccounts>, #[account(owner = ALT_PROGRAM_ID)] pub alt: Option>, pub system_program: Program<'info, System>, } impl Init<'_> { pub fn apply(ctx: &mut Context, params: &InitParams) -> Result<()> { ctx.accounts.store.endpoint_program = if let Some(endpoint_program) = params.endpoint_program { endpoint_program } else { ENDPOINT_ID }; ctx.accounts.store.bump = ctx.bumps.store; ctx.accounts.lz_receive_types_accounts.store = ctx.accounts.store.key(); // Set ALT if provided, otherwise default to Pubkey::default() ctx.accounts.lz_receive_types_accounts.alt = ctx.accounts.alt.as_ref().map(|a| a.key()).unwrap_or_default(); ctx.accounts.lz_receive_types_accounts.bump = ctx.bumps.lz_receive_types_accounts; let seeds: &[&[u8]] = &[STORE_SEED, &[ctx.accounts.store.bump]]; // Register the oapp oapp::endpoint_cpi::register_oapp( ctx.accounts.store.endpoint_program, ctx.accounts.store.key(), ctx.remaining_accounts, seeds, RegisterOAppParams { delegate: params.default_admin }, )?; Ok(()) } } ``` Create an `lz_receive_types_info` instruction: ```rust use oapp::{ lz_receive_types_v2::{LzReceiveTypesV2Accounts, LZ_RECEIVE_TYPES_VERSION}, LzReceiveParams, LZ_RECEIVE_TYPES_SEED, }; use crate::*; /// LzReceiveTypesInfo instruction implements the versioning mechanism introduced in V2. /// /// This instruction addresses the compatibility risk of the original LzReceiveType V1 design, /// which lacked any formal versioning mechanism. The LzReceiveTypesInfo instruction allows /// the Executor to determine how to interpret the structure of the data returned by /// lz_receive_types() for different versions. /// /// Returns (version, versioned_data): /// - version: u8 — A protocol-defined version identifier for the LzReceiveType logic and return /// type /// - versioned_data: Any — A version-specific structure that the Executor decodes based on the /// version /// /// For Version 2, the versioned_data contains LzReceiveTypesV2Accounts which provides information /// needed to construct the call to lz_receive_types_v2. #[derive(Accounts)] pub struct LzReceiveTypesInfo<'info> { #[account(seeds = [STORE_SEED], bump = store.bump)] pub store: Account<'info, Store>, /// PDA account containing the versioned data structure for V2 /// Contains the accounts needed to construct lz_receive_types_v2 instruction /// Derived using: seeds = [LZ_RECEIVE_TYPES_SEED, &count.key().to_bytes()] #[account(seeds = [LZ_RECEIVE_TYPES_SEED, &store.key().to_bytes()], bump = lz_receive_types_accounts.bump)] pub lz_receive_types_accounts: Account<'info, LzReceiveTypesAccounts>, } impl LzReceiveTypesInfo<'_> { /// Returns the version and versioned data for LzReceiveTypes /// /// Version Compatibility: /// - Forward Compatibility: Executors must gracefully reject unknown versions /// - Backward Compatibility: Version 1 OApps do not implement lz_receive_types_info; Executors /// may fall back to assuming V1 if the version instruction is missing or unimplemented /// /// For V2, returns: /// - version: 2 (u8) /// - versioned_data: LzReceiveTypesV2Accounts containing the accounts needed for /// lz_receive_types_v2 pub fn apply( ctx: &Context, params: &LzReceiveParams, ) -> Result<(u8, LzReceiveTypesV2Accounts)> { let receive_types_account = &ctx.accounts.lz_receive_types_accounts; let required_accounts = if receive_types_account.alt == Pubkey::default() { vec![ receive_types_account.store // You can include more accounts here if necessary ] } else { vec![ receive_types_account.store, // You can include more accounts here if necessary ] }; Ok((LZ_RECEIVE_TYPES_VERSION, LzReceiveTypesV2Accounts { accounts: required_accounts })) } } ``` Implement `lz_receive_types_v2`: ```rust use crate::*; use anchor_lang::solana_program; use oapp::{ common::{ compact_accounts_with_alts, AccountMetaRef, AddressLocator, EXECUTION_CONTEXT_VERSION_1, }, lz_receive_types_v2::{ Instruction, LzReceiveTypesV2Result, }, LzReceiveParams, }; #[derive(Accounts)] #[instruction(params: LzReceiveParams)] pub struct LzReceiveTypesV2<'info> { #[account(seeds = [STORE_SEED], bump = store.bump)] pub store: Account<'info, Store>, // Note: include more accounts here if you had done so in the previous steps } impl LzReceiveTypesV2<'_> { /// Returns the execution plan for lz_receive with a minimal account set. pub fn apply( ctx: &Context, params: &LzReceiveParams, ) -> Result { // Derive peer PDA from src_eid only (OFT key removed) let peer_seeds = [PEER_SEED, ¶ms.src_eid.to_be_bytes()]; let (peer, _) = Pubkey::find_program_address(&peer_seeds, ctx.program_id); // Event authority used for logging let (event_authority_account, _) = Pubkey::find_program_address(&[oapp::endpoint_cpi::EVENT_SEED], &ctx.program_id); let accounts = vec![ // payer AccountMetaRef { pubkey: AddressLocator::Payer, is_writable: true }, // peer AccountMetaRef { pubkey: peer.into(), is_writable: false }, // event authority account - used for event logging AccountMetaRef { pubkey: event_authority_account.into(), is_writable: false }, // system program AccountMetaRef { pubkey: solana_program::system_program::ID.into(), is_writable: false, }, // program id - the program that is executing this instruction AccountMetaRef { pubkey: crate::ID.into(), is_writable: false }, ]; // Return the execution plan (no clear/compose helper accounts) Ok(LzReceiveTypesV2Result { context_version: EXECUTION_CONTEXT_VERSION_1, alts: ctx.remaining_accounts.iter().map(|alt| alt.key()).collect(), instructions: vec![ Instruction::LzReceive { accounts: compact_accounts_with_alts(&ctx.remaining_accounts, accounts)?, }, ], }) } } ``` Ensure that you have registered the new instruction handlers in the program module in your `lib.rs`: ```rust #[program] pub mod my_oapp { use super::*; // ...the existing instructions pub fn lz_receive_types_v2( ctx: Context, params: LzReceiveParams, ) -> Result { LzReceiveTypesV2::apply(&ctx, ¶ms) } pub fn lz_receive_types_info( ctx: Context, params: LzReceiveParams, ) -> Result<(u8, LzReceiveTypesV2Accounts)> { LzReceiveTypesInfo::apply(&ctx, ¶ms) } } ``` ## `lz_compose_types_v2` `lz_compose_types_v2` achieves similar goals to `lz_receive_types_v2` (supports more accounts, multiple instructions, multiple signers) but for compose messages. The flow is similar: discover versioned accounts via `lz_compose_types_info`, return a compact, ALT-aware execution plan via `lz_compose_types_v2`, then the Executor builds and submits the transaction that includes the `lz_compose` instruction. ### Implementing `lz_compose_types_v2` Define the struct of PDA that holds versioned compose-type discovery data, e.g. `LzComposeTypesAccounts`: ```rust /// LzComposeTypesAccounts includes accounts that are used in the LzComposeTypesV2 instruction. #[account] #[derive(InitSpace)] pub struct LzComposeTypesAccounts { pub store: Pubkey, // Note: This is used as your OApp address. pub alt: Pubkey, // Optionally store a single ALT (or change to Vec for many) pub bump: u8, // You may add more Pubkeys here per your use case } ``` Initialize the `LzComposeTypesAccounts` PDA in your `init` instruction: ```rust use crate::{ state::LzComposeTypesAccounts, STORE_SEED, }; use anchor_lang::prelude::*; use anchor_lang::solana_program::address_lookup_table::program::ID as ALT_PROGRAM_ID; use oapp::{ endpoint::{instructions::RegisterOAppParams, ID as ENDPOINT_ID}, LZ_COMPOSE_TYPES_SEED, }; #[derive(Accounts)] pub struct Init<'info> { // .. existing accounts include lz_receive_types_accounts and its ALT pub lz_compose_types_accounts: Account<'info, LzComposeTypesAccounts>, // Note: For simplicity, we will use the same ALT for both lz_receive_types_accounts and lz_compose_types_accounts, but you can also accept two separate ALTs if you want the ability to specify them separately. // If you choose to specify them separately, then you can name them something like lz_receive_alt and lz_compose_alt, and just modify the instruction handler below to use the right account keys #[account(owner = ALT_PROGRAM_ID)] pub alt: Option>, // optional. pub system_program: Program<'info, System>, } impl Init<'_> { pub fn apply(ctx: &mut Context, params: &InitParams) -> Result<()> { // existing code ctx.accounts.lz_compose_types_accounts.store = ctx.accounts.store.key(); ctx.accounts.lz_compose_types_accounts.alt = ctx.accounts.alt.as_ref().map(|a| a.key()).unwrap_or_default(); ctx.accounts.lz_compose_types_accounts.bump = ctx.bumps.lz_compose_types_accounts; // existing code } } ``` Create an `lz_compose_types_info` instruction that returns the version and the accounts needed to construct `lz_compose_types_v2`: ```rust use oapp::{ lz_compose_types_v2::{LzComposeTypesV2Accounts, LZ_COMPOSE_TYPES_VERSION}, LzComposeParams, LZ_COMPOSE_TYPES_SEED, }; use crate::*; #[derive(Accounts)] pub struct LzComposeTypesInfo<'info> { #[account(seeds = [STORE_SEED], bump = store.bump)] pub store: Account<'info, Store>, /// PDA containing the versioned data structure for V2 /// Derived using: seeds = [LZ_COMPOSE_TYPES_SEED, &store.key().to_bytes()] #[account(seeds = [LZ_COMPOSE_TYPES_SEED, &store.key().to_bytes()], bump = lz_compose_types_accounts.bump)] pub lz_compose_types_accounts: Account<'info, LzComposeTypesAccounts>, } impl LzComposeTypesInfo<'_> { /// Returns (version, versioned_data) used by the Executor pub fn apply( ctx: &Context, _params: &LzComposeParams, ) -> Result<(u8, LzComposeTypesV2Accounts)> { let compose_types_account = &ctx.accounts.lz_compose_types_accounts; let required_accounts = if compose_types_account.alt == Pubkey::default() { vec![compose_types_account.store] } else { vec![compose_types_account.store, compose_types_account.alt ] }; Ok((LZ_COMPOSE_TYPES_VERSION, LzComposeTypesV2Accounts { accounts: required_accounts })) } } ``` Implement `lz_compose_types_v2` and return a compact execution plan including exactly one `Instruction::LzCompose`: ```rust use crate::*; use oapp::{ common::{compact_accounts_with_alts, AccountMetaRef, EXECUTION_CONTEXT_VERSION_1}, endpoint::ID as ENDPOINT_ID, lz_compose_types_v2::{get_accounts_for_clear_compose, Instruction, LzComposeTypesV2Result}, LzComposeParams, }; #[derive(Accounts)] #[instruction(params: LzComposeParams)] pub struct LzComposeTypesV2<'info> { #[account(seeds = [STORE_SEED], bump = store.bump)] pub store: Account<'info, Store>, } impl LzComposeTypesV2<'_> { /// Returns the execution plan for lz_compose with a minimal account set. pub fn apply( ctx: &Context, params: &LzComposeParams, ) -> Result { let mut accounts = vec![ AccountMetaRef { pubkey: ctx.accounts.store.key().into(), is_writable: true }, ]; // Endpoint helper accounts for compose let accounts_for_composing = get_accounts_for_clear_compose( ENDPOINT_ID, ¶ms.from, &ctx.accounts.store.key(), // receiver (self or target PDA) ¶ms.guid, params.index, ¶ms.message, ); accounts.extend(accounts_for_composing); Ok(LzComposeTypesV2Result { context_version: EXECUTION_CONTEXT_VERSION_1, alts: ctx.remaining_accounts.iter().map(|alt| alt.key()).collect(), instructions: vec![Instruction::LzCompose { accounts: compact_accounts_with_alts(&ctx.remaining_accounts, accounts)?, }], }) } } ``` Finally, register the new instruction handlers in your `lib.rs`: ```rust #[program] pub mod my_oapp { use super::*; // ...the existing instructions pub fn lz_compose_types_v2( ctx: Context, params: LzComposeParams, ) -> Result { LzComposeTypesV2::apply(&ctx, ¶ms) } pub fn lz_compose_types_info( ctx: Context, params: LzComposeParams, ) -> Result<(u8, LzComposeTypesV2Accounts)> { LzComposeTypesInfo::apply(&ctx, ¶ms) } } ``` --- --- title: Solana OFT sidebar_label: Solana OFT --- 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](https://spl.solana.com/token)), the [OFT Program](#the-oft-program) is different in this respect. Because every Solana Program has an Upgrade Authority, and this authority can change or modify the implementation of all child accounts, developers wishing to create cross-chain tokens on Solana should deploy their own instance of the [OFT Program](#the-oft-program) to create new [OFT Store](#oft-account-model) accounts, so that they own their OFT's Upgrade Authority. :::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. ::: ## Quickstart ### Example For the step-by-step instructions on how to build, deploy and wire a Solana OFT, view the [Solana OFT example](https://github.com/LayerZero-Labs/devtools/tree/main/examples/oft-solana). ### Scaffold Spin up a new OFT workspace (based on the example) in seconds: ```bash LZ_ENABLE_SOLANA_OFT_EXAMPLE=1 npx create-lz-oapp@latest ``` Specify the directory, select `OFT (Solana)` and proceed with the installation. Follow the provided README instructions to make your first cross-chain OFT transfer between Solana and an EVM chain. The rest of this page contains additional information that you should read before deploying to mainnet. ## Prerequisite Knowledge Understanding the following will help you with the rest of this page: - [Mint Authority and Freeze Authority](https://solana.com/docs/core/tokens#mint-account) - [Token Metadata](https://solana.com/developers/guides/token-extensions/metadata-pointer#token-metadata-interface-overview) - [Solana Account Model](https://solana.com/docs/core/accounts) - [Solana Program Library](https://spl.solana.com/token) and the [Token-2022](https://spl.solana.com/token-2022) ## The OFT Program The **OFT Program** interacts with the **Solana Token Program** to allow new or existing Fungible Tokens on Solana to transfer balances between different chains. :::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). 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 6 main accounts: | Account Name | Executable | Description | | ----------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | OFT Program | `true` | The OFT Program itself, the executable, stateless code which controls how OFTs interact with the LayerZero Endpoint and the SPL Token. | | Mint Account | `false` | This is the [Mint Account](https://solana.com/docs/core/tokens#mint-account) for the OFT's SPL Token. Stores the key metadata for a specific token, such as total supply, decimal precision, mint authority, freeze authority and update authority. | | Mint Authority Multisig | `false` | A 1 of N [Multisig](https://spl.solana.com/token#example-mint-with-multisig-authority) that serves as the Mint Authority for the SPL Token. The OFT Store is always required as a signer. It's also possible to add additional signers. | | Escrow | `false` | The **Token Account** for the corresponding **Mint Account**, owned by the **OFT Store**. For **OFT Adapter** deployments and also for storing fees, if fees are enabled. For both OFT and OFT Adapter, the Escrow address is part of the derivation for the OFT Store PDA. Escrow is a regular Token Account and not an Associated Token Account. | | OFT Store | `false` | A [PDA](https://solana.com/docs/core/pda) account that stores data about each OFT such as the underlying SPL Token Mint, the SPL Token Program, Endpoint Program, the OFT's fee structure, and extensions. Is the owner for the Escrow account. The OFT Store is a signer for the Mint Authority multisig. | | PeerConfig | `false` | A [PDA](https://solana.com/docs/core/pda) account that stores configuration for each remote chain, including peer addresses, enforced options, rate limiters, and fee settings. This account is derived from the OFT Store and remote [EID](/v2/concepts/glossary#endpoint-id). | :::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. ::: ## 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) 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. If you had set `enforcedOptions`, then you can pass an empty bytes array (`0x` if sending from EVM, `Buffer.from('')` if sending from Solana). If you did not set `enforcedOptions`, then continue reading. ### Setting Extra Options Any `_options` passed in the `send` call itself is considered as `_extraOptions`. `_extraOptions` can specify additional handling within the same message type. These `_options` will then be combined with `enforcedOption` if set. You can find how to generate all the available `_options` in [Message Execution Options](../../evm/configuration/options.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 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 [Message Execution Options](#message-execution-options). :::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 Options Inbound to Solana When sending to Solana, a `msg.value` is only required if the recipient address does not already have the [Associated Token Account (ATA)](https://www.alchemy.com/overviews/associated-token-account) for your mint. There are two ways to provide this value: - Enforced Options (app-level default): set `value` in `enforcedOptions` for the pathway. This guarantees the amount is always included, but it will waste lamports for recipients that already have an ATA. Use with caution in production. - Extra Options (per-transaction): set `msg.value` in `extraOptions` only when needed after checking whether the recipient’s ATA exists. This is the recommended approach to avoid unnecessary costs. How much `value` to provide: - SPL Token accounts: the rent-exempt amount is `2_039_280` lamports (0.00203928 SOL). - Token-2022 accounts: the required value depends on the token account size, which varies by the enabled extensions. You can inspect the size of your token's token account and [calculate the rent amount needed](https://www.quicknode.com/guides/solana-development/getting-started/understanding-rent-on-solana). If setting via `enforcedOptions` in `layerzero.config.ts`, the parameter is `value`. If building per-transaction options in TypeScript, it is the second parameter to `addExecutorLzReceiveOption(gas_limit, msg_value)`. See the next section for how to detect ATA existence and attach `msg.value` conditionally via `extraOptions`. :::info For Solana OFTs that use Token2022, you will need to increase `value` to a higher amount, which depends on the token account size, which in turn depends on the extensions that you enable. ::: :::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 when the recipient’s ATA does not already exist. ::: For EVM → Solana sends, enforce the compute units ("gas") at the application level using `enforcedOptions`. When attaching per-transaction value for ATA creation via `extraOptions`, set gas to `0` and only provide `msg.value` as needed. The protocol combines `extraOptions` with your enforced baseline at execution time. #### Conditional msg.value for ATA creation For sends to Solana, you can avoid overpaying rent by setting your enforced options `value` to 0 and supplying `msg.value` only when the recipient’s [Associated Token Account (ATA)](https://www.alchemy.com/overviews/associated-token-account) is missing. This pattern is useful when recipients may or may not have an ATA for your mint. Steps: - Set `enforcedOptions` value to 0 in `layerzero.config.ts` for pathways that deliver to Solana. - Before constructing `extraOptions` for a specific send to Solana, check if the recipient’s ATA exists. - If ATA exists: set `msg.value = 0` in `addExecutorLzReceiveOption`. - If ATA does not exist: set `msg.value` to the rent-exempt minimum for the token account (e.g., `2_039_280` lamports for SPL; Token-2022 may require more depending on enabled extensions). Example: check ATA existence using Umi and mpl-toolbox, then set options conditionally. ```typescript import {createUmi} from '@metaplex-foundation/umi-bundle-defaults'; import {findAssociatedTokenPda, safeFetchToken} from '@metaplex-foundation/mpl-toolbox'; import {publicKey} from '@metaplex-foundation/umi'; import {Options} from '@layerzerolabs/lz-v2-utilities'; const umi = createUmi('https://api.mainnet-beta.solana.com'); const mint = publicKey(''); const owner = publicKey(''); // derive ATA PDA const ata = findAssociatedTokenPda(umi, {mint, owner}); // check if it exists const account = await safeFetchToken(umi, ata); if (account) { console.log('ATA exists at', ata.toString()); } else { console.log('ATA not found'); } // set per-tx options based on ATA existence // gas is enforced at app-level; set 0 here to avoid double-charging const GAS_LIMIT = 0; const SPL_TOKEN_ACCOUNT_RENT_VALUE = 2039280; // rent-exempt lamports for SPL token account (ATA) const MSG_VALUE = account ? 0 : SPL_TOKEN_ACCOUNT_RENT_VALUE; // if Token2022, use a higher value based on account size const options = Options.newOptions().addExecutorLzReceiveOption(GAS_LIMIT, MSG_VALUE); // rest of your code; calls to quote/send ``` Use `options.toHex()` (EVM) or `options.toBytes()` (Solana) when populating `extraOptions`/`options` in your send call. These values will be combined with any `enforcedOptions` configured at the app level. If your mint is Token-2022, compute the rent-exempt minimum from the token account size (varies by enabled extensions) and replace `SPL_TOKEN_ACCOUNT_RENT_VALUE` accordingly. ## Precautions ### One OFT Adapter per OFT deployment/mesh Multiple OFT Adapters break omnichain unified liquidity by effectively creating token pools. If you create OFT Adapters on multiple chains, you have no way to guarantee finality for token transfers due to the fact that the source chain has no knowledge of the destination pool's supply (or lack of supply). This can create race conditions where if a sent amount exceeds the available supply on the destination chain, those sent tokens will be permanently lost. ### Token Transfer Precision The OFT Standard also handles differences in decimal precision before every 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. ### Choosing the right local decimals value Be careful when selecting your Solana token's local decimals. Although the default is `9` (SPL standard), choosing a value that is too high can severely limit your maximum mintable supply because Solana balances are `u64`. For instance, setting `18` decimals (common on EVM) would cap your supply to roughly ~18 whole tokens on Solana. Prefer the smallest decimals that satisfy your UX and supply requirements (many projects use `6` or `9`). This is independent from `sharedDecimals` (default `6`), which governs cross-chain precision and dust handling. See the detailed guidance and max-supply table in [Deciding the number of local decimals for your Solana OFT](../technical-reference/solana-guidance.md#deciding-the-number-of-local-decimals-for-your-solana-oft). ## (Optional) Verify the OFT Program To continue, you must first install [solana-verify](https://github.com/Ellipsis-Labs/solana-verifiable-build). You can learn about how program verification works in the [official Solana program verification guide](https://solana.com/developers/guides/advanced/verified-builds#how-does-it-work). :::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). ::: :::caution Program verification is tied to the program's Upgrade Authority. If you transfer a program's Upgrade Authority, you will need to redo the verification steps using the new Upgrade Authority address. ::: ## 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. ::: ## 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 net difference in SOL will be zero if your program's size did not change, you will still need the same amount of SOL as required by the program's rent due to how Solana program upgrades work, which is as follows: - the existing program starts off as being unaffected - the updated program's bytecode is uploaded to a **buffer account (new account, hence SOL for rent is required)** which acts as a temporary staging area - the contents of the buffer account are then copied to the program data account - the buffer account is closed, and its rent SOL is returned Run the deploy command to upgrade the program. ```bash 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). ::: ### 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. ## 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. ::: --- --- 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/common/sendOFT.ts](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/common/sendOFT.ts) for the example usage. ## Using the Solana OFT SDK in the Frontend The Solana OFT SDK has been updated to be browser-compatible. Versions prior to `3.0.71` required more additional configurations via your bundling tool. For Next projects, no additional configurations are required to use the Solana OFT SDK. For Vite projects, the following are the minimal configurations required to work. `nodePolyfills` is required as `Buffer` is required by `@solana/web3.js` but Vite does not polyfill it by default. ```typescript // vite.config.ts import {defineConfig} from 'vite'; import react from '@vitejs/plugin-react'; import {nodePolyfills} from 'vite-plugin-node-polyfills'; // https://vite.dev/config/ export default defineConfig({ plugins: [react(), nodePolyfills()], }); ``` ### Requirements - `@layerzerolabs/oft-v2-solana-sdk@^3.0.86` - `@layerzerolabs/lz-v2-utilities@^3.0.86` - `@layerzerolabs/lz-definitions@^3.0.86` - `@metaplex-foundation/umi@^0.9.2` - `@metaplex-foundation/umi-bundle-defaults@^0.9.2` - `@metaplex-foundation/umi-signer-wallet-adapters@^0.9.2` - `@solana/web3.js@^1.95.8` #### Applying overrides for `@solana/web3.js` In `package.json` add the following `resolutions` / `overrides` to ensure a consistent version of `@solana/web3.js` is used: ``` "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 Guidance --- This page provides development guidance for building on Solana. While some entries are LayerZero-specific, others cover general topics and tooling relevant to the Solana ecosystem. ## Deploying Solana programs with a priority fee This section applies if you are unable to land your deployment transaction due to network congestion. [Priority Fees](https://solana.com/developers/guides/advanced/how-to-use-priority-fees) are Solana's mechanism to allow transactions to be prioritized during periods of network congestion. When the network is busy, transactions without priority fees might never be processed. It is then necessary to include priority fees, or wait until the network is less congested. Priority fees are calculated as follows: `priorityFee = compute budget * compute unit price`. We can make use of priority fees by attaching the `--with-compute-unit-price` flag to our `solana program deploy` command. Note that the flag takes in a value in micro lamports, where 1 micro lamport = 0.000001 lamport. For example: ```bash solana program deploy --program-id target/deploy/oft-keypair.json target/verifiable/oft.so -u devnet --with-compute-unit-price ``` You can refer QuickNode's [Solana Priority Fee Tracker](https://www.quicknode.com/gas-tracker/solana) to know what value you'd need to pass into the `--with-compute-unit-price` flag. ## Transferring OFT ownership on Solana There are six roles regarding OFT ownership/authority on Solana: - **owner** - **delegate** - **upgrade authority** - **token metadata update authority** - (if applicable) **mint authority** - (if applicable) **anchor IDL authority** - (if applicable) **freeze authority** When you deploy a Solana OFT, the deployer wallet is automatically set as the **owner** and **delegate**. The deployer wallet would also have been made as the [upgrade authority](https://solana.com/docs/core/programs#updating-solana-programs) of your OFT program. **Owner** and **delegate** are specific to the LayerZero OFT context whereas **upgrade authority** is generic to Solana. The transfer of **owner** and **delegate** are separate from the transfer of **upgrade authority**. The steps below are identical regardless if the new owner and delegate are Multisig accounts. ### Transferring Owner and Delegate The transfer of both require modifying your [LZ Config](../../../concepts/glossary.md#lz-config) file and running helper tasks. Overall, you should carry out these steps: 1. Modify LZ Config to include **only** the [new delegate address](../../../get-started/create-lz-oapp/configuring-pathways.md#adding-delegate) 2. Run `pnpm hardhat lz:oapp:wire --oapp-config layerzero.config.ts` 3. Modify LZ Config to include the [new owner address](../../../get-started/create-lz-oapp/configuring-pathways.md#adding-owner) 4. Run `pnpm hardhat lz:ownable:transfer-ownership --oapp-config layerzero.config.ts` You have now transferred both owner and delegate of your Solana OFT. ### Transferring the OFT Program Upgrade Authority The steps may differ depending on whether (1) **the new upgrade authority is a regular account that you control** or whether (2) **the new upgrade authority is a Multisig or an account that you do not control**.\ They differ in whether the new upgrade authority's keypair is included or whether you use the `--skip-new-upgrade-authority-signer-check` param. #### The new upgrade authority is an account that you control Run the follwowing: ```bash solana program set-upgrade-authority --keypair --new-upgrade-authority ``` #### The new upgrade authority is a Squads Multisig or an account that you do not control The steps below are identical whether your new upgrade authority is a Squads Multisig or whether it's an account that you do no control. If it is a Squads Multisig, note that the address you want to pass in is the [Vault Account](https://docs.squads.so/main/navigating-your-squad/settings#vault-and-multisig-address) address. > This differs from the current `--multisig` param required by LayerZero helpers which requires the Multisig account address With the correct new upgrade authority address prepared, run the following: ```bash solana program set-upgrade-authority --skip-new-upgrade-authority-signer-check --new-upgrade-authority ``` ### Transferring the Token Metadata Update Authority The [Token Metadata](https://developers.metaplex.com/token-metadata) Update Authority is able to update the Solana token's metadata such as name, symbol, uri and creators information. To transfer the Update Authority, you can use the [setUpdateAuthority helper script](https://developers.metaplex.com/token-metadata/update): ```bash pnpm hardhat lz:oft:solana:set-update-authority --eid --mint --new-update-authority ``` - `` - `30168` for Solana Mainnet, `40168` for Solana Devnet > Read more on what the Update Authority can do here: https://developers.metaplex.com/token-metadata/update ### (if applicable) Transfer the Mint Authority This section only applies if the Solana OFT was created with Additional Minters. You can verify this by viewing the Solana OFT's Mint Authority. If it is an SPL Multisig with more than 1 address, then there are additional minters. If the Mint Authority is a single PDA and not an SPL Multisig, then the Mint Authority is the OFT Store and no transfer steps are necessary for the Mint Authority. SPL Multisigs can only be created and not be edited. If your current Solana OFT has an additional minter and you need to change the additional minter address, then a new 1 of N SPL Multisig needs to be created. For this, you can simply run the [setAuthority helper](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/solana/setAuthority.ts): #### If you no longer need additional minters: ```bash pnpm hardhat lz:oft:solana:setauthority --eid 30168 --mint --program-id --escrow --only-oft-store true ``` - `` - `30168` for Solana Mainnet, `40168` for Solana Devnet - `--only-oft-store true` - This is irreversible and will set the Mint Authority to the OFT Store directly. This will update the Mint Authority to be the OFT Store PDA. No SPL Multisig will be created. #### If there should be new additional minter(s) ```bash pnpm hardhat lz:oft:solana:setauthority --eid 30168 --mint --program-id --escrow --additional-minters ``` - `` - `30168` for Solana Mainnet, `40168` for Solana Devnet - `--additional-minters` comma-separated list of additional minters This will create a new 1 of N SPL Multisig with the OFT Store and the additional minter(s) as signers. ### (if applicable) Transfer the Anchor IDL Authority This only applies if you had previously uploade the Anchor IDL on-chain. A quick way to test this is by running ```bash anchor idl authority --provider.cluster mainnet ``` If it returns with `Error: AccountNotFound: pubkey=`, this means no Anchor IDL PDA had been created and you can ignore this step. To transfer the Anchor IDL Authority, run: ```bash anchor idl set-authority --program-id --new-authority ``` ### (if applicable) Transfer the Freeze Authority While the OFT Program does not require the Freeze Authority at all, your Solana Token might have its Freeze Authority set. To transfer the Freeze Authority to a new address: ```bash spl-token authorize freeze ``` To renounce the Freeze Authority (irreversible): ```bash spl-token authorize freeze --disable ``` ## Deciding the number of local decimals for your Solana OFT As OFTs can span across VMs, with each VM potentially using a different data type for token amounts, it's important to understand the concept of decimals in the context of OFTs. Make sure you understand [shared decimals](../../../concepts/glossary.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/overview.md#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 ) | :::warning If you create a Solana token with 18 decimals (common on EVM chains), the maximum supply on Solana will be only ~18 tokens due to the `u64` limit. Choose a lower decimals value that can accommodate your intended max supply. ::: ## Squads Multisig – Multisig Account vs Vault In Squads, there are two distinct address types: - **Multisig Account** – the primary account that manages the Squad. - **Vault** – a derived account (at a specific index) where assets and program interactions occur. For a deeper explanation, refer to the official Squads documentation: [Vault and Multisig Address](https://docs.squads.so/main/navigating-your-squad/settings#vault-and-multisig-address). When using LayerZero Hardhat Helpers with the `--multisig-key` flag: - **Provide the Multisig Account address**, **not** the Vault address. - The helper internally derives the Vault address at **index 0** to propose transactions to. ## Creating a Squads Multisig on Solana Devnet [Squads](https://squads.xyz/) is the most widely used multisig on Solana. The current version of Squads is v4. The OFT tasks support the usage of a Squads via the `--multisig-key` param. On mainnet, you can create a v4 Multisig using the [Mainnet UI](https://app.squads.so/squads). On the [devnet UI](https://backup.app.squads.so/), you are currently not able to create a multisig. However, you can still perform operations such as voting on transactions and executing them. In order to create a Squads v4 Multisig for Solana Devnet, you have two options: CLI and Typescript SDK. ### Creating using the CLI With the [Squads CLI](https://docs.squads.so/main/development/cli/installation) installed, you can run: ```bash multisig-create --rpc-url --keypair --members ... --threshold ``` For full context and instructions, refer to https://docs.squads.so/main/development/cli/commands#multisig-create ### Creating using the Typescript SDK Dependencies: ``` "@solana-developers/helpers": "^2.5.6", "@solana/web3.js": "^1.98.0", "@sqds/multisig": "^2.1.3", ``` Here is a minimal script for creating a multisig via the Typescript SDK: ```typescript import * as multisig from '@sqds/multisig'; import {Connection, Keypair, clusterApiUrl} from '@solana/web3.js'; import {getKeypairFromFile} from '@solana-developers/helpers'; const {Permission, Permissions} = multisig.types; (async () => { const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); // or "mainnet-beta" for mainnet // Signers const creator = await getKeypairFromFile(); // first member + fee-payer // const secondMember = Keypair.generate(); // second member // if you add another member, remember to update the threshold if not going for 1 of N const createKey = Keypair.generate(); // seed for the PDA (must sign) // Derive PDA for the multisig. This will be the multisig account address. const [multisigPda] = multisig.getMultisigPda({ createKey: createKey.publicKey, }); const programConfigPda = multisig.getProgramConfigPda({})[0]; const programConfig = await multisig.accounts.ProgramConfig.fromAccountAddress( connection, programConfigPda, ); const configTreasury = programConfig.treasury; const sig = await multisig.rpc.multisigCreateV2({ connection, createKey, // must sign creator, // must sign & pays fees multisigPda, threshold: 1, // timeLock: 0, // no timelock configAuthority: null, rentCollector: null, treasury: configTreasury, members: [ {key: creator.publicKey, permissions: Permissions.all()}, // { key: secondMember.publicKey, permissions: Permissions.fromPermissions([Permission.Vote]) }, ], }); const latestBlockhashInfo = await connection.getLatestBlockhash(); await connection.confirmTransaction({ signature: sig, blockhash: latestBlockhashInfo.blockhash, lastValidBlockHeight: latestBlockhashInfo.lastValidBlockHeight, }); console.log(`Multisig account: ${multisigPda.toBase58()}`); console.log(`Multisig creation txn link: https://solscan.io/tx/${sig}?cluster=devnet`); })(); ``` ### Using the created Multisig Account in the Squads Devnet UI In the Squads v4 Devnet UI, on the initial page load you'll be asked to fill up the value for the **Multisig Config Address**. Input the address of the **Multisig Account** you had just created. If the page is not loading, try updating the Settings to use a private RPC URL. Also ensure that the RPC in use is for Solana Devnet and not Mainnet Beta. ## Implementing Time-locks for Solana OFT Mints The Solana OFT Program's mint function cannot be altered without breaking cross-chain transfers. If you require the ability to implement time-locks for minting operations, the timelock must be configured via an additional minter and NOT on the program's mint function itself. To implement time-locks for mints: - Specify additional minters when creating the OFT - Configure the timelock on the additional minter authority - Ensure the timelock is NOT applied directly to the program's mint function ### Using Squads Multisig for Time-locked Mints The additional minter can be a Squads Multisig, which you can configure to have a timelock for minting transactions. This approach allows you to implement secure time-delayed minting while preserving cross-chain transfer functionality. When configuring a Squads Multisig as an additional minter: 1. Set up the Squads Multisig as described in the previous section 2. Configure the desired timelock duration for the multisig transactions 3. Specify the multisig address as an additional minter when deploying your OFT For more information on configuring time-locks with Squads, refer to the [Squads Time-locks documentation](https://docs.squads.so/main/development/reference/time-locks). --- --- title: LayerZero V2 Solana Protocol Overview sidebar_label: Protocol Programs 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: 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**](../../../get-started/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**](../../../get-started/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](../../../concepts/glossary#dead-dvn) This configuration includes a **Dead DVN**: | SendUlnConfig (A to B) | ReceiveUlnConfig (B to A) | | ----------------------------------- | --------------------------------------- | | confirmations: 15 | confirmations: 15 | | optionalDVNCount: 0 | optionalDVNCount: 0 | | optionalDVNThreshold: 0 | optionalDVNThreshold: 0 | | optionalDVNs: Array(0) | optionalDVNs: Array(0) | | **requiredDVNCount: 2** | **requiredDVNCount: 2** | | **requiredDVNs: Array(DVN1, DVN2)** | **requiredDVNs: Array(DVN1, DVN_DEAD)** | :::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: 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. ### Why is sending an OFT to Solana more expensive than expected? Solana has the concept of 'rent' which now actually refers to the amount needed to satisfy the minimum balance required to be [rent-exempt](https://solana.com/docs/references/terminology#rent-exempt). Any account created on Solana requires this 'rent' amount. The majority of the message 'fee' when sending an OFT to Solana is to pay for this 'rent'. This is specified either via [enforced options](https://docs.layerzero.network/v2/concepts/message-options#enforcing-options) or in [extra options](https://docs.layerzero.network/v2/concepts/message-options#extra-options) as the message `value`. More specifically, the 'rent' applies to [token accounts](https://solana.com/docs/tokens#token-account) that need to be created when an address receives any token on Solana. The 'rent' amount varies according to the size in bytes of the account that needs to be created. Solana has two token account standards: [SPL](https://www.solana-program.com/docs/token) and [Token-2022](https://www.solana-program.com/docs/token-2022). SPL token accounts have a fixed size of 165 bytes, this results in a required rent amount of `0.00203928 SOL`. For Token-2022, token accounts can vary in size depending on which [token extensions](https://solana.com/developers/guides/token-extensions/getting-started#how-do-i-create-a-token-with-token-extensions) are enabled. Given a cross-chain transfer from a chain to Solana (that sets the CU limit (`message.gas`) to `200_000`), the following is a breakdown of how much SOL is needed to execute `lzReceive` on Solana: ``` 0.00203928 SOL (rent) + 0.000015 SOL (base fee) + 0.0002 SOL (priority fee) = 0.00225428 SOL ``` Breakdown: - Base fee = `3 signatures × 5,000 lamports = 15,000 lamports = 0.000015 SOL` - [Priority fee](https://solana.com/developers/guides/advanced/how-to-use-priority-fees#what-are-priority-fees) = `200_000 CU × 1 lamport per CU = 200,000 lamports = 0.0002 SOL` Given the above, the **rent accounts for ≈ 90.43% of the total SOL** needed for an `lzReceive` execution for a Solana OFT. Note that the rent is not needed when the receiving address already has a token account. --- --- sidebar_label: Start Here title: 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](../../evm/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/overview.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 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**](../../../get-started/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](../../../concepts/glossary#dead-dvn) This configuration includes a **Dead DVN**: | SendUlnConfig (A to B) | ReceiveUlnConfig (B to A) | | ----------------------------------- | --------------------------------------- | | confirmations: 15 | confirmations: 15 | | optionalDVNCount: 0 | optionalDVNCount: 0 | | optionalDVNThreshold: 0 | optionalDVNThreshold: 0 | | optionalDVNs: Array(0) | optionalDVNs: Array(0) | | **requiredDVNCount: 2** | **requiredDVNCount: 2** | | **requiredDVNs: Array(DVN1, DVN2)** | **requiredDVNs: Array(DVN1, DVN_DEAD)** | :::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 title: 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 consists of an `EVM` named `HyperEVM` and a `L1` network called `HyperCore`. These networks function together under the same `HyperBFT` consensus to act as a singular network. `HyperCore` includes fully onchain perpetual futures and spot order books. Every order, cancel, trade, and liquidation happens transparently with one-block finality inherited from HyperBFT. `HyperCore` currently supports 200k orders / second The `HyperEVM` brings the familiar general-purpose smart contract platform pioneered by Ethereum to the Hyperliquid blockchain. With the `HyperEVM`, the performant liquidity and financial primitives of `HyperCore` are available as permissionless building blocks for all users and builders. ![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, 1-second block time, 2M gas limit. For high throughput transactions. OFT deployments are typically larger than 2M gas. - **Big Blocks**: 1 transaction per block, 1 block per minute, 30M gas limit. For deploying large contracts. You can toggle between block types for your account using an `L1 action` of type `evmUserModify`: ```json {"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 and System Contracts Hyperliquid uses precompiles in two ways: System Contracts and L1ActionPrecompiles. **System Contracts**: - `0x2222222222222222222222222222222222222222`: System contract address for the `HYPE` token. - `0x200000000000000000000000000000000000abcd`: System contract address for a created Core Spot token (asset bridge). - `0x3333333333333333333333333333333333333333`: The `CoreWriter` for sending transactions to HyperCore. **L1ActionPrecompiles**: - `0x0000000000000000000000000000000000000000`: One of many `L1Read` precompiles. {" "} - `L1Read` reads from the last produced `HyperCore` block at EVM transaction execution. - `CoreWriter` writes to the first produced `HyperCore` block after the production of the EVM block. :::note `CoreWriter` is currently available on testnet only, with no publicly available timeline for a mainnet rollout. ::: ### 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 `CoreWriter` transaction (to `0x33...33`) instructing HyperCore to execute a `spot transfer` of the corresponding HIP-1 tokens from the Composer's implied Core address (derived from its EVM address) to the **actual receiver's address** (from `composeMsg`) on HyperCore. That particular `Transfer` event is what Hyperliquid nodes/relayers listen to in order to credit the `receiver` address on Core. ```solidity 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 CoreWriter ... } } ``` ### 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); bytes memory action = abi.encodePacked(_receiver, oftAsset.coreIndexId, amounts.core); bytes memory payload = abi.encodePacked( abi.encodePacked(hex"1600", action) // 1600 is the [version][actionId][actionBytes] - https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm/interacting-with-hypercore#action-encoding-details ); // Transfers tokens from the composer address on HyperCore to the _receiver ICoreWriter(HLP_CORE_WRITER).sendRawAction(payload); } if (amounts.dust > 0) { token.safeTransfer(_receiver, amounts.dust); } } ``` --- --- sidebar_label: Hyperliquid OFT Deployment Guide title: 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 - The final step to be executed after which the token will be listed on the spot order book. #### Step 2.6: Set deployer fee share | Action | Performed by | Actionable with | Required for | | ---------------------- | ----------------- | ----------------------------------------- | ------------ | | Set Deployer Fee Share | CoreSpot Deployer | `npx @layerzerolabs/hyperliquid-composer` | `HyperCore` | | Blocked by | OFT Deployer | Step 0 | Step 6 | | Blocked by | CoreSpot Deployer | Step 2.1 | None | - [ ] Follow the instructions in [Step 2.6: Set Deployer Trading Fee Share](#step-26-set-deployer-trading-fee-share-setdeployertradingfeeshare) - [ ] Trading fee share is usually 100% (default value) - this allocates the trading fees to the token deployer instead of burning it. - [ ] Do not lose or burn your deployer address as it collects tokens. - [ ] Step can be re-run as long as the new fee% is lower than the current one. - Even though the default value is 100%, it is recommended that you set it - This can be run even after deployment and linking ### Step 3: Connect the HyperCoreSpot to HyperEVM OFT #### Step 3.1: Create a request to connect the HyperCoreSpot to HyperEVM OFT | Action | Performed by | Actionable with | Required for | | -------------- | ----------------- | ----------------------------------------- | ------------ | | Create Request | CoreSpot Deployer | `npx @layerzerolabs/hyperliquid-composer` | `HyperCore` | | Blocked by | CoreSpot Deployer | Step 0, Step 2 | Step 3.2 | - [ ] Follow the instructions in [Step 3.1: Request EVM Contract Link](#step-31-request-evm-contract-link-core--evm-intention) - [ ] Make sure the core spot deployer has the OFT address. #### Step 3.2: Accept the request to connect the HyperCoreSpot to HyperEVM OFT | Action | Performed by | Actionable with | Required for | | -------------- | ----------------- | ----------------------------------------- | ------------ | | Accept Request | OFT Deployer | `npx @layerzerolabs/hyperliquid-composer` | `HyperCore` | | Blocked by | CoreSpot Deployer | Step 3.1 | Step 4 | - [ ] Follow the instructions in [Step 3.2: Finalize EVM Contract Link](#step-32-finalize-evm-contract-link-evm--core-confirmation) - [ ] Create a deployment file for the core spot before linking. ### Step 4: Deploy the Composer | Action | Performed by | Actionable with | Required for | | --------------- | ----------------- | ----------------------------------------- | ------------ | | Deploy Composer | OFT Deployer | `npx @layerzerolabs/hyperliquid-composer` | `HyperCore` | | Blocked by | CoreSpot Deployer | Step 3 | None | - [ ] Follow the instructions in [Step 4: Deploy the HyperLiquidComposer Contract](#step-4-deploy-the-hyperliquidcomposer-contract) - Deployer script in the OFT repo will deploy the composer - it also handles block switching. - [ ] Make sure the Composer's address is activated on HyperCore (sending it at least $1 worth of `HYPE` or `USDC`). - Composer is re-deployable and independent of the OFT and does not need to be linked with anything. ### Step 5: Listing on spot order books | Action | Performed by | Actionable with | Required for | | ----------------- | ----------------- | ----------------------------------------- | ------------ | | Spot Book Listing | Automatic | `npx @layerzerolabs/hyperliquid-composer` | HyperCore | | Blocked by | CoreSpot Deployer | Step 2 | none | This is automatically completed when all steps in Step 2 are completed. ### Step 6: Listing on perp order books | Action | Performed by | Actionable with | Required for | | ----------------- | ----------------- | ----------------------------------------- | ------------ | | Perp Book Listing | Automatic | `npx @layerzerolabs/hyperliquid-composer` | HyperCore | | Blocked by | CoreSpot Deployer | Step 2 | none | This is controlled by the Hyperliquid community [(source)](https://hyperliquid.gitbook.io/hyperliquid-docs/trading/perpetual-assets): > Hyperliquid currently supports trading of 100+ assets. Assets are added according to community input. ## Full Hyperliquid OFT Deployment Guide ### Step 0: Deploy Your OFT on HyperEVM You have two main paths depending on your project setup: starting fresh or using an existing OFT project. #### **Path 1: New Project using LayerZero Hyperliquid Example** This path is recommended if you are starting fresh and intend to deploy to HyperCore. 1. **Create a new Hyperliquid example repository:** ```bash 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 `CoreWriter` 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 title: 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] ``` --- --- id: overview title: LayerZero Tools Overview slug: /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). --- --- 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: Download LLM Files slug: /ai-resources sidebar_label: AI Resources --- 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. --- --- sidebar_label: Integration Checklist title: Integration Checklist description: Comprehensive list of checks and recommendations for production OApp integrations. --- The checklist below is designed to help prepare a project that integrates LayerZero V2 [OApps](../concepts/glossary.md#oapp-omnichain-application) for an external audit or Mainnet deployment. ## 0. Introduction ### Pathway Model & Mental Map A LayerZero application operates over directional pathways: **Path A → B**: 1. **[Source Chain](../concepts/glossary.md#source-chain) (Chain A)**: `OApp(A)` calls `EndpointV2(A)` → constructs & dispatches [packet](../concepts/glossary.md#packet). 2. **[Destination Chain](../concepts/glossary.md#destination-chain) (Chain B)**: `EndpointV2(B)` verifies, inserts [packet](../concepts/glossary.md#packet) into [channel](../concepts/glossary.md#channel-lossless-channel), and calls `OApp(B).[lzReceive](../concepts/glossary.md#lzreceive)`. **Important**: A → B configuration must be checked separately from B → A. Pathways are **directional**. ### Critical Pathway Checks Use **[EndpointV2](../concepts/glossary.md#endpoint)** and **OApp** methods as documented. #### On Chain A (Source) — EndpointV2(A) 1. **Send Library in Use** `getSendLibrary(oApp, dstEid)` → confirms which send library is active. 2. **[Executor](../concepts/glossary.md#executor) & [DVN](../concepts/glossary.md#dvn-decentralized-verifier-network) Configuration (Send‑Side)** `getConfig(oApp, sendLib, dstEid, configType)` 1. `configType = 1`: Executor config (max message size, executor address). 2. `configType = 2`: [ULN](../concepts/glossary.md#uln-ultra-light-node)/DVN config (confirmations, required/optional DVNs). 3. **[Delegate](../concepts/glossary.md#delegate) Check** `delegates(oApp)` → verifies the delegate authorized to configure endpoint settings. #### On Chain B (Destination) — EndpointV2(B) 1. **Receive Library in Use** `getReceiveLibrary(oApp, srcEid)` → confirms which receive library is expected. 2. **DVN Configuration (Receive‑Side)** `getConfig(oApp, recvLib, srcEid, 2)` → ULN config (confirmations + DVN sets). 3. **Initialization Gate** `initializable(origin, receiver)` → Endpoint check if path can be initialized. Falls back to OApp’s allowInitializePath if no lazyNonce is present. 4. **Optional Diagnostic Checks** `verifiable(origin, receiver)` or `inboundPayloadHash(...)` for debugging message states. #### On OApp Contracts (Both Chains) 1. **Peer Mapping** `peers(eid)` → verifies that each OApp is correctly mapped to its counterpart on the remote chain. 2. **Initialization Override** `allowInitializePath(origin)` → ensures the OAppReceiver provides a default implementation. If using `ILayerZeroReceiver` directly, you must implement this method to control initialization permissions. ### Defaults in LayerZero Protocol LayerZero maintains **default configurations** at the Endpoint level. These serve as **fallbacks** if an OApp has not explicitly called `setSendLibrary`, `setReceiveLibrary`, or `setConfig`. 1. A default configuration may: 1. Be a working config (with active DVNs \+ Executor). 2. Be a **dead config** (e.g., DVNs not listening → hard revert on send). 3. Be **misconfigured** (Executor not set or not connected, even if pathway appears live). 2. **Review Implication:** 1. Do not assume defaults are safe for production. 2. Always check explicitly: `getSendLibrary`, `getReceiveLibrary`, and `getConfig`. If these resolve to defaults, confirm whether the defaults are valid for the intended pathway. 3. Unintentional fallbacks to defaults are a common cause of blocked or failing pathways. ## 1. OApp Implementation ### Use the Latest Version of LayerZero Packages Always use the latest version of LayerZero packages. Avoid copying contracts directly from LayerZero repositories. You can find the latest packages on each contract's home page. ### Avoid Hardcoding LayerZero Endpoint IDs Use admin-restricted setters to configure [endpoint IDs](../concepts/glossary.md#endpoint-id) instead of hardcoding them. ### Set Peers on Every Pathway To ensure successful one-way messages between chains, it's essential to establish peer configurations on both the source and destination chains. Both chains' OApps perform peer verification before executing the message on the destination chain, ensuring secure and reliable 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 Libraries on Every Pathway It is recommended that OApps explicitly set the intended libraries. ```solidity EndpointV2.setSendLibrary(aOApp, bEid, newLib) EndpointV2.setReceiveLibrary(aOApp, bEid, newLib, gracePeriod) EndpointV2.setReceiveLibraryTimeout(aOApp, bEid, lib, gracePeriod) ``` :::caution If libraries are not set, the OApp will fallback to the default libraries 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(); } } ``` ::: ### Set Security and Executor Configurations on Every Pathway You must configure Decentralized Validator Networks (DVNs) manually on all chain pathways for your OApp. LayerZero maintains a neutral stance and does not presuppose any security assumptions on behalf of deployed OApps. This approach requires you to define and implement security considerations that align with your application’s requirements. ```solidity EndpointV2.setConfig(aOApp, sendLibrary, sendConfig) EndpointV2.setConfig(aOApp, receiveLibrary, receiveConfig) ``` Follow the Protocol Configuration documentation to configure DVNs for each chain pathway. - [EVM](../developers/evm/configuration/dvn-executor-config.md) - [Solana](../developers/solana/configuration/dvn-executor-config.md) - [Aptos Move](../developers/aptos-move/configuration/dvn-executor-config.md) :::caution If no configuration is set, the OApp will fallback to the default settings set by LayerZero Labs. ```solidity // @dev get the executor config and if not set, return the default config function getExecutorConfig(address _oapp, uint32 _remoteEid) public view returns (ExecutorConfig memory rtnConfig) { ExecutorConfig storage defaultConfig = executorConfigs[DEFAULT_CONFIG][_remoteEid]; ExecutorConfig storage customConfig = executorConfigs[_oapp][_remoteEid]; uint32 maxMessageSize = customConfig.maxMessageSize; rtnConfig.maxMessageSize = maxMessageSize != 0 ? maxMessageSize : defaultConfig.maxMessageSize; address executor = customConfig.executor; rtnConfig.executor = executor != address(0x0) ? executor : defaultConfig.executor; } ``` ::: Additional considerations: 1. Using just 1 DVN for each pathway should be avoided. 2. DVNs must match on every side of the pathway. Mismatching DVN configurations may still render operational OApps if the receive configuration on the remote OApp is less strict than the send configuration on the local OApp. Nonetheless, having fully matching configurations on either side is highly encouraged. 3. DVNs and Executor must implement their respective interfaces. Configured addresses can be checked against [V2 Contracts](../deployments/deployed-contracts.md) and [DVN Providers](../deployments/dvn-addresses.md). ### Set Delegate on Every OApp It is recommended that OApps review and explicitly set the delegate for each deployment. ```solidity EndpointV2.setDelegate(delegate) ``` ### Check Initialization Logic is Valid on Every OApp Ensure that `EndpointV2` can initialize the OApp on every 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); } function initializable(Origin calldata _origin, address _receiver) external view returns (bool) { return _initializable(_origin, _receiver, lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender]); } ``` ## 2. Custom Business Logic via LayerZero Interfaces ### Check Message Safety Ensure that either one action is executed per cross-chain message, OR that bundled actions cannot fail mid-sequence. Enforcing action per message is recommended. Consider **Instant Finality Guarantee (IFG)** if state safety is critical. ### Check Mock and Test Functions Are Removed When example contracts are used as boilerplates, ensure that both any mock or test function existing or added is removed in the production deployments. ### Check Enforced Gas and Value Destination gas and value consumption should be profiled and enforced for each OApp unless it's unpredictable. 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 source have sufficient gas to be executed on the destination. [Profile the gas required](../concepts/protocol/transaction-pricing.md#gas-profiling-considerations) 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); ``` See more on Solana [OFT Message Execution Options](../developers/solana/oft/overview.md#message-execution-options). ### EVM-Specific #### Check `_lzReceive` Security 1. If using `OAppReceiver` (inherited by `OApp` and `OFT`), `msg.sender != endpoint` and `_origin.srcEid != expectedOApp` checks are already enforced in [`OAppReceiver.lzReceive`](../concepts/glossary.md#lzreceive) (endpoint-only access, peer validation). 2. If implementing directly from `ILayerZeroReceiver`, you must implement these checks and initialization safeguards. #### Check `lzCompose` Security Unlike child contracts with the `OAppReceiver.lzReceive` method, the [`ILayerZeroComposer.lzCompose`](../concepts/glossary.md#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. ## 3. LayerZero OFT/ONFT Implementation ### Check Use-Case Contracts New tokens that are launched with native LayerZero messaging capabilities should use plain token implementations ([OFT](../concepts/glossary.md#oft-omnichain-fungible-token) or [ONFT](../concepts/glossary.md#onft-omnichain-non-fungible-token)) on every chain. Existing tokens in one or many chains with mint and burn capabilities should use a mint-and-burn Adapter such as `MintAndBurnOFTAdapter` in every existing chain, and plain token implementations in new chains. Existing tokens in one chain without mint and burn capabilities should use a lockbox [Adapter](../concepts/glossary.md#oft-adapter) such as `OFTAdapter` or `ONFT721Adapter` in the existing chain, and plain token implementations in new chains. Native tokens should use a native lockbox Adapter such as `NativeOFTAdapter`. For example, ETH in Ethereum or BNB in Binance Smart Chain. :::warning **There can only be one lockbox OFT Adapter used in an OFT deployment.** Multiple OFT Adapters break omnichain unified liquidity by effectively creating token pools. If you create OFT Adapters on multiple chains, you have no way to guarantee finality for token transfers due to the fact that the source chain has no knowledge of the destination pool's supply (or lack of supply). This can create race conditions where if a sent amount exceeds the available supply on the destination chain, those sent tokens will be permanently lost. ::: ### Check Shared Decimals [Shared Decimals](../concepts/glossary.md#shared-decimals) must be consistent across all OFT deployments, or amount conversion will vary by orders of magnitude and allow double spending. ### Check Minter and Burner Permissions When using mint-and-burn Adapters such as `MintAndBurnOFTAdapter`, ensure that the Adapter has the required roles to mint and burn the underlying token through the specified interface. ### Check Structured Codecs Use type-safe bytes codec for message encoding. Use custom codecs only if necessary and if your app requires deep optimization. Examples: - [EVM OFT](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/oapp/contracts/oft/libs/OFTMsgCodec.sol). - [Solana OFT](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/solana/programs/programs/oft/src/msg_codec.rs). ### Solana-Specific #### Avoid Enforcing Options Value to Initialize Accounts :::caution OFT sends to Solana to uninitialized token accounts **require additional options value** to [pay for ATA creation](../developers/solana/oft/overview.md#setting-options-inbound-to-solana). The first transfer of a specific token to a recipient will require value, but any subsequent transaction will not. **Static enforced options value should be avoided** to deal with it, as it'd keep overpaying after the first send. Nonetheless, enforcing options for regular gas consumption and other value requirements is still recommended in Solana. ::: Examples: - First OFT send [transaction](https://testnet.layerzeroscan.com/tx/4jatt3yWnyzYcdatJkMziKvw5seJkFFobjVKfJ1Qv5pbSgpLHHdeoeGSCMec6WcUruS5D5tBfNwiuymWRDapGweY) to a Solana recipient. Note that the value received is non-zero, as it is used to pay for ATA creation of the token recipient. - Second OFT send [transaction](https://testnet.layerzeroscan.com/tx/2jtLoZPXBDAYhwJYVWMp7THjeTT2EVvuy8LnxyhLcKJ8VGgdgwiD2cpheJG2bzdStk76Y8H8wCUq13Ho1t87WY3p) to Solana recipient. Note that the SOL value sent is zero, as ATA is already created for the token recipient. ## 4. Authority & Ownership Transfers ### Check OApp Ownership Ensure the OApp owner is set or transferred to the intended address. Check [Solana reference](../developers/solana/technical-reference/solana-guidance.md#transferring-oft-ownership-on-solana). ### Check OApp Delegate Ensure the OApp delegate at the EndpointV2 is set or transferred to the intended address. It must be transferred before transferring ownership, as only the OApp owner can set the delegate. ### Check Upgradeable Contracts Admin Ensure proxy admin for upgradeable contracts or upgrade authority is set or transferred to the intended addresses. ### EVM-Specific #### Check Upgradeable Contracts Implementation Initialization Ensure implementation contracts for EVM upgradeable contracts disable initializers in the constructor. ```solidity contract MyOFTUpgradeable is OFTUpgradeable { constructor(address _lzEndpoint) OFTUpgradeable(_lzEndpoint) { _disableInitializers(); } function initialize(string memory _name, string memory _symbol, address _delegate) public initializer { __OFT_init(_name, _symbol, _delegate); __Ownable_init(_delegate); } } ``` ## Usage Notes - This checklist is **production-focused**: it ensures pathway correctness, contract readiness, and monitoring preparedness. - It is **not a substitute for an audit**, but provides: - A systematic way to review OApp state. - Clear visibility into configuration consistency across chains. - Guidance on what Scan or external dashboards should surface automatically. - OFT/ONFT checks are categorized separately to avoid conflating with protocol-level messaging. ### References - [EVM Interactive Contract Playground](../developers/evm/contracts-playground) - [Production Deployment Checklist (Upgradeable OFT Example)](https://github.com/LayerZero-Labs/devtools/tree/main/examples/oft-upgradeable#production-deployment-checklist) --- --- title: LayerZero Simple Config Generator sidebar_label: Simple Config Generator description: Learn how to use the LayerZero Simple Config Generator for streamlined OApp configuration --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; The LayerZero Simple Config Generator makes use of the `@layerzerolabs/metadata-tools` package to provide a streamlined approach to configuring your OApp connections. It allows for a more simplified LayerZero config file. :::note **Current Support**: The Simple Config Generator currently supports EVM chains and Solana. Aptos support is not yet available. ::: 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, }, ]; 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, }, ]; const SOLANA_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 200000, value: 2039280, // SPL token account rent value in lamports }, ]; export default async function () { // note: pathways declared here are automatically bidirectional // if you declare A,B there's no need to declare B,A const connections = await generateConnectionsConfig([ [ avalancheContract, // Chain A contract solanaContract, // Chain B contract [['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [1, 1], // [A to B confirmations, B to A confirmations] [SOLANA_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions ], ]); return { contracts: [{contract: avalancheContract}, {contract: solanaContract}], connections, }; } ``` - Note that only the Solana contract object requires `address` to be specified. Do not specify `address` for non-Solana contract objects. - The above examples contain a minimal mesh with only one pathway (two chains) for demonstration purposes. You are able to add as many pathways as you need into the `connections` param, via `generateConnectionsConfig`. 3. If your pathways include Solana, run the Solana init config command: ``` npx hardhat lz:oft:solana:init-config --oapp-config layerzero.config.ts ``` 4. Run the wire command: ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` The wire command processes all the transactions required to connect all pathways specified in the LZ Config file. You need to only run this once regardless of how many pathways there are. If you change anything in the LZ Config file, then it should be run again. ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` ## Key Features - **Automatic Bidirectional Connections**: Define one pathway, get both directions automatically - **Built-in Best Practices**: Uses recommended DVN and executor configurations - **Cross-VM Compatibility**: Works seamlessly with EVM chains and Solana - **Reduced Complexity**: Fewer configuration parameters to manage - **Less Error-Prone**: Automated configuration generation reduces manual errors ## Configuration Parameters ### Pathway Definition ```typescript [ contractA, // Source chain contract contractB, // Destination chain contract [['LayerZero Labs'], []], // DVN configuration [1, 1], // Confirmations for each direction [optionsA, optionsB], // Enforced options for each direction ]; ``` ### DVN Configuration ```typescript [['LayerZero Labs'], []]; // [ requiredDVN[], [ optionalDVN[], threshold ] ] ``` - **Required DVNs**: Must verify the message for it to be considered valid - **Optional DVNs**: Additional verifiers (with threshold) for enhanced security ### Enforced Options ```typescript const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, // Message type (1 = OFT, 2 = OApp) optionType: ExecutorOptionType.LZ_RECEIVE, // Option type gas: 80000, // Gas limit for destination execution value: 0, // Value to send (usually 0) }, ]; ``` ## VM-Specific Considerations ### EVM Chains - Use `contractName` for contract identification - Gas values represent actual gas units - Value is typically 0 ### Solana - Use `address` for contract identification (required) - Gas values represent compute units - Value represents lamports (typically 2039280 for SPL token account rent) :::note **Custom Metadata**: For advanced use cases, you can provide custom metadata by passing a `fetchMetadata` function to `generateConnectionsConfig`. This allows you to extend the default metadata with custom DVNs and executors. ::: ## Next Steps - **Migrate from Manual Config**: See the [Migrate to Simple Config](/v2/tools/migrate-to-simple-config) guide - **Production Deployment**: Review and adjust settings for production environments - **Gas Optimization**: Profile your contracts to set optimal gas limits - **Custom DVNs**: Consider adding custom DVNs for enhanced security --- --- title: Migrate to Simple Config sidebar_label: Migrate to Simple Config description: Learn how to migrate from manual LayerZero configuration to the Simple Config Generator --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; The LayerZero Simple Config Generator provides a streamlined approach to configuring your OApp connections across supported VMs (EVM, Solana, etc.). This guide will help you migrate from manual configuration to the Simple Config approach. :::note **Current Support**: The Simple Config Generator currently supports EVM chains and Solana. Aptos support is not yet available. ::: ## Why Migrate to Simple Config? The Simple Config Generator offers several advantages over manual configuration: - **Reduced complexity**: Fewer configuration parameters to manage - **Automatic bidirectional connections**: Define one pathway, get both directions - **Built-in best practices**: Uses recommended DVN and executor configurations - **Cross-VM compatibility**: Works seamlessly with EVM chains, Solana, and other supported VMs - **Less error-prone**: Automated configuration generation reduces manual errors - **VM-agnostic**: Same configuration approach works across different virtual machines ## Before Migration: Manual Configuration In the traditional manual approach, you would need to: 1. Define each connection direction separately 2. Manually specify send and receive libraries 3. Configure ULN settings for each direction 4. Set up executor configurations 5. Define enforced options for each pathway 6. Handle VM-specific configurations separately Here's an example of manual configuration for EVM chains: ```typescript connections: [ // ETH <--> ARB PATHWAY: START { from: ethereumContract, to: arbitrumContract, }, { from: arbitrumContract, to: ethereumContract, }, // ETH <--> ARB PATHWAY: END ]; // Then define config settings for each direction connections: [ { from: ethereumContract, to: arbitrumContract, config: { sendLibrary: contractsConfig.ethereum.sendLib302, receiveLibraryConfig: { receiveLibrary: contractsConfig.ethereum.receiveLib302, gracePeriod: BigInt(0), }, sendConfig: { executorConfig: { maxMessageSize: 10000, executor: contractsConfig.ethereum.executor, }, ulnConfig: { confirmations: BigInt(15), requiredDVNs: [ contractsConfig.ethereum.horizenDVN, contractsConfig.ethereum.polyhedraDVN, contractsConfig.ethereum.lzDVN, ], optionalDVNs: [], optionalDVNThreshold: 0, }, }, receiveConfig: { ulnConfig: { confirmations: BigInt(20), requiredDVNs: [ contractsConfig.ethereum.lzDVN, contractsConfig.ethereum.horizenDVN, contractsConfig.ethereum.polyhedraDVN, ], optionalDVNs: [], optionalDVNThreshold: 0, }, }, enforcedOptions: [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 65000, value: 0, }, { msgType: 2, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 65000, value: 0, }, { msgType: 2, optionType: ExecutorOptionType.COMPOSE, index: 0, gas: 50000, value: 0, }, ], }, }, // Repeat for the reverse direction... ]; ``` ## After Migration: Simple Config With the Simple Config Generator, the same configuration becomes much simpler and works across all VMs: ```typescript import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {OAppEnforcedOption, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const ethereumContract: OmniPointHardhat = { eid: EndpointId.ETHEREUM_V2_MAINNET, contractName: 'MyOFT', }; const arbitrumContract: OmniPointHardhat = { eid: EndpointId.ARBITRUM_V2_MAINNET, contractName: 'MyOFT', }; const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 65000, value: 0, }, ]; export default async function () { const connections = await generateConnectionsConfig([ [ ethereumContract, // Chain A contract arbitrumContract, // Chain B contract [['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [15, 20], // [A to B confirmations, B to A confirmations] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain A enforcedOptions, Chain B enforcedOptions ], ]); return { contracts: [{contract: ethereumContract}, {contract: arbitrumContract}], connections, }; } ``` ## Migration Steps ### 1. Install Required Dependencies ```bash pnpm add -D @layerzerolabs/metadata-tools ``` ### 2. Update Your Configuration File Replace your manual `layerzero.config.ts` with the Simple Config approach: ```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'; // Define your contracts const contractA: OmniPointHardhat = { eid: EndpointId.CHAIN_A_ENDPOINT_ID, contractName: 'YourContract', }; const contractB: OmniPointHardhat = { eid: EndpointId.CHAIN_B_ENDPOINT_ID, contractName: 'YourContract', }; // Define enforced options (gas settings for destination chain execution) const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 80000, value: 0, }, ]; export default async function () { const connections = await generateConnectionsConfig([ [ contractA, // Chain A contract contractB, // Chain B contract [['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [1, 1], // [A to B confirmations, B to A confirmations] [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain A enforcedOptions, Chain B enforcedOptions ], ]); return { contracts: [{contract: contractA}, {contract: contractB}], connections, }; } ``` For EVM + Solana configurations: ```typescript import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities'; import {OAppEnforcedOption, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat'; import {EndpointId} from '@layerzerolabs/lz-definitions'; import {generateConnectionsConfig} from '@layerzerolabs/metadata-tools'; const evmContract: OmniPointHardhat = { eid: EndpointId.ETHEREUM_V2_MAINNET, contractName: 'MyOFT', }; const solanaContract: OmniPointHardhat = { eid: EndpointId.SOLANA_V2_MAINNET, address: 'YourSolanaAddress', // Required for Solana contracts }; const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 80000, value: 0, }, ]; const SOLANA_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, optionType: ExecutorOptionType.LZ_RECEIVE, gas: 200000, value: 2039280, // SPL token account rent value in lamports }, ]; export default async function () { const connections = await generateConnectionsConfig([ [ evmContract, // EVM contract solanaContract, // Solana contract [['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] [1, 1], // [EVM to Solana confirmations, Solana to EVM confirmations] [SOLANA_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Solana enforcedOptions, EVM enforcedOptions ], ]); return { contracts: [{contract: evmContract}, {contract: solanaContract}], connections, }; } ``` ### 3. Update Your Deployment Scripts Replace your manual wire commands with the Simple Config approach: ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` ```bash # Initialize Solana config (first time only) npx hardhat lz:oft:solana:init-config --oapp-config layerzero.config.ts # Wire all connections npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` ## Key Differences | Aspect | Manual Config | Simple Config | | ------------------------- | -------------------------------- | ------------------------------ | | **Connection Definition** | Define each direction separately | Define once, get bidirectional | | **DVN Configuration** | Manual specification | Automated with metadata | | **Executor Setup** | Manual configuration | Automated with metadata | | **Library Selection** | Manual specification | Automated with metadata | | **VM-Specific Handling** | Separate configs per VM | Unified approach | | **Configuration Length** | 100+ lines per pathway | ~20 lines per pathway | | **Error Prone** | High (manual configuration) | Low (automated generation) | ## Configuration Parameters Explained :::note **Custom Metadata**: For advanced use cases, you can provide custom metadata by passing a `fetchMetadata` function to `generateConnectionsConfig`. This allows you to extend the default metadata with custom DVNs and executors. ::: ### Pathway Definition ```typescript [ contractA, // Source chain contract contractB, // Destination chain contract [['LayerZero Labs'], []], // DVN configuration [15, 20], // Confirmations for each direction [optionsA, optionsB], // Enforced options for each direction ]; ``` ### DVN Configuration ```typescript [['LayerZero Labs'], []]; // [ requiredDVN[], [ optionalDVN[], threshold ] ] ``` - **Required DVNs**: Must verify the message for it to be considered valid - **Optional DVNs**: Additional verifiers (with threshold) for enhanced security ### Confirmations ```typescript [15, 20]; // [A to B confirmations, B to A confirmations] ``` The number of block confirmations to wait before considering a message verified. ### Enforced Options ```typescript const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ { msgType: 1, // Message type (1 = OFT, 2 = OApp) optionType: ExecutorOptionType.LZ_RECEIVE, // Option type gas: 80000, // Gas limit for destination execution value: 0, // Value to send (usually 0) }, ]; ``` ## VM-Specific Considerations ### EVM Chains - Use `contractName` for contract identification - Gas values represent actual gas units - Value is typically 0 ### Solana - Use `address` for contract identification (required) - Gas values represent compute units - Value represents lamports (typically 2039280 for SPL token account rent) :::note **Aptos Support**: Aptos is not yet supported by the Simple Config Generator. Use manual configuration for Aptos integrations. ::: ## Migration Checklist - [ ] Install `@layerzerolabs/metadata-tools` - [ ] Update `layerzero.config.ts` to use Simple Config format - [ ] Define your contract objects with correct EIDs - [ ] Configure enforced options for your use case - [ ] Set appropriate DVN requirements - [ ] Handle VM-specific requirements (`address` for Solana contract objects) - [ ] Test by running `npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts` (if there were no config changes, there should be no transactions to be submitted) ## Troubleshooting ### Common Issues 1. **Missing Dependencies**: Ensure `@layerzerolabs/metadata-tools` is installed 2. **Incorrect EndpointId**: Verify you are using the correct Endpoint ID (if using Endpoint V2, the constant name should include `V2`) 3. **VM-Specific Requirements**: Remember to specify `address` for Solana contracts and `value` for enforced options `msgType` 1 when sending to Solana. 4. **Gas Estimation**: Profile your contracts to set appropriate gas limits ### Getting Help If you encounter issues during migration: 1. Check the [Simple Config documentation](/v2/tools/simple-config) 2. Review the [examples in devtools](https://github.com/LayerZero-Labs/devtools/tree/main/examples) 3. Consult VM-specific documentation. The Simple Config Generator significantly reduces the complexity of LayerZero configuration while maintaining the same functionality and security guarantees as manual configuration. --- --- title: create-lz-oapp CLI guide sidebar_label: CLI Guide --- # create-lz-oapp CLI Guide ## Introduction The `create-lz-oapp` CLI is a powerful command-line tool designed to streamline the development of LayerZero Omnichain Applications (OApps). This CLI toolkit simplifies the process of building, testing, deploying, and configuring omnichain applications by providing a structured project template and essential development tools. With `create-lz-oapp`, you can quickly bootstrap a new LayerZero project with both Hardhat and Foundry development frameworks pre-configured, along with example contracts, cross-chain unit tests, LayerZero configuration files, and deployment scripts. ## Installation & Usage Create a new LayerZero OApp project with a single command: ```bash npx create-lz-oapp@latest ``` This will launch an interactive project creation wizard that guides you through setting up your omnichain application. ## CLI Options The `create-lz-oapp` CLI supports the following options: ### Version Information ```bash -V, --version ``` Output the current version number of the CLI tool. ### CI Mode ```bash --ci ``` Run the CLI in CI (Continuous Integration) mode, which operates in non-interactive mode. This is useful for automated deployments and scripts where user interaction is not available. - **Default**: `false` ### Project Directory ```bash -d, --destination ``` Specify the target directory where the new project should be created. If not provided, the CLI will use the current directory or prompt for a location. ### Example Project Template ```bash -e, --example ``` Choose which example project template to use as the starting point for your application. #### Always Available Examples These examples are always available: | Example Name | Description | | ------------- | ----------------------------------------------------------- | | `oapp` | OApp: Basic Omnichain Application for cross-chain messaging | | `oft` | OFT: Omnichain Fungible Token implementation | | `oft-adapter` | OFTAdapter: Adapter for existing ERC20 tokens | | `onft721` | ONFT721: Omnichain Non-Fungible Token (ERC721) | #### Feature-Flagged Examples These examples are available only when specific environment variables are set: | Example Name | Description | Required Environment Variable | | ------------------------ | ---------------------------------- | -------------------------------------------- | | `lzapp-migration` | EndpointV1 Migration | `LZ_ENABLE_MIGRATION_EXAMPLE` | | `onft721-zksync` | ONFT721 zksolc | `LZ_ENABLE_ZKSOLC_EXAMPLE` | | `oft-upgradeable` | UpgradeableOFT | `LZ_ENABLE_UPGRADEABLE_EXAMPLE` | | `native-oft-adapter` | NativeOFTAdapter | `LZ_ENABLE_NATIVE_EXAMPLE` | | `oft-alt` | OFTAlt | `LZ_ENABLE_ALT_EXAMPLE` | | `mint-burn-oft-adapter` | MintBurnOFTAdapter | `LZ_ENABLE_MINTBURN_EXAMPLE` | | `oapp-read` | lzRead View/Pure Functions Example | `LZ_ENABLE_READ_EXAMPLE` | | `view-pure-read` | lzRead Public Variables Example | `LZ_ENABLE_READ_EXAMPLE` | | `uniswap-read` | lzRead UniswapV3 Quote | `LZ_ENABLE_READ_EXAMPLE` | | `oft-solana` | OFT (Solana) | `LZ_ENABLE_SOLANA_OFT_EXAMPLE` | | `oapp-solana` | OApp (Solana) | `LZ_ENABLE_SOLANA_OAPP_EXAMPLE` | | `oft-initia` | OFT (Initia) | `LZ_ENABLE_EXPERIMENTAL_INITIA_EXAMPLES` | | `oft-adapter-initia` | OFT Adapter (Initia) | `LZ_ENABLE_EXPERIMENTAL_INITIA_EXAMPLES` | | `oft-aptos-move` | OFT (Aptos Move) | `LZ_ENABLE_EXPERIMENTAL_MOVE_VM_EXAMPLES` | | `oft-adapter-aptos-move` | OFT Adapter (Aptos Move) | `LZ_ENABLE_EXPERIMENTAL_MOVE_VM_EXAMPLES` | | `oapp-aptos-move` | OApp (Aptos Move) | `LZ_ENABLE_EXPERIMENTAL_MOVE_VM_EXAMPLES` | | `oft-hyperliquid` | OFT + Composer (Hyperliquid) | `LZ_ENABLE_EXPERIMENTAL_HYPERLIQUID_EXAMPLE` | | `omni-call` | EVM OmniCall | `LZ_ENABLE_EXPERIMENTAL_OMNI_CALL_EXAMPLE` | ### Log Level ```bash --log-level ``` Set the verbosity level for CLI output and logging information. - **Available choices**: `"error"`, `"warn"`, `"info"`, `"http"`, `"verbose"`, `"debug"`, `"silly"` - **Default**: `"info"` ### Package Manager ```bash -p, --package-manager ``` Choose which Node.js package manager to use for dependency management in your project. - **Available choices**: `"npm"`, `"pnpm"`, `"bun"` ## Environment Variables for Feature-Flagged Examples To access feature-flagged examples, you can set the corresponding environment variables inline with the command. These examples provide access to experimental, specialized, or advanced features. ### Setting Environment Variables **On Unix/macOS/Linux:** ```bash LZ_ENABLE_READ_EXAMPLE=1 npx create-lz-oapp@latest -e oapp-read ``` **On Windows (Command Prompt):** ```cmd set "LZ_ENABLE_READ_EXAMPLE=1" && npx create-lz-oapp@latest -e oapp-read ``` **On Windows (PowerShell):** ```powershell $env:LZ_ENABLE_READ_EXAMPLE=1; npx create-lz-oapp@latest -e oapp-read ``` ## Example Usage ### Basic Interactive Setup ```bash npx create-lz-oapp@latest ``` ### Non-Interactive Setup with Options ```bash npx create-lz-oapp@latest --ci -d ./my-oapp-project -e oapp -p pnpm --log-level verbose ``` ### Create an OFT Project ```bash npx create-lz-oapp@latest -e oft -d ./my-oft-token -p pnpm ``` ### Create an ONFT721 Project ```bash npx create-lz-oapp@latest -e onft721 -d ./my-nft-project -p pnpm ``` ### Using Feature-Flagged Examples To use feature-flagged examples, include the required environment variable in the command: ```bash # Enable lzRead examples and create an lzRead project LZ_ENABLE_READ_EXAMPLE=1 npx create-lz-oapp@latest -e oapp-read -d ./my-read-project -p pnpm ``` ```bash # Enable Solana examples and create a Solana OFT project LZ_ENABLE_SOLANA_OFT_EXAMPLE=1 npx create-lz-oapp@latest -e oft-solana -d ./my-solana-oft -p pnpm ``` ```bash # Enable upgradeable examples in CI mode LZ_ENABLE_UPGRADEABLE_EXAMPLE=1 npx create-lz-oapp@latest --ci -e oft-upgradeable -d ./upgradeable-oft -p bun ``` ## Next Steps After creating your project with `create-lz-oapp`, you'll have a fully structured omnichain application ready for development. The generated project includes: - Example smart contracts implementing LayerZero standards - Deployment scripts and configuration The project structure will vary depending on the example template you selected, with each template providing the appropriate contracts, tests, and configuration for that specific use case (OApp, OFT, ONFT, etc.). Refer to the generated project's README file for specific instructions on how to configure, test, and deploy your omnichain application. --- --- title: Recreating Deployments sidebar_label: Recreating Deployments --- # Recreating Deployments This guide is intended for situations where you might have lost access to the original project code, or the deployment was made outside of a project initialized by [create-lz-oapp](./guide.md). It explains how to reconstruct the expected deployments layout so you can use the Hardhat helper tasks. ## Invariants Note the following when using a project created via `create-lz-oapp` or when using any of the hardhat helper tasks: - For EVM chains, the network name set in `hardhat.config.ts` must match the folder names under `/deployments` - For Solana, the deployment subfolder should either be `/solana-mainnet` or `/solana-testnet` (`solana-testnet` refers to Solana Devnet) ## Recreate Deployments For this example, we'll recreate deployments for an **OFT** that was deployed on **Arbitrum Sepolia** and **Solana Devnet**. Before following the steps, you need to at least have the address of the OFT on Arbitrum Sepolia, and the OFT Store address on Solana Devnet (not the Mint Address). 1. Create a new project using the [create-lz-oapp CLI](./guide.md). For an EVM-only OFT, you can choose `OFT` when prompted. For an OFT that is both on EVM and Solana, choose `OFT (Solana)`. 2. Create a `deployments` folder in the root of the repo. The eventual structure would look like this: ```text /deployments /arbitrum-sepolia .chainId MyOFT.json /solana-testnet OFT.json ``` > For the EVM chain deployments, if you previously deployed using Hardhat Deploy, simply copy over the contents of the deployments folder from your previous project. 3. Under `/deployments`, for the EVM chain (Arbitrum Sepolia in this example), create an `/arbitrum-sepolia` folder which contains: - `/arbitrum-sepolia/.chainId` - this file should contain the chain id for the network, and **not** the Endpoint ID. Chain IDs can be found in the table [here](/v2/deployments/deployed-contracts). - `/arbitrum-sepolia/MyOFT.json` - in here is where the OFT's address is set. The only key that is necessary in the JSON file are `address`. You can see a sample of that below, insert your OFT address into the `address` field. ```json { "address": "" } ``` 4. For Solana, create a `/solana-testnet` folder which contains: - `/solana-testnet/OFT.json` - in here are several addresses required by the Solana OFT. Below is an example of a full file with all the necessary keys: ```json { "programId": "", "mint": "", "mintAuthority": "", "escrow": "", "oftStore": "" } ``` > Given the Solana OFT Store address, you can run `npx hardhat lz:oft:store:debug --oft-store --eid ` to view debug information that contains the relevant Solana addresses. 5. Modify `hardhat.config.ts` and `layerzero.config.ts` accordingly. For how to configure `layerzero.config.ts`, refer to the [Simple Config Generator](/v2/tools/simple-config) page. 6. Run `npm run compile:hardhat` to ensure relevant artifacts that are required by Hardhat helper tasks involving the EVM OFT are generated. For example, the `send` helper task when sending from an EVM chain requires the `OFT` artifact's ABI. 7. Run `npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts` and it should suggest transactions for your recreated deployments. ```bash npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts ``` --- --- title: OFT API Overview sidebar_id: developers/api --- The OFT API is a simple way to discover LayerZero OFTs across all chains within the LayerZero [omnichain mesh](/v2/concepts/protocol/mesh-network). You can use the API for free to discover all available OFTs, or get an API key to generate transaction calldata for any network to call the send function on any of the OFTs with given parameters. :::info - **What is an OFT?** Learn about [**Omnichain Fungible Tokens**](/v2/concepts/applications/oft-standard) in our Core Concepts - **Want to build your own OFT?** Check out the [**EVM OFT Quickstart**](/v2/developers/evm/oft/quickstart) guide ::: ## List Route API An endpoint that provides structured metadata and availability information about supported OFTs across chains. #### Use for: - Viewing OFTs as well as their availability on different chains - Looking up contract addresses - Getting configuration details You can try out the List Route API [here](./oft-reference.mdx#get-/list). ## Direct Transfer API An endpoint that provides anyone with a simple, efficient way to offer native OFT transfers directly within their interfaces. **Requires an API key** Request an [API key](https://forms.monday.com/forms/c64c278b03d2b40a24e305943a743527?r=use1). #### Use for: - Completing direct debit and credit transfers of OFTs across supported chains You can explore the List Route API endpoints [here](./oft-reference.mdx#get-/transfer). :::info Terms of Use By using the OFT API, you agree to the [**OFT API Terms of Use**](./terms.md). ::: --- --- title: OFT API Reference --- import {CustomSwagger} from '../../../src/components/CustomSwagger'; import mainnet from '../../../src/data/swagger/oft-mainnet.json'; :::info Early Access This is an early version of our API and is not yet available for general release. List is open for use; Transfer requires an API key. If you're interested in gaining access or providing feedback, [reach out](https://forms.monday.com/forms/c64c278b03d2b40a24e305943a743527?r=use1) to the LayerZero team. ::: --- --- title: OFT API Usage Examples sidebar_label: OFT API Usage --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # LayerZero OFT Transfer API Usage Guide A developer guide to using the LayerZero OFT Transfer API to simply orchestrate OFT transfers between chains. ## Overview The LayerZero OFT Transfer API provides a simple way to fetch the calldata necessary to call the `send` implementation on any known OFT deployment. This guide demonstrates complete integration examples for both EVM and Solana chains, showing how the same API endpoints work across different blockchain environments. **Key Benefits:** - **Universal API**: Same endpoints work for EVM ↔ EVM, Solana ↔ EVM, and EVM ↔ Solana transfers - **Chain-agnostic discovery**: Find tokens across all supported chains with a single API call - **Pre-built transaction data**: Get ready-to-execute transaction data for any OFT transfer between chains - **Built-in validation and error handling** - **LayerZero transaction tracking** ## Cross-Chain Architecture The LayerZero OFT API abstracts the complexity of cross-chain transfers by providing the same interface regardless of source and destination chains. Whether you're transferring from Ethereum to Solana or Solana to BSC, you use the same API endpoints with chain-specific transaction execution. ## Prerequisites - Node.js 16+ and npm/yarn - LayerZero API key ([request here](https://forms.monday.com/forms/c64c278b03d2b40a24e305943a743527?r=use1)) - Basic knowledge of the [OFT standard](../../concepts/applications/oft-standard.md) ## Installation ```bash npm install ethers axios dotenv @layerzerolabs/lz-definitions ``` ```bash npm install @metaplex-foundation/umi-bundle-defaults @metaplex-foundation/umi @metaplex-foundation/umi-web3js-adapters @solana/web3.js axios bs58 dotenv @layerzerolabs/lz-definitions ``` ## Environment Setup Create a `.env` file in your project root: ```bash # Required for API access OFT_API_KEY=your-api-key-here # Required for executing transactions PRIVATE_KEY=your-private-key-here ``` Create a `.env` file in your project root: ```bash # Required for API access OFT_API_KEY=your-api-key-here # Required for executing transactions (base58 format) SOLANA_PRIVATE_KEY=your-solana-private-key-here # Optional: Custom RPC endpoint SOLANA_RPC_URL=https://api.mainnet-beta.solana.com ``` ## Understanding Chain Names The LayerZero OFT API uses **chain names** to identify different blockchain networks. These chain names are standardized across the LayerZero ecosystem and can be imported from the `@layerzerolabs/lz-definitions` package for type safety. ### Available Chain Names You can import chain names as constants to avoid typos and get TypeScript autocomplete: ```typescript import {Chain} from '@layerzerolabs/lz-definitions'; // Examples of available chains [Chain.ETHEREUM][Chain.BSC][Chain.ARBITRUM][Chain.ABSTRACT][Chain.BASE][Chain.OPTIMISM][ // "ethereum" // "bsc" // "arbitrum" // "abstract" // "base" // "optimism" Chain.POLYGON ]; // "polygon" [Chain.SOLANA]; // "solana" ``` ### Using Chain Names in API Calls Chain names are used directly in API requests without any conversion needed: ```typescript import {Chain} from '@layerzerolabs/lz-definitions'; // Use chain constants in API requests const response = await axios.get(`${API_BASE_URL}/list`, { params: {chainNames: `${Chain.SOLANA},${Chain.BSC}`}, }); ``` ## LayerZero OFT API Endpoints The OFT API provides two main endpoints for token operations: ### 1. Token Discovery API (`/list`) **Purpose**: Discover available OFT tokens across chains and get their canonical contract addresses. Use this to find where a token is deployed and whether it's an OFT. **Endpoint**: `GET https://metadata.layerzero-api.com/v1/metadata/experiment/ofts/list` **Parameters**: - `chainNames` (optional, string): Comma-separated list of chain names to search across - `symbols` (optional, string): Comma-separated list of token symbols to filter by **Common Usage Patterns**: ```typescript import {Chain} from '@layerzerolabs/lz-definitions'; // 1. Find all deployments of a specific token (recommended approach) const response = await axios.get(`${API_BASE_URL}/list`, { params: {symbols: 'PENGU'}, // Discovers PENGU on all available chains }); // 2. Search for tokens on specific chains const response = await axios.get(`${API_BASE_URL}/list`, { params: { chainNames: `${Chain.ABSTRACT},${Chain.BSC}`, symbols: 'PENGU,USDC', }, }); // 3. List all available OFTs (no filters) const response = await axios.get(`${API_BASE_URL}/list`); ``` **Response Structure**: ```json { "USDT0": [ { "name": "USDT0", "sharedDecimals": 6, "endpointVersion": "v2", "deployments": { "ethereum": { "address": "0x6c96de32cea08842dcc4058c14d3aaad7fa41dee", "localDecimals": 6, "type": "OFT_ADAPTER", "innerToken": "0xdac17f958d2ee523a2206206994597c13d831ec7", "approvalRequired": true }, "solana": { "address": "So11111111111111111111111111111111111111112", "localDecimals": 6, "type": "OFT", "approvalRequired": false } } } ] } ``` **Key Response Fields**: - `name`: Token display name - `sharedDecimals`: Number of decimals used across all chains for LayerZero transfers - `endpointVersion`: LayerZero endpoint version ("v2") - `deployments`: Object with chain names as keys, containing deployment details for each chain - `address`: The OFT contract address on that specific chain (Program ID for Solana) - `localDecimals`: Number of decimals the token uses on that specific chain - `type`: Contract type ("OFT_ADAPTER" for wrapped tokens, "OFT" for native OFTs) - `innerToken`: The underlying ERC20 token address (for OFT_ADAPTER types, not applicable to Solana) - `approvalRequired`: Whether token approval is required before transfers (always false for Solana) :::info Understanding Decimals For detailed explanations of `sharedDecimals` and `localDecimals` concepts, including the decimal conversion process and overflow considerations, see the [**OFT Technical Reference**](../../concepts/technical-reference/oft-reference.md#1-transferring-value-across-different-vms). ::: **Using the Response**: ```typescript const response = await axios.get(`${API_BASE_URL}/list`, { params: {symbols: 'PENGU'}, }); const tokenData = response.data['PENGU'][0]; const availableChains = Object.keys(tokenData.deployments); console.log(`PENGU available on: ${availableChains.join(', ')}`); // Get contract address for a specific chain const contractAddress = tokenData.deployments['abstract'].address; ``` ### 2. Transfer Transaction API (`/transfer`) **Purpose**: Generate pre-built transaction data for executing OFT transfers from a source to destination network. The API returns chain-specific transaction data that can be executed using the appropriate blockchain SDK. **Endpoint**: `GET https://metadata.layerzero-api.com/v1/metadata/experiment/ofts/transfer` **Authentication Required**: ```typescript headers: { 'x-layerzero-api-key': API_KEY } ``` **Parameters**: - `srcChainName` (string): Source chain name (e.g., "solana", "ethereum", "bsc") - `dstChainName` (string): Destination chain name (e.g., "ethereum", "bsc", "solana") - `srcAddress` (string): Source chain OFT contract address or Program ID - `amount` (string): Transfer amount in token's smallest unit - `from` (string): Sender wallet address (public key for Solana) - `to` (string): Recipient wallet address on destination chain - `validate` (boolean): Pre-validate balances and parameters - `options` (string, optional): Structured LayerZero execution options as JSON string **Complete Example Workflow**: ```typescript import {Chain} from '@layerzerolabs/lz-definitions'; // Step 1: Discover token deployments const listResponse = await axios.get(`${API_BASE_URL}/list`, { params: {symbols: 'PENGU'}, }); const tokenData = listResponse.data['PENGU'][0]; // Step 2: Choose your transfer route from available deployments const fromChain = Chain.ABSTRACT; const toChain = Chain.BSC; const contractAddress = tokenData.deployments[fromChain].address; // Step 3: Get transfer calldata const transferResponse = await axios.get(`${API_BASE_URL}/transfer`, { params: { srcChainName: fromChain, // "abstract" dstChainName: toChain, // "bsc" srcAddress: contractAddress, // Contract address from /list amount: '1000000000000000000', // 1 token (18 decimals) from: wallet.address, to: wallet.address, validate: true, }, headers: {'x-layerzero-api-key': API_KEY}, }); ``` **Response Structure**: ```json { "transactionData": { "populatedTransaction": { "to": "0x...", "data": "0x...", "value": "0x...", "gasLimit": "0x..." }, "approvalTransaction": { "to": "0x...", "data": "0x...", "gasLimit": "0x..." } } } ``` ```json { "transactionData": { "populatedTransaction": "base64-encoded-versioned-transaction" } } ``` **Key Response Fields**: - `populatedTransaction`: The main transfer transaction ready to be sent via `wallet.sendTransaction()` - `approvalTransaction`: Token approval transaction (if required for OFT adapters) - Both transactions contain pre-built calldata and gas estimates **Executing the Transactions**: ```typescript const {transactionData} = transferResponse.data; // Step 4: Configure RPC for the source chain to execute transactions const RPC_URLS = { [Chain.ABSTRACT]: 'https://api.mainnet.abs.xyz', [Chain.BSC]: 'https://bsc.drpc.org', // Add other chains as needed }; const provider = new ethers.providers.JsonRpcProvider(RPC_URLS[fromChain]); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider); // Step 5: Execute approval if needed (for OFT adapters) if (transactionData.approvalTransaction) { const approvalTx = await wallet.sendTransaction(transactionData.approvalTransaction); await approvalTx.wait(); } // Step 6: Execute the transfer const transferTx = await wallet.sendTransaction(transactionData.populatedTransaction); await transferTx.wait(); ``` ### (Optional) Add extraOptions For advanced use cases, you can include LayerZero execution options to extend the base OFT functionality. The `options` parameter allows you to specify additional gas limits, native token drops, and compose message settings. **Example with extraOptions**: ```typescript const transferResponse = await axios.get(`${API_BASE_URL}/transfer`, { params: { srcChainName: fromChain, dstChainName: toChain, srcAddress: contractAddress, amount: '1000000000000000000', from: wallet.address, to: wallet.address, validate: true, // Optional: Add extra execution options options: JSON.stringify({ executor: { lzReceive: { gasLimit: 300000, // Extra gas for complex lzReceive logic }, nativeDrops: [ { amount: '1000000000000000', // 0.001 ETH in wei receiver: '0xd8538fa8fdd5872e68c4040449f64452ae536fa6', }, ], }, }), }, headers: {'x-layerzero-api-key': API_KEY}, }); ``` **How extraOptions work**: - **`lzReceive` gas limit**: The gas you specify here is **added to** the base gas limit already set by the OFT deployer. For example, if the OFT enforces 65,000 gas and you add 35,000, the total execution will have 100,000 gas available. - **`nativeDrops`**: Allows you to send native chain currency (ETH, MATIC, BNB, etc.) to any receiver wallet address alongside your token transfer. The amount is specified in wei and sent directly to the specified receiver address. - **`composeOptions`**: Used specifically for [omnichain composers](../../concepts/applications/composer-standard.md) when your OFT transfer triggers additional smart contract logic on the destination chain. See the [EVM Composer Overview](../../developers/evm/composer/overview.md) for implementation details. - For detailed information about LayerZero message options, see [Message Options](../../concepts/message-options.md) and [Message Execution Options](../sdks/options.md). ### Chain Name Reference Here are common chain names available in the `@layerzerolabs/lz-definitions` package: | Chain Constant | Chain Name | Network | | ---------------- | ---------- | ---------------- | | `Chain.ETHEREUM` | `ethereum` | Ethereum Mainnet | | `Chain.BSC` | `bsc` | BNB Smart Chain | | `Chain.ARBITRUM` | `arbitrum` | Arbitrum One | | `Chain.OPTIMISM` | `optimism` | Optimism | | `Chain.BASE` | `base` | Base | | `Chain.POLYGON` | `polygon` | Polygon | | `Chain.ABSTRACT` | `abstract` | Abstract | **Usage Tips**: - Import `Chain` constants to avoid typos and get autocomplete - Use `/list` API without chain filters to discover all supported chains - If you see a chain missing, make sure your `@layerzerolabs/lz-definitions` package is updated to the latest version - Check the [LayerZero API reference](https://docs.layerzero.network/v2/developers/evm/api/oft-api) for the complete list of supported chains ## Complete Transfer Examples ### Example: Send $PENGU from BSC to Abstract ```typescript import {Chain} from '@layerzerolabs/lz-definitions'; import {ethers} from 'ethers'; import axios from 'axios'; import 'dotenv/config'; /** * LayerZero OFT Transfer API - Ethers.js Integration Example * * This example demonstrates how to integrate the LayerZero OFT Transfer API * to execute OFT transfers using ethers.js and TypeScript. * * How it works: * 1. **Token Discovery**: Uses the LayerZero API to discover tokens * across multiple chains and get their canonical contract addresses * * 2. **Fetching Transaction Data**: Requests pre-built transaction data from the * LayerZero API instead of manually encoding contract calls * * 3. **Executing the Transaction**: Executes the transaction using the provider wallet * * 4. **LayerZero Scan Tracking**: Provides LayerZero scan links to track the * complete cross-chain journey of transfers * * Key Benefits: * - No need to understand complex LayerZero contract interfaces * - Built-in validation and error handling from the API * - Automatic gas estimation and fee calculation * - Handling for both OFTs and OFT Adapters * - Real-time transaction tracking across chains * */ // Configuration const API_KEY = process.env.OFT_API_KEY!; const API_BASE_URL = 'https://metadata.layerzero-api.com/v1/metadata/experiment/ofts'; // RPC configuration using chain names from LayerZero API // RPCs are required only for sending the /transfer transaction, not for /list const RPC_URLS: Record = { [Chain.BSC]: 'https://bsc.drpc.org', [Chain.ABSTRACT]: 'https://api.mainnet.abs.xyz', }; /** * Get the appropriate RPC URL for a chain name. * Chain names come directly from LayerZero API discovery. */ function getRpcUrl(chainName: string): string { const url = RPC_URLS[chainName]; if (!url) { throw new Error(`RPC URL not configured for chain: ${chainName}`); } return url; } /** * Discover where a token is available across chains. * This is the typical user flow - start with a token symbol and see where it's deployed. */ async function discoverTokenDeployments(symbol: string) { try { console.log('🔍 Token Discovery:'); console.log(` Searching for ${symbol} deployments...`); const response = await axios.get(`${API_BASE_URL}/list`, { params: {symbols: symbol}, }); const tokenData = response.data[symbol]?.[0]; if (!tokenData) { throw new Error(`Token ${symbol} not found`); } const availableChains = Object.keys(tokenData.deployments); console.log(` ✓ ${symbol} found on: ${availableChains.join(', ')}`); console.log(); return response.data; } catch (error: any) { console.error('Error discovering token:', error.response?.data || error.message); throw error; } } /** * Extract the OFT contract address for a token on a specific chain. * Prevents address lookup errors and handles chain-specific deployments. */ function getOftAddress(tokens: any, symbol: string, chainName: string): string { const tokenData = tokens[symbol]?.[0]; if (!tokenData) { throw new Error(`Token ${symbol} not found`); } const address = tokenData.deployments[chainName]?.address; if (!address) { throw new Error(`Token ${symbol} not available on ${chainName}`); } return address; } /** * Generate LayerZero scan links for cross-chain transaction tracking. * LayerZero scan shows the complete cross-chain journey, unlike regular block explorers. */ function getLayerZeroScanLink(hash: string): string { // For simplicity, always use mainnet scan since we're working with mainnet chains return `https://layerzeroscan.com/tx/${hash}`; } /** * Execute an OFT token transfer using the LayerZero API. * Uses chain names discovered from the LayerZero API. */ async function transferOFT(fromChain: string, toChain: string, oftAddress: string, amount: string) { try { console.log('🚀 OFT Transfer:'); console.log(` Route: ${fromChain} → ${toChain}`); console.log(); // Get RPC URL for the source chain const rpcUrl = getRpcUrl(fromChain); // Initialize wallet on the source chain const provider = new ethers.providers.JsonRpcProvider(rpcUrl); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); // Check wallet balance const balance = await wallet.getBalance(); console.log('Wallet Information:'); console.log(` Address: ${wallet.address}`); console.log(` Balance: ${ethers.utils.formatEther(balance)} native tokens`); console.log(); // Request pre-built transaction data from LayerZero API console.log('Transaction Preparation:'); console.log(' Requesting transaction data from LayerZero API...'); const response = await axios.get(`${API_BASE_URL}/transfer`, { params: { srcChainName: fromChain, dstChainName: toChain, srcAddress: oftAddress, // Source chain OFT contract address amount, // Amount in token's smallest unit (wei) from: wallet.address, to: wallet.address, // Same address on destination chain validate: true, // Pre-validate balances and parameters }, headers: {'x-layerzero-api-key': API_KEY}, }); const {transactionData} = response.data; console.log(' ✓ Transaction data prepared'); console.log(); // Handle token approval if required (for OFT adapters wrapping existing ERC20s) if (transactionData.approvalTransaction) { console.log('Token Approval:'); console.log(' Sending approval transaction...'); const approvalTx = await wallet.sendTransaction(transactionData.approvalTransaction); await approvalTx.wait(); console.log(' ✓ Approval confirmed'); console.log(); } // Execute the omnichain transfer transaction (includes LayerZero messaging fees) console.log('OFT Transfer:'); console.log(' Sending transfer transaction...'); const transferTx = await wallet.sendTransaction(transactionData.populatedTransaction); await transferTx.wait(); console.log(' ✓ Transaction confirmed'); console.log(); // Provide tracking link for the complete cross-chain journey const scanLink = getLayerZeroScanLink(transferTx.hash); console.log('🎉 Transaction Results:'); console.log(` LayerZero Scan: ${scanLink}`); console.log(); return transferTx.hash; } catch (error: any) { // Extract meaningful error messages from API responses if (error.response?.data) { throw new Error(`API Error: ${JSON.stringify(error.response.data)}`); } throw error; } } /** * Example: OFT transfer * * Demonstrates the complete integration flow: * 1. Use explicit LayerZero chain names for chain identification * 2. Dynamically discover contract addresses * 3. Execute transfers with automatic approval handling * 4. Provide LayerZero transaction tracking */ async function main() { try { console.log('LayerZero OFT API - Ethers.js Example'); console.log('==========================================\n'); // Discover where PENGU is available and choose transfer route const tokens = await discoverTokenDeployments('PENGU'); // Choose source and destination chains from available deployments const fromChain = 'abstract'; const toChain = 'bsc'; // Extract the source chain OFT contract address (always use FROM chain address) const oftAddress = getOftAddress(tokens, 'PENGU', fromChain); console.log(`OFT contract: ${oftAddress}`); console.log(); // Execute the cross-chain transfer using chain names from API await transferOFT( fromChain, // Source chain name toChain, // Destination chain name oftAddress, // Source chain OFT contract address '1000000000000', // Amount in token's smallest unit ); console.log('Example completed successfully!'); } catch (error: any) { // Handle errors gracefully with actionable feedback if (error.response?.data) { console.error('API Error:', error.response.data); } else if (error.message) { console.error('Error:', error.message); } else { console.error('Unknown error:', error); } } } // Export functions for use in other scripts export {discoverTokenDeployments, transferOFT, getRpcUrl, getOftAddress}; // Run main function if this script is executed directly if (require.main === module) { main(); } ``` ### Example: Send $PENGU from Solana to BSC ```typescript import {createUmi} from '@metaplex-foundation/umi-bundle-defaults'; import {createSignerFromKeypair, signerIdentity} from '@metaplex-foundation/umi'; import {fromWeb3JsKeypair} from '@metaplex-foundation/umi-web3js-adapters'; import {Connection, Keypair, VersionedTransaction} from '@solana/web3.js'; import axios from 'axios'; import bs58 from 'bs58'; import 'dotenv/config'; /** * LayerZero OFT Transfer API - Solana to EVM Example * * This example demonstrates sending PENGU tokens from Solana to any EVM chain * using the LayerZero OFT Transfer API and Solana web3.js for transaction handling. * * Key Differences from EVM: * 1. **Transaction Format**: Solana returns base64-encoded VersionedTransaction * 2. **No Approval Required**: Solana doesn't require separate approval transactions * 3. **Different Signing**: Uses Solana keypairs instead of private key strings * 4. **Program IDs**: Uses Solana Program IDs instead of contract addresses */ // Configuration const API_KEY = process.env.OFT_API_KEY!; const API_BASE_URL = 'https://metadata.layerzero-api.com/v1/metadata/experiment/ofts'; const SOLANA_RPC_URL = process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com'; /** * Initialize Solana connection and wallet */ function initializeSolana() { const connection = new Connection(SOLANA_RPC_URL); // Load wallet from private key (base58 format) const privateKeyString = process.env.SOLANA_PRIVATE_KEY!; if (!privateKeyString) { throw new Error('SOLANA_PRIVATE_KEY environment variable is required (base58 format)'); } // Create keypair from base58 private key const privateKeyBytes = bs58.decode(privateKeyString); const keypair = Keypair.fromSecretKey(privateKeyBytes); return {connection, keypair}; } /** * Discover PENGU token deployments (same API as EVM) */ async function discoverTokenDeployments(symbol: string) { try { console.log('🔍 Token Discovery:'); console.log(` Searching for ${symbol} deployments...`); const response = await axios.get(`${API_BASE_URL}/list`, { params: {symbols: symbol}, }); const tokenData = response.data[symbol]?.[0]; if (!tokenData) { throw new Error(`Token ${symbol} not found`); } const availableChains = Object.keys(tokenData.deployments); console.log(` ✓ ${symbol} found on: ${availableChains.join(', ')}`); console.log(); return response.data; } catch (error: any) { console.error('Error discovering token:', error.response?.data || error.message); throw error; } } /** * Send PENGU from Solana to EVM chain using LayerZero API */ async function transferFromSolana( tokens: any, destinationChain: string, amount: string, recipientAddress: string, ) { try { console.log('🚀 OFT Transfer:'); console.log(` Route: solana → ${destinationChain}`); console.log(); // Initialize Solana connection and wallet const {connection, keypair} = initializeSolana(); // Check wallet balance const balance = await connection.getBalance(keypair.publicKey); console.log('Wallet Information:'); console.log(` Address: ${keypair.publicKey.toString()}`); console.log(` SOL Balance: ${(balance / 1e9).toFixed(4)} SOL`); console.log(); // Get Solana PENGU Program ID const solanaOftAddress = tokens.PENGU[0].deployments.solana.address; // Request transaction data from LayerZero API (same API as EVM) console.log('Transaction Preparation:'); console.log(' Requesting transaction data from LayerZero API...'); const response = await axios.get(`${API_BASE_URL}/transfer`, { params: { srcChainName: 'solana', dstChainName: destinationChain, srcAddress: solanaOftAddress, amount, from: keypair.publicKey.toString(), to: recipientAddress, validate: true, }, headers: {'x-layerzero-api-key': API_KEY}, }); const {transactionData} = response.data; console.log(' ✓ Transaction data received from API'); console.log(); // Key Difference: Solana transaction execution if (transactionData.populatedTransaction) { console.log('Transaction Execution:'); console.log(' Deserializing Solana transaction...'); // Deserialize the base64-encoded VersionedTransaction const transactionBuffer = Buffer.from(transactionData.populatedTransaction, 'base64'); const transaction = VersionedTransaction.deserialize(transactionBuffer); console.log(' Signing and sending transaction...'); // Sign transaction with Solana keypair transaction.sign([keypair]); // Send transaction to Solana network const signature = await connection.sendRawTransaction(transaction.serialize()); // Wait for confirmation console.log(' Waiting for confirmation...'); await connection.confirmTransaction(signature, 'confirmed'); console.log(' ✓ Transaction confirmed on Solana'); console.log(); // Provide tracking links console.log('🎉 Transfer Initiated:'); console.log(` Solana Transaction: https://solscan.io/tx/${signature}`); console.log(` LayerZero Scan: https://layerzeroscan.com/tx/${signature}`); console.log(); return signature; } else { throw new Error('API did not return Solana transaction data'); } } catch (error: any) { if (error.response?.data) { console.error('API Error:', JSON.stringify(error.response.data, null, 2)); } else { console.error('Error:', error.message); } throw error; } } /** * Main example: Send PENGU from Solana to BSC */ async function main() { try { console.log('LayerZero OFT API - Solana to EVM Example'); console.log('==========================================\n'); // Step 1: Discover PENGU token (same API as EVM) const tokens = await discoverTokenDeployments('PENGU'); // Step 2: Choose destination and set transfer parameters const destinationChain = 'bsc'; const recipientAddress = '0x742d35Cc6634C0532925a3b8D45A5E6e8b4b5Bca'; // Replace with your address const amount = '1000000000000'; // Amount in smallest units // Step 3: Execute the transfer await transferFromSolana(tokens, destinationChain, amount, recipientAddress); console.log('PENGU transfer example completed successfully!'); } catch (error: any) { console.error('\n❌ Transfer failed:'); if (error.response?.data) { console.error('API Response:', JSON.stringify(error.response.data, null, 2)); } else { console.error('Error:', error.message); } } } // Export functions for use in other scripts export {discoverTokenDeployments, transferFromSolana, initializeSolana}; // Run main function if this script is executed directly if (require.main === module) { main(); } ``` ## Expected Output After running the EVM example, you should see: ```bash ✗ npx ts-node scripts/testOFTAPI.ts LayerZero OFT API - Ethers.js Example ========================================== 🔍 Token Discovery: Searching for PENGU deployments... ✓ PENGU found on: abstract, bsc, ethereum, solana OFT contract: 0x9ebe3a824ca958e4b3da772d2065518f009cba62 🚀 OFT Transfer: Route: abstract → bsc Wallet Information: Address: 0xed422098669cBB60CAAf26E01485bAFdbAF9eBEA Balance: 0.009736997308229952 native tokens Transaction Preparation: Requesting transaction data from LayerZero API... ✓ Transaction data prepared OFT Transfer: Sending transfer transaction... ✓ Transaction confirmed 🎉 Transaction Results: LayerZero Scan: https://layerzeroscan.com/tx/0x49c44f1ff5ab82ceaeee6c780e991863d75ad544cb6123583c2e335b314b77ab Example completed successfully! ``` After running the Solana example, you should see: ```bash ✗ npx ts-node scripts/testSolanaOFTAPI.ts LayerZero OFT API - Solana to EVM Example ========================================== 🔍 Token Discovery: Searching for PENGU deployments... ✓ PENGU found on: abstract, bsc, ethereum, solana 🚀 OFT Transfer: Route: solana → bsc Wallet Information: Address: 7BgBvyjrZX1YKz4oh9mjb8XScatufuNqPH7YLyRWCATp SOL Balance: 0.0421 SOL Transaction Preparation: Requesting transaction data from LayerZero API... ✓ Transaction data received from API Transaction Execution: Deserializing Solana transaction... Signing and sending transaction... Waiting for confirmation... ✓ Transaction confirmed on Solana 🎉 Transfer Initiated: Solana Transaction: https://solscan.io/tx/2xK8vKv7... LayerZero Scan: https://layerzeroscan.com/tx/2xK8vKv7... PENGU transfer example completed successfully! ``` ## Common Issues & Solutions ### Amount Validation Errors **Error:** ``` API Error: {"code":4000,"message":"Amount Invalid. Config: {\"sharedDecimals\":6,\"localDecimals\":18,\"currentAmount\":\"1000\",\"minAmount\":\"1000000000000\"}"} ``` **Cause:** This error occurs due to the decimal conversion rate between `localDecimals` and `sharedDecimals`. The OFT standard enforces that transfer amounts must be greater than or equal to the decimal conversion rate to prevent precision loss when transferring tokens between blockchains. The `minAmount` in the error response represents the decimal conversion rate: `10^(localDecimals - sharedDecimals)`. In this example: `10^(18-6) = 10^12 = 1000000000000`. **Solution:** Ensure your amount (in minor units) is greater than or equal to the decimal conversion rate: ```typescript // ❌ Wrong - amount smaller than conversion rate const amount = '1000'; // Less than 10**(localDecimals - sharedDecimals) // ✅ Correct - amount meets minimum conversion rate requirement const amount = '1000000000000'; // Exactly 10**(localDecimals - sharedDecimals) (minimum) const amount = '5000000000000000000'; // 5 tokens (5*10**localDecimals) ``` For detailed explanation of how `sharedDecimals` and `localDecimals` work together to enforce minimum transfer amounts, see the [OFT Technical Reference](../../concepts/technical-reference/oft-reference.md#1-transferring-value-across-different-vms). ### Insufficient Balance **Error:** ``` API Error: {"code":4000,"message":"Insufficient OFT balance: 114000000000000 < 100000000000000000000000"} ``` **Cause:** This error occurs when the API validates that your wallet doesn't have enough OFT tokens to perform the requested transfer. The error message shows your current balance vs. the requested transfer amount (both in the token's smallest unit). In this example: `114000000000000` (current balance) < `100000000000000000000000` (requested amount). **Solution:** Check both native token (for fees) and token balances: ```typescript // Check native balance for gas fees const nativeBalance = await wallet.getBalance(); console.log(`Native balance: ${ethers.utils.formatEther(nativeBalance)}`); // Check token balance const tokenContract = new ethers.Contract( tokenAddress, ['function balanceOf(address) view returns (uint256)'], provider, ); const tokenBalance = await tokenContract.balanceOf(wallet.address); console.log(`Token balance: ${tokenBalance.toString()}`); // Ensure your transfer amount is <= your token balance const transferAmount = '1000000000000000000'; // 1 token with 18 decimals if (tokenBalance.lt(transferAmount)) { console.error('Insufficient token balance for transfer'); } ``` ### Network Not Supported **Error:** `Unsupported source chain: chainName` **Solution:** Ensure the chain is configured in your RPC_URLS mapping and supported by the API. --- _By using the OFT API, you agree to the [OFT API Terms of Use](./terms.md)._ --- --- title: OFT API Terms of Use --- **Terms of Use** Last Updated: June 23, 2025 ## 1. **Introduction** Welcome to the [LayerZero OFT Direct Transfer API](./oft-reference.mdx) (the “**Transfer API**”), provided by LayerZero Labs Ltd. (“**LayerZero**”, “**we**”, “**our**”, or “**us**”). The Transfer API is an application programming interface (API) which enables direct cross-chain transfers of any Omnichain Fungible Token (OFT) utilizing the native burn/mint mechanisms of OFTs. The Transfer API provides two core functionalities: (i) an OFT verification mechanism which lists all supported OFTs, connected blockchain endpoint details, and verified OFT contract addresses across all connected blockchains; and (ii) direct burn/mint transfer paths for verified OFTs. The Transfer API is designed primarily for developers and technical users.  By accessing or using this Transfer API, you agree to be bound by these Terms of Use (the “**Terms**”) and our Privacy Policy which apply to the Transfer API and any related content, tools, software, documentation, features, and functionality, and any updated or new features thereof, offered by LayerZero on or through the Transfer API. If you do not agree to these Terms, you are not authorized to access or use the Transfer API and should not use the Transfer API. Please read these Terms carefully, as they include important information about your legal rights. You are solely responsible for determining whether your access to and use of the Transfer API complies with the Terms as well as any applicable laws and regulations in your jurisdiction. For purposes of these Terms, “**you**” and “**your**” means you as the user of the Transfer API. If you access or use the Transfer API on behalf of a company or other entity then “you” includes both you in an individual capacity and that entity, and you represent and warrant that: (a) you are an authorized representative of the entity with the authority to bind the entity to these Terms; and (b) you agree to these Terms on the entity’s behalf, as well as on your individual behalf. PLEASE NOTE: THE "DISPUTE RESOLUTION" SECTION OF THESE TERMS CONTAINS AN ARBITRATION CLAUSE THAT REQUIRES DISPUTES TO BE ARBITRATED ON AN INDIVIDUAL BASIS, AND PROHIBITS CLASS ACTION CLAIMS. IT AFFECTS HOW DISPUTES BETWEEN YOU AND LAYERZERO ARE RESOLVED. BY ACCEPTING THESE TERMS, YOU AGREE TO BE BOUND BY THIS ARBITRATION PROVISION. PLEASE READ IT CAREFULLY. ## 2. **Modification of these Terms** LayerZero reserves the right, in its sole discretion, to modify these Terms from time to time. If any modifications are made, you will be notified by an update to the “Last Updated” date at the top of these Terms. All modifications will be effective when they are posted, or such later date as may be specified in the updated Terms, and your continued access or use of the Transfer API after any modifications have become effective will serve as confirmation of your acceptance of those modifications. If you do not agree with any modifications to these Terms, you must immediately stop accessing or using the Transfer API. ## 3. **Eligibility and Permitted Use** To access or use the Transfer API, you must be able to form a legally binding contract with us. Accordingly, you represent that you are at least 18 years old or the age of majority in your jurisdiction and have the full right, power, and authority to enter into and comply with the terms and conditions of these Terms on behalf of yourself and any company or legal entity for which you may access or use the Transfer API. You further represent that you are not (a) the subject of any economic or trade sanctions administered or enforced by any governmental authority, including any person designated on any list of prohibited or restricted parties by any governmental authority, including, without limitation, the European Union (“EU”) Consolidated List of Persons, Groups, and Entities, the United Kingdom (“UK”) Consolidated List of Financial Sanctions Targets (including as extended to the British Virgin Islands by statutory instrument), the United States (“U.S.”) Treasury Department’s list of Specially Designated Nationals, and any other lists or sanctions programs managed by the Office of Foreign Assets Control (“OFAC”) of the U.S. Department of the Treasury; (b) located in, incorporated in, or otherwise organized or established in, or resident of, any country, territory, or jurisdiction that is the subject of comprehensive country-wide, territory-wide, or regional economic sanctions or embargoes or has been designated as “terrorist supporting” by the United Nations (“UN”) or any governmental authority of the EU, UK (including as extended to the British Virgin Islands by statutory instrument), the British Virgin Islands, or the U.S., including the OFAC of the U.S. Treasury Department or the Office of Financial Sanctions (“OFSI”) of HM Treasury of the UK; (c) owned or controlled by such persons or entities described in (a)-(b); or (d) accessing or using the Transfer API on behalf of persons or entities described in (a)-(c).  You acknowledge and agree that you are solely responsible for complying with all applicable laws of the jurisdiction you are a resident of, or located or accessing the Transfer API from, and you represent that your access and use of the Transfer API will fully comply with all applicable laws and regulations. By using the Transfer API you represent and warrant that you meet these requirements, will not use the Transfer API for any illegal activity or to engage in the “Prohibited Activities” as set forth below, and will not access or use the Transfer API to conduct, promote, or otherwise facilitate any illegal activity. You further represent and warrant that you are not, will not, and will not attempt to access or use the Transfer API via a virtual private network or any other similar means intended to circumvent the restrictions set forth herein. Subject to your strict compliance with these Terms, LayerZero grants you a **limited, non-exclusive, non-transferable, non-sublicensable, revocable license** to access and use the Transfer API solely for your personal, non-commercial use, in each case in accordance with these Terms. If any software, content, or other materials owned or controlled by us are distributed or made available to you as part of your use of the Transfer API, we hereby grant you a personal, non-assignable, non-sublicensable, non-transferrable, and non-exclusive license to download, access, and/or display such software, content, and/or materials provided to you as part of the Transfer API, in each case for the sole purpose of enabling you to use the Transfer API as permitted by these Terms. These licenses are provided solely to enable you to use and enjoy the benefit of the Transfer API as intended by LayerZero and as permitted by these Terms. These licenses will terminate immediately if you breach any provision of these Terms or upon any termination or suspension of your access to the Transfer API. No other rights, interests, or licenses are granted to you with respect to the Transfer API, whether by implication, estoppel, or otherwise. You agree to review, understand, and comply with any and all applicable licenses, usage guidelines, terms of service, and technical documentation that are provided, directly or indirectly, through the Transfer API, bundled with or referenced in any API(s), SDK(s), or libraries made available, directly or indirectly, through the Transfer API, and/or otherwise published or made available by LayerZero from time to time, including, but not limited to, any updates, amendments, or successor versions of such materials. Your obligations hereunder shall include, but are not limited to: (A) respecting any constraints on data access, processing, storage, or redistribution as outlined in such documentation; (B) adhering to any permitted use cases, restrictions, or disclaimers associated with particular datasets or features; (C) following implementation requirements or recommendations specified in technical documents to ensure interoperability, security, and proper attribution; and (D) monitoring for and incorporating updates to the documentation or terms, as continued use of the Transfer API after such updates constitutes acceptance of the revised materials. Failure to comply with any applicable licenses, guidelines, or documentation may result in immediate suspension or termination of access to the Transfer API, and may subject you to legal liability. You agree to properly attribute any data or content retrieved via the Transfer API to LayerZero. Your attribution obligations shall include, but are not limited to: (i) crediting LayerZero and the Transfer API as the source of the data or content in any application, product, service, publication, or display that incorporates, visualizes, or redistributes such data or content; (ii) including any mandatory attribution statements, watermarks, logos, or links as specified from time to time; and (iii) preserving data and content integrity and refraining from modifying, omitting, or misrepresenting any portion of the data or content in any way, including, but not limited to, by presenting the data or content in a way that may be misleading or falsely suggest sponsorship, endorsement, or association with LayerZero. Failure to comply with attribution requirements may result in suspension or termination of access to the Transfer API, and may constitute a violation of applicable intellectual property laws or licensing terms. You further agree to comply fully with any authentication, API key management, and rate limiting requirements as established by LayerZero from time to time. This obligation shall include, but is not limited to: (I) use of valid and authorized credentials (such as API keys or tokens) assigned specifically to you or your organization (such credentials not to be shared, sublicensed, or exposed to any third party); (II) maintaining the confidentiality and security of all access credentials, and providing immediate notification to LayerZero of any, known or suspected, unauthorized use, compromise, or security breach involving your credentials, or the credentials of your organization, or access to the Transfer API; (III) strict adherence with all rate limits or usage quotas imposed by LayerZero, including those published in the API documentation or enforced via technical measures, and neither doing anything or attempting to do anything that would circumvent, disable, or tamper with any rate-limiting functionality or usage quotas; and (IV) avoiding any automated or excessive request activity that may degrade, disrupt, or interfere with the performance or availability of the Transfer API to other users. LayerZero reserves the right to monitor the Transfer API usage and enforce any requirements, restrictions, and/or limits through throttling, suspension, or revocation of access where misuse, abuse, or violations are detected. Violations of this section may also result in legal action, especially if such actions compromise the security, integrity, or availability of the Transfer API. Your access and use of the Transfer API may be interrupted from time to time for any or for no reason, including, without limitation, in the event of the malfunction of equipment, periodic updating, maintenance, or repair of the Transfer API or other actions that LayerZero, in its sole discretion, may elect to take. WITHOUT PREJUDICE TO ANY OTHER RIGHTS OF LAYERZERO UNDER THESE TERMS, LAYERZERO RESERVES THE RIGHT TO, AT ITS SOLE DISCRETION AND WITHOUT PRIOR NOTICE, SUSPEND, LIMIT, OR TERMINATE ACCESS TO OR USE OF THE TRANSFER API AT ANY TIME, FOR ANY REASON OR NO REASON. YOU AGREE THAT LAYERZERO SHALL HAVE NO LIABILITY TO YOU OR ANY THIRD PARTY FOR ANY INABILITY TO ACCESS OR USE THE TRANSFER API, OR FOR ANY SUSPENSION OR TERMINATION OF ACCESS TO OR USE OF THE TRANSFER API. All rights not expressly granted to you under these Terms are reserved by LayerZero and its licensors.  ## 4. **Proprietary Rights** The Transfer API, including, without limitation, its “look and feel” (e.g., text, graphics, images, logos), content, functionality, APIs, documentation, data, features, software, trademarks, service marks, copyrights, patents, and designs as well as any other proprietary content, information, and material (collectively, the “**Transfer API IP**”), are the exclusive property of LayerZero and its related entities and are protected under copyright, trademark, and other intellectual property laws. You agree that LayerZero, its related entities, and/or its licensors exclusively own all right, title, and interest in and to the Transfer API IP (including any and all intellectual property rights therein) and you agree not to take any action(s) inconsistent with such ownership interests. You shall not obtain any right, title, interest, or share in or to the Transfer API IP by virtue of these Terms or your access to or use of the Transfer API. You agree not to remove, alter, or obscure any copyright, trademark, service mark, or other proprietary rights notices incorporated in or accompanying the Transfer API. LayerZero reserves all rights in connection with the Transfer API and its content. ## 5. **Additional Rights** LayerZero reserve the following rights: (a) with or without prior notice to you, to modify, substitute, eliminate, or add to the Transfer API; (b) to review, modify, filter, disable, delete, and remove the Transfer API, including any and all content and information associated with it; and (c) to cooperate with any law enforcement agency, court order, government investigation or order, or third party requesting and requiring, directing that we disclose information or content that you provide. ## 6. **Prohibited Activities** You agree not to engage in, or attempt to engage in, or do any of the following categories of prohibited activities in connection with your access and/or use of the Transfer API, unless applicable laws or regulations prohibit these restrictions or you have our written permission to do so: 1. copy, reproduce, alter, modify, adapt, translate, distribute, transmit, display, perform, duplicate, publish, license, sublicense, assign, create derivative works from, or offer for sale the Transfer API, including any underlying code, software, or other components that comprise the Transfer API, except for temporary files that are automatically cached by your web browser for display purposes, or as otherwise expressly permitted in these Terms; 2. disclose, or otherwise make available, the Transfer API, in whole or in part, to any person or entity; 3. decompile, disassemble, reverse engineer, attempt to reconstruct or derive the source code or structure of, or otherwise attempt to gain unauthorized access to any component of, the Transfer API; 4. remove, alter, or obscure, any copyright, trademark, service mark, trade name, slogan, logo, image, or other proprietary notation displayed on or through the Transfer API; 5. any activity that infringes on or violates any copyright, trademark, service mark, patent, right of publicity, right of privacy, or other proprietary or intellectual property rights under the law; 6. use automation software (bots), hacks, modifications (mods) or any other unauthorized third-party software designed to access, use, or modify the Transfer API; 7. access or use the Transfer API in any manner that could disable, overburden, damage, disrupt, degrade, or impair the Transfer API or its performance, or interfere with any other party’s access to or use of the Transfer API, including, without limitation, by exceeding any rate limit(s) imposed, or use any device, software, or routine that causes the same; 8. attempt to gain unauthorized access to, interfere with, damage, or disrupt the Transfer API, or the computer systems or networks connected to the Transfer API; 9. circumvent, remove, alter, deactivate, degrade, or thwart any technological measure or content protections of the Transfer API or any of the computer systems, wallets, accounts, protocols, or networks connected to the Transfer API; 10. use any robot, spider, crawlers, or other automatic device, process, software, or queries that intercepts, “mines,” scrapes, or otherwise accesses the Transfer API to monitor, extract, copy, or collect information or data from or through the Transfer API, or engage in any manual process to do the same; 11. introduce any viruses, trojan horses, worms, logic bombs, or other materials that are malicious or technologically harmful into our systems; 12. any activity that seeks to interfere with or compromise the integrity, security, or proper functioning of any computer, server, network, personal device, or other information technology system, including (but not limited to) the deployment of viruses and denial of service attacks; 13. impersonate any other person or entity using the Transfer API, including by falsely stating, implying, or otherwise misrepresenting your affiliation with any person or entity; 14. any activity that seeks to defraud us or any other person or entity, including (but not limited to) providing any false, inaccurate, or misleading information in order to unlawfully obtain the property of another; 15. violate any applicable law, rule, or regulation of a relevant jurisdiction in connection with your access to or use of the Transfer API, including, without limitation, any restrictions or regulatory requirements of Canada, the United States, or the British Virgin Islands; or 16. access or use the Transfer API in any way not expressly permitted by these Terms. ## 7. **Feedback** You acknowledge and expressly agree that any contribution by you of any bug report, comment, idea, enhancement and/or enhancement request, recommendation, proposal, correction, suggestion for improvement(s), or other feedback of any kind, in any forum, with respect to the Transfer API (“**Feedback**”) shall become the sole and exclusive property of LayerZero and does not and will not give or grant you any right, title, or interest in or to the Transfer API, Transfer API IP, or any such Feedback. You agree that LayerZero may use and disclose Feedback in any manner and for any purpose whatsoever without further notice or compensation to you, and without retention by you of any proprietary or other right or claim. You hereby irrevocably assign to LayerZero any and all right, title, and interest (including, but not limited to, any patent, copyright, trade secret, trademark, show-how, know-how, moral rights, and any and all other intellectual property right) that you may have in and to any and all Feedback, and, to the extent that any rights in and to Feedback cannot be assigned (including without limitation any moral rights), you hereby agree to waive such rights. To the extent that any Feedback is not assignable, you hereby grant to LayerZero a fully paid up, royalty-free, worldwide, perpetual, exclusive, irrevocable, sublicensable (with the right to sublicense through multiple tiers), and transferable right and license to use, create derivative works of, reproduce, re-format, perform, display, adapt, modify, distribute, and commercialize, or otherwise commercially or non-commercially exploit in any manner, any and all such Feedback for any purpose, including, but not limited to, by incorporating any Feedback into the Transfer API. ## 8. **Privacy** All information collected on the Transfer API is subject to our Privacy Policy. By using the Transfer API, you consent to all actions taken by us with respect to any collection and/or use of your information in compliance with the Privacy Policy which further describes how we handle the information you provide to us when you use the Transfer API. For an explanation of our privacy practices, visit our Privacy Policy located on the Transfer API.  Without limiting anything in the Privacy Policy, you acknowledge and agree that when you use the Transfer API you may be interacting with public blockchains, which provide transparency into your transactions. LayerZero does not control and is not responsible for any information you make public on any public blockchain by taking actions through the Transfer API. ## 9. **Third Party Services and Materials** The Transfer API may display, include, reference, link to (including links to third-party websites), or otherwise make available services, products, promotions, content, data, information, resources, applications, or any other material(s) made available by a third-party or by third-parties (“**Third-Party Services and Materials**”). All Third-Party Services and Materials are made available solely as a convenience, and LayerZero does not own, control, or endorse any Third-Party Services and Materials. You agree that your access and use of such Third-Party Services and Materials is governed solely by the terms and conditions of such Third-Party Services and Materials, as applicable. LayerZero is not responsible or liable for, and makes no representations as to any aspect of such Third-Party Services and Materials, including, without limitation, their content, availability, or the manner in which they handle, protect, manage, or process data, or any interaction between you and the provider of such Third-Party Services and Materials. Any statements and/or opinions expressed by or through any Third-Party Services and Materials by any third-party or third-parties is/are solely the opinion(s) and the responsibility of the person or entity providing those materials. LayerZero is not responsible for examining or evaluating the content, accuracy, completeness, availability, timeliness, validity, copyright compliance, legality, decency, quality, or any other aspect of such Third-Party Services and Materials or websites. You irrevocably waive any claim against LayerZero with respect to such Third-Party Services and Materials. LayerZero is not liable for any damage or loss caused or alleged to be caused by or in connection with your enablement, access, or use of any such Third-Party Services and Materials, or your reliance on the privacy practices, data security processes, or other policies of such Third-Party Services and Materials. Third-Party Services and Materials and links to other websites are provided solely as a convenience to you and you access and/or use them at your own risk. ## 10. **Not Registered with FinCEN or Any Agency; No Advice Given** LayerZero is not registered with the Financial Crimes Enforcement Network as a money services business or in any other capacity. You understand and acknowledge that we are not a marketplace facilitator, a financial institution, broker, exchange, clearing house, or creditor, nor do we broker trading orders on your behalf or match orders for buyers and sellers of securities. While we provide infrastructure to transmit cross-chain messages via the LayerZero Protocol, we are not involved in executing or settling trades (all such activity takes place directly between users entirely on public distributed blockchains). You acknowledge and agree that all transfers, liquidity pooling, farming, staking, or any other actions you may undertake via the Transfer API (or via any other platform based on information available on the Transfer API) are unsolicited. This means you have not received, nor relied upon, any investment advice from us regarding such actions. Furthermore, we do not assess the suitability of any such actions for you. You alone are responsible for determining whether any investment, investment strategy, or related transaction is appropriate for you based on your personal investment objectives, financial circumstances, and risk tolerance. The Transfer API does not provide, and does not purport to provide, any financial, investment, legal, or tax advice or services. Use of the Transfer API should not be construed as an offer or solicitation to buy or sell any financial instrument or as a recommendation to engage in any transaction. You should consult with qualified professionals before making any financial decisions. The Transfer API is intended solely as a technical interface to decentralized systems, and no fiduciary relationship is created between you and the Transfer API or LayerZero. ## 11. **No Warranties** THE TRANSFER API IS PROVIDED ON AN "AS-IS" AND "AS-AVAILABLE" BASIS, WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, OR NON-INFRINGEMENT. TO THE FULLEST EXTENT PERMITTED BY LAW, LAYERZERO WILL NOT BE LIABLE FOR ANY DAMAGES OF ANY KIND ARISING OUT OF OR RELATED TO YOUR ACCESS TO OR USE OF THE TRANSFER API, INCLUDING, BUT NOT LIMITED TO, ANY DIRECT, INDIRECT, INCIDENTAL, PUNITIVE, EXEMPLARY, SPECIAL, OR CONSEQUENTIAL DAMAGES, EVEN IF LAYERZERO HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. YOU ACKNOWLEDGE AND AGREE THAT YOUR ACCESS TO AND USE OF THE TRANSFER API WILL BE AT YOUR SOLE RISK, AND THAT LAYERZERO SHALL NOT BE LIABLE FOR ANY LOSS OR DAMAGE THAT MAY RESULT FROM YOUR ACCESS TO OR USE OF THE TRANSFER API, INCLUDING, BUT NOT LIMITED TO: YOUR INABILITY TO ACCESS OR USE THE TRANSFER API; MODIFICATION, SUSPENSION, OR TERMINATION OF THE TRANSFER API; ERRORS, OMISSIONS, INTERRUPTIONS, DELAYS, OR TRANSMISSION FAILURES; UNAUTHORIZED ACCESS TO OR ALTERATION OF ANY TRANSMISSION OR DATA; ANY TRANSACTION OR AGREEMENT ENTERED INTO THROUGH THE TRANSFER API; ANY ACTIVITIES, CONDUCT, CONTENT, OR COMMUNICATIONS OF THIRD PARTIES; OR ANY DATA OR MATERIAL OBTAINED FROM A THIRD PARTY SOURCE ON OR THROUGH THE TRANSFER API. LAYERZERO MAKES NO WARRANTIES OR REPRESENTATIONS REGARDING THE ACCURACY, COMPLETENESS, OR RELIABILITY OF THE TRANSFER API OR ANY CONTENT LINKED TO OR ACCESSED THROUGH IT. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, LAYERZERO EXPRESSLY DISCLAIMS ANY LIABILITY FOR ANY (1) ERRORS, MISTAKES, INACCURACIES, OR OMISSIONS IN ANY CONTENT OR MATERIALS, (2) PERSONAL INJURY OR PROPERTY DAMAGE, OF ANY NATURE WHATSOEVER, RESULTING FROM YOUR ACCESS TO AND/OR USE OF THE TRANSFER API, (3) UNAUTHORIZED ACCESS TO OR USE OF LAYERZERO’S SECURE SERVERS AND/OR ANY AND ALL PERSONAL OR FINANCIAL INFORMATION STORED THEREIN, (4) INTERRUPTION, SUSPENSION, CESSATION, OR TERMINATION OF TRANSMISSION TO OR FROM THE TRANSFER API, (5) BUGS, VIRUSES, TROJAN HORSES, OR OTHER HARMFUL CODE THAT MAY BE TRANSMITTED TO OR THROUGH THE TRANSFER API BY ANY THIRD PARTY, AND (6) LOSS OR DAMAGE INCURRED AS A RESULT OF THE USE OF ANY CONTENT OR MATERIALS MADE AVAILABLE THROUGH THE TRANSFER API, WHETHER POSTED, TRANSMITTED, OR OTHERWISE DISSEMINATED.  IF YOU ARE DISSATISFIED WITH THE TRANSFER API, YOU AGREE THAT YOUR SOLE AND EXCLUSIVE REMEDY SHALL BE FOR YOU TO DISCONTINUE YOUR USE OF THE TRANSFER API. CERTAIN JURISDICTIONS DO NOT PERMIT THE EXCLUSION OR LIMITATION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES; AS SUCH, THE FOREGOING LIMITATIONS AND EXCLUSIONS MAY NOT APPLY TO YOU TO THE EXTENT PROHIBITED BY APPLICABLE LAW. ## 12. **Non-Custodial and No Fiduciary Duties** The Transfer API is a non-custodial application. At no point does LayerZero or the Transfer API ever take possession, custody, or control of user assets. All transactions are initiated and completed directly by users through self-custody wallets that they control. By accessing and using the Transfer API, you acknowledge and agree that you are solely responsible for the management, custody, and security of your private cryptographic keys and wallet credentials. Loss or compromise of your private key(s) may result in permanent loss of access to your digital wallet and assets, and the Transfer API cannot retrieve or restore such access. The Transfer API allows users to connect self-custody wallets. At no point does LayerZero or the Transfer API host, maintain, or manage such wallets. By accessing and using the Transfer API, you acknowledge and agree that: (i) any interaction with your self-custody wallet is solely between you and the third-party provider of that wallet; (ii) your use of any self-custody wallet is subject to the terms and conditions of the applicable third-party provider; (iii) neither LayerZero nor the Transfer API is responsible for any acts or omissions of the wallet provider, including, but not limited to, delays, errors, or losses related to wallet connectivity or signature approval mechanisms; and (iv) any actions you take through your wallet (e.g., approving transactions) are final and irreversible. These Terms are not intended to, and do not, create or impose any fiduciary duties on us. To the maximum extent permitted by applicable law, you acknowledge and agree that we owe no fiduciary duties or liabilities to you or to any other party. Any such duties or liabilities that may otherwise exist at law or in equity are hereby fully disclaimed and waived. You further agree that our only duties and obligations are those explicitly set forth in these Terms. ## 13. **Assumption of Risk** By accessing and using the Transfer API, you represent that you are financially and technically sophisticated enough to understand the inherent risks associated with using cryptographic and blockchain-based systems, and that you have a working knowledge of the usage and intricacies of blockchain technologies, cryptocurrencies, and other digital assets, storage mechanisms, and blockchain-based software systems to be able to assess and evaluate the risks and benefits of the Transfer API contemplated hereunder, and will bear the risks thereof, including loss of all amounts paid or stored, and the risk that the cryptocurrencies and other digital assets may have little or no value. You understand that blockchain-based transactions are irreversible. You acknowledge that there are inherent risks associated with using or interacting with public blockchains and blockchain technology. There is no guarantee that such technology will be available or not subject to errors, hacking, or other security risks. Blockchain protocols may also be subject to sudden changes in operating rules, including forks, and it is your responsibility to make yourself aware of upcoming operating changes. You acknowledge and agree that there are risks associated with purchasing and holding cryptocurrency. These include, but are not limited to, risk of losing access to cryptocurrency or digital assets due to slashing; loss of private key(s); custodial error or purchaser or user error; risk of mining, staking, or blockchain-related attacks; risk of hacking and security weaknesses; risk of unfavorable regulatory intervention in one or more jurisdictions; risk related to token taxation; risk of personal information disclosure; risk of uninsured losses; volatility risks; and unanticipated risks. You further understand that the markets for digital assets are highly volatile due to factors including (but not limited to) adoption, speculation, technology, security, and regulation. You acknowledge and accept that the cost and speed of transacting with cryptographic and blockchain-based systems are variable and may increase dramatically at any time. You further acknowledge and accept the risk that your digital assets may lose some or all of their value and that you may suffer loss due to the fluctuation of prices of tokens.  You understand that anyone can create a token, including fake versions of existing tokens and tokens that falsely claim to represent projects, and acknowledge and accept the risk that you may mistakenly trade those or other tokens. You further acknowledge that we are not responsible for any of these variables or risks and cannot be held liable for any resulting losses that you experience while accessing or using the Transfer API. Accordingly, you understand and agree to assume full responsibility for all of the risks of accessing and using the Transfer API. You further acknowledge and agree that your access and use of the Transfer API may be interrupted from time to time for any or for no reason, including, without limitation, in the event of the malfunction of equipment, periodic updating, maintenance or repair of the Transfer API or other actions that LayerZero, in its sole discretion, may elect to take. You agree that we shall have no liability to you arising from or related to any inability to access or use the Transfer API. ## 14. **Third-Party Beneficiaries** You and LayerZero acknowledge and agree that LayerZero’s affiliates, subsidiaries, related companies, service providers, and its and their officers, directors, supervisors, consultants, advisors, agents, representatives, partners, employees, and licensors are third party beneficiaries of these Terms. ## 15. **Release of Claims** You expressly agree that you assume all risks in connection with your access and use of the Transfer API. You further expressly waive and release us from any and all liability, claims, causes of action, or damages arising from or in any way relating to your access or use of the Transfer API.  If you are a California resident, you waive the benefits and protections of California Civil Code § 1542, which provides: "\[a\] general release does not extend to claims that the creditor or releasing party does not know or suspect to exist in his or her favor at the time of executing the release and that, if known by him or her, would have materially affected his or her settlement with the debtor or released party." ## 16. **Indemnity** You agree to hold harmless, release, defend, and indemnify us, our affiliates, subsidiaries, related companies, service providers, and its and their officers, directors, employees, contractors, and agents from and against all claims, damages, obligations, losses, liabilities, costs, and expenses (including attorneys’ fees and costs) arising out of or in connection with: (a) your access and use, or misuse, of the Transfer API; (b) your violation of any term or condition of these Terms, the right of any third party, or any other applicable law, rule, or regulation; (c) your dishonesty, negligence, fraudulence, or willful misconduct; and (d) any other party's access and use, or misuse, of the Transfer API with your assistance or using any device or account that you own or control. ## 17. **Limitation of Liability** TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL LAYERZERO, OR ITS OFFICERS, DIRECTORS, EMPLOYEES, CONTRACTORS, AGENTS, AFFILIATES, OR SUBSIDIARIES, BE LIABLE TO YOU FOR ANY INDIRECT, PUNITIVE, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR EXEMPLARY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA, OR OTHER INTANGIBLE LOSSES, ARISING OUT OF OR RELATING TO YOUR ACCESS TO OR USE OF THE TRANSFER API, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. LAYERZERO SHALL NOT BE LIABLE FOR ANY DAMAGE, LOSS, OR INJURY RESULTING FROM HACKING, TAMPERING, OR OTHER UNAUTHORIZED ACCESS TO OR USE OF THE TRANSFER API OR THE INFORMATION CONTAINED THEREIN.  WITHOUT LIMITING THE FOREGOING, LAYERZERO ASSUMES NO LIABILITY OR RESPONSIBILITY FOR ANY: (A) ERRORS, MISTAKES, INACCURACIES, OR OMISSIONS IN ANY CONTENT OR MATERIALS; (B) PERSONAL INJURY OR PROPERTY DAMAGE, OF ANY NATURE WHATSOEVER, RESULTING FROM ANY ACCESS TO OR USE OF THE TRANSFER API; (C) UNAUTHORIZED ACCESS TO OR USE OF ANY SECURE SERVER OR DATABASE UNDER OUR CONTROL, OR ANY DATA STORED THEREIN; (D) INTERRUPTION OR CESSATION OF ANY FUNCTION RELATED TO THE TRANSFER API; (E) BUGS, VIRUSES, TROJAN HORSES, OR OTHER HARMFUL CODE THAT MAY BE TRANSMITTED VIA THE TRANSFER API; (F) ANY CONTENT MADE AVAILABLE THROUGH THE TRANSFER API, OR ANY LOSS OR DAMAGE ARISING FROM ITS USE; OR (G) THE DEFAMATORY, OFFENSIVE, OR UNLAWFUL CONDUCT OF ANY THIRD PARTY.  IN NO EVENT SHALL LAYERZERO’S, OR ANY OF ITS OFFICERS, DIRECTORS, EMPLOYEES, CONTRACTORS, AGENTS, AFFILIATES, OR SUBSIDIARIES, TOTAL AGGREGATE LIABILITY TO YOU FOR ANY AND ALL CLAIMS, PROCEEDINGS, LIABILITIES, OBLIGATIONS, DAMAGES, LOSSES, OR COSTS EXCEED THE AMOUNT YOU PAID TO US IN EXCHANGE FOR ACCESS TO AND USE OF THE TRANSFER API, OR USD$50.00, WHICHEVER IS GREATER. THIS LIMITATION OF LIABILITY APPLIES REGARDLESS OF THE FORM OF ACTION, WHETHER BASED IN CONTRACT, TORT, NEGLIGENCE, STRICT LIABILITY, OR OTHERWISE, AND EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH LIABILITY.  CERTAIN JURISDICTIONS DO NOT PERMIT THE EXCLUSION OR LIMITATION OF CERTAIN WARRANTIES OR LIABILITIES. ACCORDINGLY, CERTAIN OF THE FOREGOING DISCLAIMERS AND LIMITATIONS MAY NOT APPLY TO YOU TO THE EXTENT PROHIBITED BY APPLICABLE LAW. THIS LIMITATION OF LIABILITY SHALL APPLY TO THE FULLEST EXTENT PERMITTED BY LAW. ## 18. **Dispute Resolution** 1. **READ THIS SECTION CAREFULLY** – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT AND TO HAVE A JURY HEAR YOUR CLAIMS. IT CONTAINS PROCEDURES FOR MANDATORY BINDING ARBITRATION AND A CLASS ACTION WAIVER. 2. **Informal Process First.** You and LayerZero agree that in the event of any dispute, claim, or controversy arising out of or relating to these Terms or the breach, termination, enforcement, interpretation, or validity thereof or the access and use of the Transfer API (individually, a “**Dispute**”, and collectively, the “**Disputes**”) between you and LayerZero, you must contact us by sending an email to [notices@layerzerolabs.org](mailto:notices@layerzerolabs.org). You and LayerZero agree to make a good faith sustained effort to resolve any Dispute before resorting to more formal means of resolution, including without limitation, any court action. Both you and LayerZero agree that this dispute resolution procedure is a condition precedent which must be satisfied before initiating any arbitration against the other party. Nothing in this clause shall prevent a party from seeking interim or provisional relief where it is reasonably necessary to do so. 3. **Mandatory Arbitration of Disputes.** If the informal dispute resolution process should fail to produce a satisfactory result within sixty (60) days of your email, or if any Dispute or portion thereof remains unresolved following such process, we each agree that such Dispute will be resolved by binding, individual arbitration pursuant to the following provisions of this “Dispute Resolution” clause, and not in a class, representative, or consolidated action or proceeding. You and LayerZero agree that British Virgin Islands law governs the interpretation and enforcement of these Terms. This arbitration provision shall survive termination of these Terms. 4. **Exceptions.** As limited exceptions to the provisions of this “Dispute Resolution” clause: (i) we both may seek to resolve a Dispute in the Magistrate’s Court of the British Virgin Islands if it qualifies; and (ii) we each retain the right to seek injunctive or other equitable relief from a court to prevent (or enjoin) the infringement or misappropriation of our intellectual property rights. 5. **Conducting Arbitration and Arbitration Rules.** Any Disputes arising out of or relating to these Terms, including the existence, validity, interpretation, performance, breach, or termination thereof, or any Dispute regarding non-contractual obligations arising out of or relating to them, shall be referred to and finally resolved by binding arbitration to be administered by the BVI International Arbitration Centre (“**BVI IAC**”) in accordance with the BVI IAC Arbitration Rules (the “**Arbitration Rules**”) in force as at the date of these Terms, which Arbitration Rules are deemed to be incorporated by reference into these Terms. The arbitration shall be conducted in the English language and the seat of arbitration shall be in Road Town, Tortola, British Virgin Islands. The arbitration shall be determined by a sole arbitrator to be appointed in accordance with the Arbitration Rules.  The decision of the sole arbitrator shall be in writing and shall be final and binding upon both parties without any right of appeal, and judgment upon any award thus obtained may be entered in or enforced by any court having jurisdiction thereof. No action at law or in equity based upon any claim arising out of or in relation to these Terms shall be instituted in any court of any jurisdiction, except as specifically permitted herein. Each party waives any right it may have to assert the doctrine of forum non conveniens, to assert that it is not subject to the jurisdiction of such arbitration or courts, or to object to venue, to the extent any proceeding is brought in accordance herewith. 6. **Arbitration Costs.** Responsibility for payment of all filing, administration, and arbitrator fees will be governed by the Arbitration Rules. We each agree that the prevailing party in arbitration will be entitled to an award of attorneys’ fees and expenses to the extent provided under applicable law. 7. **Injunctive and Declaratory Relief.** Except as provided in the “Exceptions” section above, the arbitrator shall determine all issues of liability on the merits of any claim asserted by either party and may award declaratory or injunctive relief only in favor of the individual party seeking relief and only to the extent necessary to provide relief warranted by that party’s individual claim.  8. **Class Action and Jury Trial Waiver.**  **YOU AND LAYERZERO AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS ACTION, COLLECTIVE ACTION, PRIVATE ATTORNEY GENERAL ACTION, OR OTHER REPRESENTATIVE PROCEEDING**. Further, if the parties’ Dispute is resolved through arbitration, the arbitrator may not consolidate another person’s claims with your claims, and may not otherwise preside over any form of a representative or class proceeding. If this specific provision is found to be unenforceable, then the entirety of this Dispute Resolution section shall be null and void. You and we both agree to waive the right to demand a trial by jury. 9. **Severability.** With the exception of any of the provisions in the immediately preceding paragraph of these Terms (“**Class Action Waiver**”), if an arbitrator or court of competent jurisdiction decides that any part of these Terms is invalid or unenforceable, the other parts of these Terms will still apply. ## 19. **Injunctive Relief** You agree that a breach of these Terms will cause irreparable injury to LayerZero for which monetary damages would not be an adequate remedy and LayerZero shall be entitled to equitable relief in addition to any remedies it may have hereunder or at law without a bond, other security or proof of damages. ## 20. **Force Majeure** We will not be liable or responsible to you, nor be deemed to have defaulted under or breached these Terms, for any failure or delay in fulfilling or performing any of our obligations under these Terms, when and to the extent such failure or delay is caused by or results from any events beyond our ability to control, including acts of God; flood, fire, earthquake, epidemics, pandemics, tsunami, explosion, war, invasion, hostilities (whether war is declared or not), terrorist threats or acts, riot or other civil unrest, government order, law, or action, embargoes or blockades, strikes, labor stoppages or slowdowns or other industrial disturbances, shortage of adequate or suitable Internet connectivity, telecommunication breakdown or shortage of adequate power or electricity, cyberattacks, Protocol-level disruptions, chain-level failures, governance attacks, and other similar events beyond our control. ## 21. **Miscellaneous** If any provision of these Terms shall be unlawful, void or for any reason unenforceable, then that provision shall be deemed severable from these Terms and shall not affect the validity and enforceability of any remaining provisions. These Terms and the licenses granted hereunder may be assigned by LayerZero but may not be assigned by you without the prior express written consent of LayerZero. LayerZero’s failure to enforce any right or provision of these Terms will not be considered a waiver of such right or provision. The waiver of any such right or provision will be effective only if in writing and signed by a duly authorized representative of LayerZero. Except as expressly set forth in these Terms, the exercise by either party of any of its remedies under these Terms will be without prejudice to its other remedies under these Terms or otherwise. The section headings used herein are for reference only and shall not be read to have any legal effect. ## 22. **Governing Law** You agree that the laws of the British Virgin Islands, without regard to principles of conflict of laws, govern these Terms and any Dispute between you and us. You further agree that the Transfer API shall be deemed to be based solely in the British Virgin Islands, and that although the Transfer API may be available in other jurisdictions, its availability does not give rise to general or specific personal jurisdiction in any forum outside the British Virgin Islands. You agree that the courts of the British Virgin Islands are the proper forum for any appeals of an arbitration award or for court proceedings in the event that the binding arbitration clause of these Terms is found to be unenforceable. ## 23. **Entire Agreement** These Terms, and the Privacy Policy, constitute the entire agreement between you and us with respect to the subject matter hereof. These Terms supersedes any and all prior or contemporaneous written and oral agreements, communications and other understandings (if any) relating to the subject matter of the terms. The information provided on the Transfer API is not intended for distribution to or use by any person or entity in any jurisdiction or country where such distribution or use would be contrary to law or regulation or which would subject us to any registration requirement within such jurisdiction or country. Accordingly, those persons who choose to use or access the Transfer API from other locations do so on their own initiative and are solely responsible for compliance with local laws, if and to the extent local laws are applicable. --- --- id: overview title: 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. :::info Terms of Use By using the LayerZero Scan API, you agree to the [**LayerZero Scan API Terms of Use**](./terms.md). ::: --- --- title: LayerZero Scan API Mainnet --- import {CustomSwagger} from '../../../../src/components/CustomSwagger'; import mainnet from '../../../../src/data/swagger/scan-mainnet.json'; --- --- title: LayerZero Scan API Testnet --- import {CustomSwagger} from '../../../../src/components/CustomSwagger'; import testnet from '../../../../src/data/swagger/scan-testnet.json'; --- --- title: LayerZero Scan API Terms of Use --- **Terms of Use** Last Updated: July 23, 2025 ## 1. **Introduction** Welcome to [**LayerZeroScan API Mainnet** ](/tools/api/scan/mainnet.mdx) and [**LayerZeroScan API Testnet** ](/tools/api/scan/testnet.mdx) (collectively, the “Scan API”), provided by LayerZero Labs Ltd. (“**LayerZero**”, “**we**”, “**our**”, or “**us**”). The Scan API hosts the public-facing Swagger (OpenAPI) documentation for the LayerZeroScan application programming interface (API). The Scan API provides an API interface and related documentation to query and interact with blockchain related data indexed and made available on LayerZeroScan. The Scan API is designed primarily for developers and technical users. By accessing or using this Scan API, you agree to be bound by these Terms of Use (the “**Terms**”) and our Privacy Policy. If you do not agree to these Terms, you are not authorized to access or use the Scan API and should not use the Scan API. Please read these Terms carefully, as they include important information about your legal rights. You are solely responsible for determining whether your access to and use of the Scan API complies with the Terms as well as any applicable laws and regulations in your jurisdiction. For purposes of these Terms, “**you**” and “**your**” means you as the user of the Scan API. If you access or use the Scan API on behalf of a company or other entity then “you” includes both you in an individual capacity and that entity, and you represent and warrant that: (a) you are an authorized representative of the entity with the authority to bind the entity to these Terms; and (b) you agree to these Terms on the entity’s behalf, as well as on your individual behalf. PLEASE NOTE: THE "DISPUTE RESOLUTION" SECTION OF THESE TERMS CONTAINS AN ARBITRATION CLAUSE THAT REQUIRES DISPUTES TO BE ARBITRATED ON AN INDIVIDUAL BASIS, AND PROHIBITS CLASS ACTION CLAIMS. IT AFFECTS HOW DISPUTES BETWEEN YOU AND LAYERZERO ARE RESOLVED. BY ACCEPTING THESE TERMS, YOU AGREE TO BE BOUND BY THIS ARBITRATION PROVISION. PLEASE READ IT CAREFULLY. ## 2. **Modification of these Terms** LayerZero reserves the right, in its sole discretion, to modify these Terms from time to time. If any modifications are made, you will be notified by an update to the “Last Updated” date at the top of these Terms. All modifications will be effective when they are posted, or such later date as may be specified in the updated Terms, and your continued access or use of the Scan API after any modifications have become effective will serve as confirmation of your acceptance of those modifications. If you do not agree with any modifications to these Terms, you must immediately stop accessing or using the Scan API. ## 3. **Eligibility and Permitted Use** To access or use the Scan API, you must be able to form a legally binding contract with us. Accordingly, you represent that you are at least 18 years old or the age of majority in your jurisdiction and have the full right, power, and authority to enter into and comply with the terms and conditions of these Terms on behalf of yourself and any company or legal entity for which you may access or use the Scan API. You further represent that you are not (a) the subject of any economic or trade sanctions administered or enforced by any governmental authority, including any person designated on any list of prohibited or restricted parties by any governmental authority, including, without limitation, the European Union (“EU”) Consolidated List of Persons, Groups, and Entities, the United Kingdom (“UK”) Consolidated List of Financial Sanctions Targets (including as extended to the British Virgin Islands by statutory instrument), the United States (“U.S.”) Treasury Department’s list of Specially Designated Nationals, and any other lists or sanctions programs managed by the Office of Foreign Assets Control (“OFAC”) of the U.S. Department of the Treasury; (b) located in, incorporated in, or otherwise organized or established in, or resident of, any country, territory, or jurisdiction that is the subject of comprehensive country-wide, territory-wide, or regional economic sanctions or embargoes or has been designated as “terrorist supporting” by the United Nations (“UN”) or any governmental authority of the EU, UK (including as extended to the British Virgin Islands by statutory instrument), the British Virgin Islands, or the U.S., including the OFAC of the U.S. Treasury Department or the Office of Financial Sanctions (“OFSI”) of HM Treasury of the UK; (c) owned or controlled by such persons or entities described in (a)-(b); or (d) accessing or using the Scan API on behalf of persons or entities described in (a)-(c). You acknowledge and agree that you are solely responsible for complying with all applicable laws of the jurisdiction you are a resident of, or located or accessing the Scan API from, and you represent that your access and use of the Scan API will fully comply with all applicable laws and regulations. By using the Scan API you represent and warrant that you meet these requirements, will not use the Scan API for any illegal activity or to engage in the “Prohibited Activities” as set forth below, and will not access or use the Scan API to conduct, promote, or otherwise facilitate any illegal activity. You further represent and warrant that you are not, will not, and will not attempt to access or use the Scan API via a virtual private network or any other similar means intended to circumvent the restrictions set forth herein. Subject to your strict compliance with these Terms, LayerZero grants you a **limited, non-exclusive, non-transferable, non-sublicensable, revocable license** to access and use the Scan API solely for your personal, non-commercial use, in each case in accordance with these Terms. If any software, content, or other materials owned or controlled by us are distributed or made available to you as part of your use of the Scan API, we hereby grant you a personal, non-assignable, non-sublicensable, non-transferrable, and non-exclusive license to download, access, and/or display such software, content, and/or materials provided to you as part of the Scan API, in each case for the sole purpose of enabling you to use the Scan API as permitted by these Terms. These licenses are provided solely to enable you to use and enjoy the benefit of the Scan API as intended by LayerZero and as permitted by these Terms. These licenses will terminate immediately if you breach any provision of these Terms or upon any termination or suspension of your access to the Scan API. You agree to review, understand, and comply with any and all applicable licenses, usage guidelines, terms of service, and technical documentation that are provided, directly or indirectly, through the Scan API, bundled with or referenced in any API(s), SDK(s), or libraries made available, directly or indirectly, through the Scan API, and/or otherwise published or made available by LayerZero from time to time, including, but not limited to, any updates, amendments, or successor versions of such materials. Your obligations hereunder shall include, but are not limited to: (A) respecting any constraints on data access, processing, storage, or redistribution as outlined in such documentation; (B) adhering to any permitted use cases, restrictions, or disclaimers associated with particular datasets or features; (C) following implementation requirements or recommendations specified in technical documents to ensure interoperability, security, and proper attribution; and (D) monitoring for and incorporating updates to the documentation or terms, as continued use of the Scan API after such updates constitutes acceptance of the revised materials. Failure to comply with any applicable licenses, guidelines, or documentation may result in immediate suspension or termination of access to the Scan API, and may subject you to legal liability. You agree to properly attribute any data or content retrieved via the Scan API to LayerZero. Your attribution obligations shall include, but are not limited to: (i) crediting LayerZero and the Scan API as the source of the data or content in any application, product, service, publication, or display that incorporates, visualizes, or redistributes such data or content; (ii) including any mandatory attribution statements, watermarks, logos, or links as specified from time to time; and (iii) preserving data and content integrity and refraining from modifying, omitting, or misrepresenting any portion of the data or content in any way, including, but not limited to, by presenting the data or content in a way that may be misleading or falsely suggest sponsorship, endorsement, or association with LayerZero. Failure to comply with attribution requirements may result in suspension or termination of access to the Scan API, and may constitute a violation of applicable intellectual property laws or licensing terms. You further agree to comply fully with any authentication, API key management, and rate limiting requirements as established by LayerZero from time to time. This obligation shall include, but is not limited to, strict adherence with all rate limits or usage quotas imposed by LayerZero, including those published in the API documentation or enforced via technical measures, and avoiding any automated or excessive request activity that may degrade, disrupt, or interfere with the performance or availability of the Scan API to other users. LayerZero reserves the right to monitor API usage and enforce limits through throttling, suspension, or revocation of access where misuse, abuse, or violations are detected. Violations of this section may also result in legal action, especially if such actions compromise the security, integrity, or availability of the Scan API. Your access and use of the Scan API may be interrupted from time to time for any or for no reason, including, without limitation, in the event of the malfunction of equipment, periodic updating, maintenance, or repair of the Scan API or other actions that LayerZero, in its sole discretion, may elect to take. WITHOUT PREJUDICE TO ANY OTHER RIGHTS OF LAYERZERO UNDER THESE TERMS, LAYERZERO RESERVES THE RIGHT TO, AT ITS SOLE DISCRETION AND WITHOUT PRIOR NOTICE, SUSPEND, LIMIT, OR TERMINATE ACCESS TO OR USE OF THE SCAN API AT ANY TIME, FOR ANY REASON OR NO REASON. YOU AGREE THAT LAYERZERO SHALL HAVE NO LIABILITY TO YOU OR ANY THIRD PARTY FOR ANY INABILITY TO ACCESS OR USE THE SCAN API, OR FOR ANY SUSPENSION OR TERMINATION OF ACCESS TO OR USE OF THE SCAN API. All rights not expressly granted to you under these Terms are reserved by LayerZero and its licensors. ## 4. **Proprietary Rights** The Scan API, including, without limitation, its “look and feel” (e.g., text, graphics, images, logos), content, functionality, APIs, documentation, data, features, software, trademarks, service marks, copyrights, patents, and designs as well as any other proprietary content, information, and material (collectively, the “**Scan API IP**”), are the exclusive property of LayerZero and its related entities and are protected under copyright, trademark, and other intellectual property laws. You agree that LayerZero, its related entities, and/or its licensors exclusively own all right, title, and interest in and to the Scan API IP (including any and all intellectual property rights therein) and you agree not to take any action(s) inconsistent with such ownership interests. You agree not to remove, alter, or obscure any copyright, trademark, service mark, or other proprietary rights notices incorporated in or accompanying the Scan API. LayerZero reserves all rights in connection with the Scan API and its content. ## 5. **Additional Rights** LayerZero reserve the following rights: (a) with or without prior notice to you, to modify, substitute, eliminate, or add to the Scan API; (b) to review, modify, filter, disable, delete, and remove the Scan API, including any and all content and information associated with it; and (c) to cooperate with any law enforcement agency, court order, government investigation or order, or third party requesting and requiring, directing that we disclose information or content that you provide. ## 6. **Prohibited Activities** You agree not to engage in, or attempt to engage in, or do any of the following categories of prohibited activities in connection with your access and/or use of the Scan API, unless applicable laws or regulations prohibit these restrictions or you have our written permission to do so: (a) modify, copy, distribute, transmit, display, perform, reproduce, duplicate, publish, license, create derivative works from, or offer for sale any information contained on, or obtained from or through, the Scan API, except for temporary files that are automatically cached by your web browser for display purposes, or as otherwise expressly permitted in these Terms; (b) remove, alter, or obscure, any copyright, trademark, service mark, trade name, slogan, logo, image, or other proprietary notation displayed on or through the Scan API; (c) any activity that infringes on or violates any copyright, trademark, service mark, patent, right of publicity, right of privacy, or other proprietary or intellectual property rights under the law; (d) use automation software (bots), hacks, modifications (mods) or any other unauthorized third-party software designed to access, use, or modify the Scan API; (e) access or use the Scan API in any manner that could disable, overburden, damage, disrupt, degrade, or impair the Scan API or its performance, or interfere with any other party’s access to or use of the Scan API, including, without limitation, by exceeding any rate limit(s) imposed, or use any device, software, or routine that causes the same; (f) attempt to gain unauthorized access to, interfere with, damage, or disrupt the Scan API, or the computer systems or networks connected to the Scan API; (g) circumvent, remove, alter, deactivate, degrade, or thwart any technological measure or content protections of the Scan API or any of the computer systems, wallets, accounts, protocols, or networks connected to the Scan API; (h) use any robot, spider, crawlers, or other automatic device, process, software, or queries that intercepts, “mines,” scrapes, or otherwise accesses the Scan API to monitor, extract, copy, or collect information or data from or through the Scan API, or engage in any manual process to do the same; (i) introduce any viruses, trojan horses, worms, logic bombs, or other materials that are malicious or technologically harmful into our systems; (j) any activity that seeks to interfere with or compromise the integrity, security, or proper functioning of any computer, server, network, personal device, or other information technology system, including (but not limited to) the deployment of viruses and denial of service attacks; (k) impersonate any other person or entity using the Scan API, including by falsely stating, implying, or otherwise misrepresenting your affiliation with any person or entity; (l) any activity that seeks to defraud us or any other person or entity, including (but not limited to) providing any false, inaccurate, or misleading information in order to unlawfully obtain the property of another; (m) violate any applicable law, rule, or regulation of a relevant jurisdiction in connection with your access to or use of the Scan API, including, without limitation, any restrictions or regulatory requirements of Canada, the United States, or the British Virgin Islands; or (n) access or use the Scan API in any way not expressly permitted by these Terms. ## 7. **Feedback** You acknowledge and expressly agree that any contribution by you of any bug report, comment, idea, enhancement and/or enhancement request, recommendation, proposal, correction, suggestion for improvement(s), or other feedback of any kind, in any forum, with respect to the Scan API (**“Feedback”**) shall become the sole and exclusive property of LayerZero and does not and will not give or grant you any right, title, or interest in or to the Scan API, Scan API IP, or any such Feedback. You agree that LayerZero may use and disclose Feedback in any manner and for any purpose whatsoever without further notice or compensation to you, and without retention by you of any proprietary or other right or claim. You hereby irrevocably assign to LayerZero any and all right, title, and interest (including, but not limited to, any patent, copyright, trade secret, trademark, show-how, know-how, moral rights, and any and all other intellectual property right) that you may have in and to any and all Feedback, and, to the extent that any rights in and to Feedback cannot be assigned (including without limitation any moral rights), you hereby agree to waive such rights. To the extent that any Feedback is not assignable, you hereby grant to LayerZero a fully paid up, royalty-free, worldwide, perpetual, exclusive, irrevocable, sublicensable (with the right to sublicense through multiple tiers), and transferable right and license to use, create derivative works of, reproduce, re-format, perform, display, adapt, modify, distribute, and commercialize, or otherwise commercially or non-commercially exploit in any manner, any and all such Feedback for any purpose, including, but not limited to, by incorporating any Feedback into the Scan API. ## 8. **Privacy** All information collected on the Scan API is subject to our Privacy Policy. By using the Scan API, you consent to all actions taken by us with respect to any collection and/or use of your information in compliance with the Privacy Policy which further describes how we handle the information you provide to us when you use the Scan API. For an explanation of our privacy practices, visit our Privacy Policy located on the Scan API. Without limiting anything in the Privacy Policy, you acknowledge and agree that when you use the Scan API you may be interacting with public blockchains, which provide transparency into your transactions. LayerZero does not control and is not responsible for any information you make public on any public blockchain by taking actions through the Scan API. ## 9. **Third Party Services and Materials** The Scan API may display, include, reference, link to (including links to third-party websites), or otherwise make available services, products, promotions, content, data, information, resources, applications, or any other material(s) made available by a third-party or by third-parties (“**Third-Party Services and Materials**”). All Third-Party Services and Materials are made available solely as a convenience, and LayerZero does not own, control, or endorse any Third-Party Services and Materials. You agree that your access and use of such Third-Party Services and Materials is governed solely by the terms and conditions of such Third-Party Services and Materials, as applicable. LayerZero is not responsible or liable for, and makes no representations as to any aspect of such Third-Party Services and Materials, including, without limitation, their content, availability, or the manner in which they handle, protect, manage, or process data, or any interaction between you and the provider of such Third-Party Services and Materials. Any statements and/or opinions expressed by or through any Third-Party Services and Materials by any third-party or third-parties is/are solely the opinion(s) and the responsibility of the person or entity providing those materials. LayerZero is not responsible for examining or evaluating the content, accuracy, completeness, availability, timeliness, validity, copyright compliance, legality, decency, quality, or any other aspect of such Third-Party Services and Materials or websites. You irrevocably waive any claim against LayerZero with respect to such Third-Party Services and Materials. LayerZero is not liable for any damage or loss caused or alleged to be caused by or in connection with your enablement, access, or use of any such Third-Party Services and Materials, or your reliance on the privacy practices, data security processes, or other policies of such Third-Party Services and Materials. Third-Party Services and Materials and links to other websites are provided solely as a convenience to you and you access and/or use them at your own risk. ## 10. **Not Registered with FinCEN or Any Agency; No Advice Given** LayerZero is not registered with the Financial Crimes Enforcement Network as a money services business or in any other capacity. You understand and acknowledge that we are not a marketplace facilitator, a financial institution, broker, exchange, clearing house, or creditor, nor do we broker trading orders on your behalf or match orders for buyers and sellers of securities. While we provide infrastructure to transmit cross-chain messages via the LayerZero Protocol, we are not involved in executing or settling trades (all such activity takes place directly between users entirely on public distributed blockchains). You acknowledge and agree that all transfers, liquidity pooling, farming, staking, or any other actions you may undertake via the Scan API (or via any other platform based on information available on the Scan API) are unsolicited. This means you have not received, nor relied upon, any investment advice from us regarding such actions. Furthermore, we do not assess the suitability of any such actions for you. You alone are responsible for determining whether any investment, investment strategy, or related transaction is appropriate for you based on your personal investment objectives, financial circumstances, and risk tolerance. The Scan API does not provide, and does not purport to provide, any financial, investment, legal, or tax advice or services. Use of the Scan API should not be construed as an offer or solicitation to buy or sell any financial instrument or as a recommendation to engage in any transaction. You should consult with qualified professionals before making any financial decisions. The Scan API is intended solely as a technical interface to decentralized systems, and no fiduciary relationship is created between you and the Scan API or LayerZero. ## 11. **Scan API for General Information Purposes Only** All information provided on or through the Scan API is made available solely for general informational purposes. LayerZero does not warrant the accuracy, completeness, or usefulness of this information. Any reliance you place on such information is strictly at your own risk. LayerZero disclaims all liability and responsibility arising from any reliance placed on such materials by you or any other visitor to the Scan API, or by anyone who may be informed of any of its contents. ## 12. **No Warranties** THE SCAN API IS PROVIDED ON AN "AS-IS" AND "AS-AVAILABLE" BASIS, WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, OR NON-INFRINGEMENT. TO THE FULLEST EXTENT PERMITTED BY LAW, LAYERZERO WILL NOT BE LIABLE FOR ANY DAMAGES OF ANY KIND ARISING OUT OF OR RELATED TO YOUR ACCESS TO OR USE OF THE SCAN API, INCLUDING, BUT NOT LIMITED TO, ANY DIRECT, INDIRECT, INCIDENTAL, PUNITIVE, EXEMPLARY, SPECIAL, OR CONSEQUENTIAL DAMAGES, EVEN IF LAYERZERO HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. YOU ACKNOWLEDGE AND AGREE THAT YOUR ACCESS TO AND USE OF THE SCAN API WILL BE AT YOUR SOLE RISK, AND THAT LAYERZERO SHALL NOT BE LIABLE FOR ANY LOSS OR DAMAGE THAT MAY RESULT FROM YOUR ACCESS TO OR USE OF THE SCAN API, INCLUDING, BUT NOT LIMITED TO: YOUR INABILITY TO ACCESS OR USE THE SCAN API; MODIFICATION, SUSPENSION, OR TERMINATION OF THE SCAN API; ERRORS, OMISSIONS, INTERRUPTIONS, DELAYS, OR TRANSMISSION FAILURES; UNAUTHORIZED ACCESS TO OR ALTERATION OF ANY TRANSMISSION OR DATA; ANY TRANSACTION OR AGREEMENT ENTERED INTO THROUGH THE SCAN API; ANY ACTIVITIES, CONDUCT, CONTENT, OR COMMUNICATIONS OF THIRD PARTIES; OR ANY DATA OR MATERIAL OBTAINED FROM A THIRD PARTY SOURCE ON OR THROUGH THE SCAN API. LAYERZERO MAKES NO WARRANTIES OR REPRESENTATIONS REGARDING THE ACCURACY, COMPLETENESS, OR RELIABILITY OF THE SCAN API OR ANY CONTENT LINKED TO OR ACCESSED THROUGH IT. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, LAYERZERO EXPRESSLY DISCLAIMS ANY LIABILITY FOR ANY (1) ERRORS, MISTAKES, INACCURACIES, OR OMISSIONS IN ANY CONTENT OR MATERIALS, (2) PERSONAL INJURY OR PROPERTY DAMAGE, OF ANY NATURE WHATSOEVER, RESULTING FROM YOUR ACCESS TO AND/OR USE OF THE SCAN API, (3) UNAUTHORIZED ACCESS TO OR USE OF LAYERZERO’S SECURE SERVERS AND/OR ANY AND ALL PERSONAL OR FINANCIAL INFORMATION STORED THEREIN, (4) INTERRUPTION, SUSPENSION, CESSATION, OR TERMINATION OF TRANSMISSION TO OR FROM THE SCAN API, (5) BUGS, VIRUSES, TROJAN HORSES, OR OTHER HARMFUL CODE THAT MAY BE TRANSMITTED TO OR THROUGH THE SCAN API BY ANY THIRD PARTY, AND (6) LOSS OR DAMAGE INCURRED AS A RESULT OF THE USE OF ANY CONTENT OR MATERIALS MADE AVAILABLE THROUGH THE SCAN API, WHETHER POSTED, TRANSMITTED, OR OTHERWISE DISSEMINATED. IF YOU ARE DISSATISFIED WITH THE SCAN API, YOU AGREE THAT YOUR SOLE AND EXCLUSIVE REMEDY SHALL BE FOR YOU TO DISCONTINUE YOUR USE OF THE SCAN API. CERTAIN JURISDICTIONS DO NOT PERMIT THE EXCLUSION OR LIMITATION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES; AS SUCH, THE FOREGOING LIMITATIONS AND EXCLUSIONS MAY NOT APPLY TO YOU TO THE EXTENT PROHIBITED BY APPLICABLE LAW. ## 13. **Non-Custodial and No Fiduciary Duties** The Scan API is a non-custodial application. This means that you alone are responsible for managing and securing the private cryptographic keys associated with your digital asset wallets. These Terms are not intended to, and do not, create or impose any fiduciary duties on us. To the maximum extent permitted by applicable law, you acknowledge and agree that we owe no fiduciary duties or liabilities to you or to any other party. Any such duties or liabilities that may otherwise exist at law or in equity are hereby fully disclaimed and waived. You further agree that our only duties and obligations are those explicitly set forth in these Terms. ## 14. **Assumption of Risk** By accessing and using the Scan API, you represent that you are financially and technically sophisticated enough to understand the inherent risks associated with using cryptographic and blockchain-based systems, and that you have a working knowledge of the usage and intricacies of blockchain technologies, cryptocurrencies, and other digital assets, storage mechanisms, and blockchain-based software systems to be able to assess and evaluate the risks and benefits of the Scan API contemplated hereunder, and will bear the risks thereof, including loss of all amounts paid or stored, and the risk that the cryptocurrencies and other digital assets may have little or no value. You understand that blockchain-based transactions are irreversible. You acknowledge that there are inherent risks associated with using or interacting with public blockchains and blockchain technology. There is no guarantee that such technology will be available or not subject to errors, hacking, or other security risks. Blockchain protocols may also be subject to sudden changes in operating rules, including forks, and it is your responsibility to make yourself aware of upcoming operating changes. You acknowledge and agree that there are risks associated with purchasing and holding cryptocurrency. These include, but are not limited to, risk of losing access to cryptocurrency or digital assets due to slashing; loss of private key(s); custodial error or purchaser or user error; risk of mining, staking, or blockchain-related attacks; risk of hacking and security weaknesses; risk of unfavorable regulatory intervention in one or more jurisdictions; risk related to token taxation; risk of personal information disclosure; risk of uninsured losses; volatility risks; and unanticipated risks. You further understand that the markets for digital assets are highly volatile due to factors including (but not limited to) adoption, speculation, technology, security, and regulation. You acknowledge and accept that the cost and speed of transacting with cryptographic and blockchain-based systems are variable and may increase dramatically at any time. You further acknowledge and accept the risk that your digital assets may lose some or all of their value and that you may suffer loss due to the fluctuation of prices of tokens. You understand that anyone can create a token, including fake versions of existing tokens and tokens that falsely claim to represent projects, and acknowledge and accept the risk that you may mistakenly trade those or other tokens. You further acknowledge that we are not responsible for any of these variables or risks and cannot be held liable for any resulting losses that you experience while accessing or using the Scan API. Accordingly, you understand and agree to assume full responsibility for all of the risks of accessing and using the Scan API. You further acknowledge and agree that your access and use of the Scan API may be interrupted from time to time for any or for no reason, including, without limitation, in the event of the malfunction of equipment, periodic updating, maintenance or repair of the Scan API or other actions that LayerZero, in its sole discretion, may elect to take. You agree that we shall have no liability to you arising from or related to any inability to access or use the Scan API. ## 15. **Third-Party Beneficiaries** You and LayerZero acknowledge and agree that LayerZero’s affiliates, subsidiaries, related companies, service providers, and its and their officers, directors, supervisors, consultants, advisors, agents, representatives, partners, employees, and licensors are third party beneficiaries of these Terms. ## 16. **Release of Claims** You expressly agree that you assume all risks in connection with your access and use of the Scan API. You further expressly waive and release us from any and all liability, claims, causes of action, or damages arising from or in any way relating to your access or use of the Scan API. If you are a California resident, you waive the benefits and protections of California Civil Code § 1542, which provides: "\[a\] general release does not extend to claims that the creditor or releasing party does not know or suspect to exist in his or her favor at the time of executing the release and that, if known by him or her, would have materially affected his or her settlement with the debtor or released party." ## 17. **Indemnity** You agree to hold harmless, release, defend, and indemnify us, our affiliates, subsidiaries, related companies, service providers, and its and their officers, directors, employees, contractors, and agents from and against all claims, damages, obligations, losses, liabilities, costs, and expenses (including attorneys’ fees and costs) arising out of or in connection with: (a) your access and use, or misuse, of the Scan API; (b) your violation of any term or condition of these Terms, the right of any third party, or any other applicable law, rule, or regulation; (c) your dishonesty, negligence, fraudulence, or willful misconduct; and (d) any other party's access and use, or misuse, of the Scan API with your assistance or using any device or account that you own or control. ## 18. **Limitation of Liability** TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL LAYERZERO, OR ITS OFFICERS, DIRECTORS, EMPLOYEES, CONTRACTORS, AGENTS, AFFILIATES, OR SUBSIDIARIES, BE LIABLE TO YOU FOR ANY INDIRECT, PUNITIVE, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR EXEMPLARY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA, OR OTHER INTANGIBLE LOSSES, ARISING OUT OF OR RELATING TO YOUR ACCESS TO OR USE OF THE SCAN API, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. LAYERZERO SHALL NOT BE LIABLE FOR ANY DAMAGE, LOSS, OR INJURY RESULTING FROM HACKING, TAMPERING, OR OTHER UNAUTHORIZED ACCESS TO OR USE OF THE SCAN API OR THE INFORMATION CONTAINED THEREIN. WITHOUT LIMITING THE FOREGOING, LAYERZERO ASSUMES NO LIABILITY OR RESPONSIBILITY FOR ANY: (A) ERRORS, MISTAKES, INACCURACIES, OR OMISSIONS IN ANY CONTENT OR MATERIALS; (B) PERSONAL INJURY OR PROPERTY DAMAGE, OF ANY NATURE WHATSOEVER, RESULTING FROM ANY ACCESS TO OR USE OF THE SCAN API; (C) UNAUTHORIZED ACCESS TO OR USE OF ANY SECURE SERVER OR DATABASE UNDER OUR CONTROL, OR ANY DATA STORED THEREIN; (D) INTERRUPTION OR CESSATION OF ANY FUNCTION RELATED TO THE SCAN API; (E) BUGS, VIRUSES, TROJAN HORSES, OR OTHER HARMFUL CODE THAT MAY BE TRANSMITTED VIA THE SCAN API; (F) ANY CONTENT MADE AVAILABLE THROUGH THE SCAN API, OR ANY LOSS OR DAMAGE ARISING FROM ITS USE; OR (G) THE DEFAMATORY, OFFENSIVE, OR UNLAWFUL CONDUCT OF ANY THIRD PARTY. IN NO EVENT SHALL LAYERZERO’S, OR ANY OF ITS OFFICERS, DIRECTORS, EMPLOYEES, CONTRACTORS, AGENTS, AFFILIATES, OR SUBSIDIARIES, TOTAL AGGREGATE LIABILITY TO YOU FOR ANY AND ALL CLAIMS, PROCEEDINGS, LIABILITIES, OBLIGATIONS, DAMAGES, LOSSES, OR COSTS EXCEED THE AMOUNT YOU PAID TO US IN EXCHANGE FOR ACCESS TO AND USE OF THE SCAN API, OR USD$50.00, WHICHEVER IS GREATER. THIS LIMITATION OF LIABILITY APPLIES REGARDLESS OF THE FORM OF ACTION, WHETHER BASED IN CONTRACT, TORT, NEGLIGENCE, STRICT LIABILITY, OR OTHERWISE, AND EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH LIABILITY. CERTAIN JURISDICTIONS DO NOT PERMIT THE EXCLUSION OR LIMITATION OF CERTAIN WARRANTIES OR LIABILITIES. ACCORDINGLY, CERTAIN OF THE FOREGOING DISCLAIMERS AND LIMITATIONS MAY NOT APPLY TO YOU TO THE EXTENT PROHIBITED BY APPLICABLE LAW. THIS LIMITATION OF LIABILITY SHALL APPLY TO THE FULLEST EXTENT PERMITTED BY LAW. ## 19. **Dispute Resolution** a. **READ THIS SECTION CAREFULLY** – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT AND TO HAVE A JURY HEAR YOUR CLAIMS. IT CONTAINS PROCEDURES FOR MANDATORY BINDING ARBITRATION AND A CLASS ACTION WAIVER. b. **Informal Process First.** You and LayerZero agree that in the event of any dispute, claim, or controversy arising out of or relating to these Terms or the breach, termination, enforcement, interpretation, or validity thereof or the access and use of the Scan API (individually, a “Dispute”, and collectively, the “**Disputes**”) between you and LayerZero, you must contact us by sending an email to notices@layerzerolabs.org. You and LayerZero agree to make a good faith sustained effort to resolve any Dispute before resorting to more formal means of resolution, including without limitation, any court action. Both you and LayerZero agree that this dispute resolution procedure is a condition precedent which must be satisfied before initiating any arbitration against the other party. Nothing in this clause shall prevent a party from seeking interim or provisional relief where it is reasonably necessary to do so. c. Mandatory Arbitration of Disputes. If the informal dispute resolution process should fail to produce a satisfactory result within sixty (60) days of your email, or if any Dispute or portion thereof remains unresolved following such process, we each agree that such Dispute will be resolved by binding, individual arbitration pursuant to the following provisions of this “Dispute Resolution” clause, and not in a class, representative, or consolidated action or proceeding. You and LayerZero agree that British Virgin Islands law governs the interpretation and enforcement of these Terms. This arbitration provision shall survive termination of these Terms. e. **Exceptions.** As limited exceptions to the provisions of this “Dispute Resolution” clause: (i) we both may seek to resolve a Dispute in the Magistrate’s Court of the British Virgin Islands if it qualifies; and (ii) we each retain the right to seek injunctive or other equitable relief from a court to prevent (or enjoin) the infringement or misappropriation of our intellectual property rights. Conducting Arbitration and Arbitration Rules. Any Disputes arising out of or relating to these Terms, including the existence, validity, interpretation, performance, breach, or termination thereof, or any Dispute regarding non-contractual obligations arising out of or relating to them, shall be referred to and finally resolved by binding arbitration to be administered by the BVI International Arbitration Centre (“BVI IAC”) in accordance with the BVI IAC Arbitration Rules (the “Arbitration Rules”) in force as at the date of these Terms, which Arbitration Rules are deemed to be incorporated by reference into these Terms. The arbitration shall be conducted in the English language and the seat of arbitration shall be in Road Town, Tortola, British Virgin Islands. The arbitration shall be determined by a sole arbitrator to be appointed in accordance with the Arbitration Rules. The decision of the sole arbitrator shall be in writing and shall be final and binding upon both parties without any right of appeal, and judgment upon any award thus obtained may be entered in or enforced by any court having jurisdiction thereof. No action at law or in equity based upon any claim arising out of or in relation to these Terms shall be instituted in any court of any jurisdiction, except as specifically permitted herein. Each party waives any right it may have to assert the doctrine of forum non conveniens, to assert that it is not subject to the jurisdiction of such arbitration or courts, or to object to venue, to the extent any proceeding is brought in accordance herewith. f. **Arbitration Costs.** Responsibility for payment of all filing, administration, and arbitrator fees will be governed by the Arbitration Rules. We each agree that the prevailing party in arbitration will be entitled to an award of attorneys’ fees and expenses to the extent provided under applicable law. g. **Injunctive and Declaratory Relief.** Except as provided in the “Exceptions” section above, the arbitrator shall determine all issues of liability on the merits of any claim asserted by either party and may award declaratory or injunctive relief only in favor of the individual party seeking relief and only to the extent necessary to provide relief warranted by that party’s individual claim. h. **Class Action and Jury Trial Waiver.** **YOU AND LAYERZERO AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS ACTION, COLLECTIVE ACTION, PRIVATE ATTORNEY GENERAL ACTION, OR OTHER REPRESENTATIVE PROCEEDING.** Further, if the parties’ Dispute is resolved through arbitration, the arbitrator may not consolidate another person’s claims with your claims, and may not otherwise preside over any form of a representative or class proceeding. If this specific provision is found to be unenforceable, then the entirety of this Dispute Resolution section shall be null and void. You and we both agree to waive the right to demand a trial by jury. i. **Severability.** With the exception of any of the provisions in the immediately preceding paragraph of these Terms (“**Class Action Waiver**”), if an arbitrator or court of competent jurisdiction decides that any part of these Terms is invalid or unenforceable, the other parts of these Terms will still apply. ## 20. **Injunctive Relief** You agree that a breach of these Terms will cause irreparable injury to LayerZero for which monetary damages would not be an adequate remedy and LayerZero shall be entitled to equitable relief in addition to any remedies it may have hereunder or at law without a bond, other security or proof of damages. ## 21. **Force Majeure** We will not be liable or responsible to you, nor be deemed to have defaulted under or breached these Terms, for any failure or delay in fulfilling or performing any of our obligations under these Terms, when and to the extent such failure or delay is caused by or results from any events beyond our ability to control, including acts of God; flood, fire, earthquake, epidemics, pandemics, tsunami, explosion, war, invasion, hostilities (whether war is declared or not), terrorist threats or acts, riot or other civil unrest, government order, law, or action, embargoes or blockades, strikes, labor stoppages or slowdowns or other industrial disturbances, shortage of adequate or suitable Internet connectivity, telecommunication breakdown or shortage of adequate power or electricity, cyberattacks, Protocol-level disruptions, chain-level failures, governance attacks, and other similar events beyond our control. ## 22. **Miscellaneous** If any provision of these Terms shall be unlawful, void or for any reason unenforceable, then that provision shall be deemed severable from these Terms and shall not affect the validity and enforceability of any remaining provisions. These Terms and the licenses granted hereunder may be assigned by LayerZero but may not be assigned by you without the prior express written consent of LayerZero. LayerZero’s failure to enforce any right or provision of these Terms will not be considered a waiver of such right or provision. The waiver of any such right or provision will be effective only if in writing and signed by a duly authorized representative of LayerZero. Except as expressly set forth in these Terms, the exercise by either party of any of its remedies under these Terms will be without prejudice to its other remedies under these Terms or otherwise. The section headings used herein are for reference only and shall not be read to have any legal effect. ## 23. **Governing Law** You agree that the laws of the British Virgin Islands, without regard to principles of conflict of laws, govern these Terms and any Dispute between you and us. You further agree that the Scan API shall be deemed to be based solely in the British Virgin Islands, and that although the Scan API may be available in other jurisdictions, its availability does not give rise to general or specific personal jurisdiction in any forum outside the British Virgin Islands. You agree that the courts of the British Virgin Islands are the proper forum for any appeals of an arbitration award or for court proceedings in the event that the binding arbitration clause of these Terms is found to be unenforceable. ## 24. **Entire Agreement** These Terms, and the Privacy Policy, constitute the entire agreement between you and us with respect to the subject matter hereof. These Terms supersedes any and all prior or contemporaneous written and oral agreements, communications and other understandings (if any) relating to the subject matter of the terms. The information provided on the Scan API is not intended for distribution to or use by any person or entity in any jurisdiction or country where such distribution or use would be contrary to law or regulation or which would subject us to any registration requirement within such jurisdiction or country. Accordingly, those persons who choose to use or access the Scan API from other locations do so on their own initiative and are solely responsible for compliance with local laws, if and to the extent local laws are applicable. --- --- title: Message Execution Options description: >- A comprehensive reference for LayerZero message execution options across all supported chains. --- 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. **Message Execution Options** provide a standardized way to specify the execution requirements for transactions on the destination chain. You can think of `options` as serialized requests in `bytes` that inform the off-chain infrastructure (`DVNs` and `Executors`) how to handle the execution of your message on the destination chain. See [Message Options](../../concepts/message-options.md) for more details on why Options exist in the LayerZero protocol. ## Options Builders LayerZero provides tools to build specific Message Execution Options for your application: ### EVM - `OptionsBuilder.sol`: Can be imported from [`@layerzerolabs/oapp-evm`](https://www.npmjs.com/package/@layerzerolabs/oapp-evm) - `options.ts`: Can be imported from [`@layerzerolabs/lz-v2-utilities`](https://www.npmjs.com/package/@layerzerolabs/lz-v2-utilities) ### Aptos & Solana - `options.ts`: Can be imported from [`@layerzerolabs/lz-v2-utilities`](https://www.npmjs.com/package/@layerzerolabs/lz-v2-utilities) ## Generating Options ### EVM (Solidity) ```solidity using OptionsBuilder for bytes; bytes memory options = OptionsBuilder.newOptions() .addExecutorLzReceiveOption(50000, 0) .toBytes(); ``` ### All Chains (TypeScript) ```typescript import {Options} from '@layerzerolabs/lz-v2-utilities'; const options = Options.newOptions().addExecutorLzReceiveOption(gas_limit, msg_value).toBytes(); ``` ## Option Types ### `lzReceive` Option Specifies the gas values the Executor uses when calling `lzReceive` on the destination chain. ```typescript Options.newOptions().addExecutorLzReceiveOption(gas_limit, msg_value); ``` ### `lzRead` Option Specifies the gas values and response data size the Executor uses when delivering lzRead responses. :::caution Since the return data size is not known to the Executor ahead of time, you must estimate the expected response data size. This size is priced into the Executor's fee formula. Failure to correctly estimate the return data size will result in the Executor not delivering the response. ::: ```typescript Options.newOptions().addExecutorLzReadOption(gas_limit, return_data_size, msg_value); ``` Parameters: - `gas_limit`: The amount of gas for delivering the lzRead response - `return_data_size`: The estimated size (in bytes) of the response data from the read operation - `msg_value`: The `msg.value` for the call ### `lzCompose` Option Allocates gas and value for **Composed Messages** on the destination chain. ```typescript Options.newOptions().addExecutorLzComposeOption(index, gas_limit, msg_value); ``` Parameters: - `_index`: The index of the `lzCompose()` function call - `_gas`: The gas amount for the lzCompose call - `_value`: The `msg.value` for the call ### `lzNativeDrop` Option Specifies how much native gas to drop to any address on the destination chain. ```typescript Options.newOptions().addExecutorNativeDropOption(amount, receiverAddressInBytes32); ``` Parameters: - `_amount`: The amount of gas in wei/lamports to drop - `_receiver`: The `bytes32` representation of the receiver address ### `OrderedExecution` Option Enables ordered message delivery, overriding the default unordered delivery. ```typescript Options.newOptions().addExecutorOrderedExecutionOption(''); ``` ## Chain-Specific Considerations ### EVM Chains - Gas values are specified in wei - Gas costs vary by chain and opcode pricing ### Aptos - Gas units are similar to EVM but may have different costs - Recommended starting gas limit: 1,500 units for `lzReceive` - Uses APT as native token ### Solana - Uses compute units instead of gas - For SPL token ATAs, rent-exempt minimum is 0.00203928 SOL (2,039,280 lamports); Token-2022 accounts may require more depending on enabled extensions - Native token drops are in lamports - Programs pull SOL from sender's account rather than pushing with transaction - Prefer per-tx `extraOptions` with `gas=0` and non-zero `msg.value` only if the recipient’s [Associated Token Account (ATA)](https://www.alchemy.com/overviews/associated-token-account) is missing; enforce gas via app-level `enforcedOptions` (options are combined). See [Solana OFT: Conditional msg.value for ATA creation](../../developers/solana/oft/overview.md#conditional-msgvalue-for-ata-creation). ## Determining Gas Costs ### Tenderly For supported chains, the [Tenderly Gas Profiler](https://dashboard.tenderly.co/explorer) can help determine optimal gas values: 1. Deploy and test your contract 2. Use Tenderly to profile actual gas usage 3. Set your options slightly above the profiled amount ### Testing Always test your gas settings thoroughly: 1. Start with conservative estimates 2. Profile actual usage 3. Adjust based on real-world performance 4. Consider chain-specific gas mechanisms ## Best Practices 1. **Gas Profiling**: Always profile your contract's gas usage on each target chain 2. **Conservative Estimates**: Start with higher gas limits and adjust down 3. **Chain-Specific Testing**: Test thoroughly on each target chain 4. **Native Caps**: Check Executor's native cap for each pathway 5. **Multiple Options**: Consider combining options for complex scenarios ## Further Reading - [EVM Gas Documentation](https://ethereum.org/en/developers/docs/gas/) - [Aptos Gas Fees](https://aptos.dev/en/network/blockchain/gas-txn-fee) - [Solana Compute Units](https://solana.com/docs/core/fees) - [LayerZero Executors](../../concepts/permissionless-execution/executors.md) --- --- title: LayerZero Solana SDK --- ### Package Use the `@layerzerolabs/lz-solana-sdk-v2` package to interact with the LayerZero Endpoint program on Solana from TypeScript/JavaScript. ### Interacting with the Endpoint Note that the SDK makes use of [`Umi`](https://developers.metaplex.com/umi) in place of `@solana/web3.js` Create an `endpoint` instance: ```ts import {TransactionBuilder, publicKey as umiPublicKey} from '@metaplex-foundation/umi'; import {EndpointProgram} from '@layerzerolabs/lz-solana-sdk-v2/umi'; const endpoint = new EndpointProgram.Endpoint(EndpointProgram.ENDPOINT_PROGRAM_ID); ``` Note: If the payload account is missing in some flows, call `endpoint.initVerify(umiWalletSigner, { srcEid, sender, receiver, nonce })` before `skip` or `clear`. #### Skip a message `endpoint.skip(umiWalletSigner, { sender, receiver, srcEid, nonce })` - **When to use**: Bypass a stuck inbound message at a future nonce to unblock subsequent processing. - **Preconditions**: - `nonce > inboundNonce` - `nonce <= inboundNonce + 256` (sliding window) - If the payload account is missing, call `initVerify` first - Caller is the authorized delegate ```ts const skipIxn = endpoint.skip(umiWalletSigner, { sender: senderBytes32, // bytes32 normalized sender receiver: umiPublicKey(''), srcEid: , nonce: BigInt(), }) await new TransactionBuilder([skipIxn]).sendAndConfirm(umi) ``` Example usage: https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/solana/endpoint/skip.ts #### Nilify a nonce `endpoint.oAppNilify(umiWalletSigner, { nonce, receiver, sender, srcEid, payloadHash })` - **When to use**: Invalidate a verified payload by setting its payload hash to NIL without deleting the account. - **Preconditions**: - Provide the exact `payloadHash` (must match on-chain) - Typically after verification; does not create the payload account - Caller is the authorized delegate ```ts const nilifyIxn = endpoint.oAppNilify(umiWalletSigner, { nonce: BigInt(), receiver: umiPublicKey(''), sender: senderBytes32, srcEid: , payloadHash: payloadHashBytes32, }) await new TransactionBuilder([nilifyIxn]).sendAndConfirm(umi) ``` Example usage: https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/solana/endpoint/nilify.ts #### Burn a nonce `endpoint.oAppBurnNonce(umiWalletSigner, { nonce, receiver, sender, srcEid, payloadHash })` - **When to use**: Delete the payload hash account for an older nonce after inbound processing has advanced beyond it (state cleanup). - **Preconditions**: - `nonce < inboundNonce` - Provide the exact `payloadHash` (must match on-chain) - Caller is the authorized delegate ```ts const burnIxn = endpoint.oAppBurnNonce(umiWalletSigner, { nonce: BigInt(), receiver: umiPublicKey(''), sender: senderBytes32, srcEid: , payloadHash: payloadHashBytes32, }) await new TransactionBuilder([burnIxn]).sendAndConfirm(umi) ``` Example usage: https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/solana/endpoint/burn.ts #### Clear a payload Note that `clear` does not make use of the `endpoint` class, but instead requires usage of `EndpointProgram.instruction`. `EndpointProgram.instructions.clear({ programs }, { accounts }, { args })` - **When to use**: Finalize/ack a payload for a nonce that has already been verified; clean up state for a known payload. - **Preconditions**: - `nonce <= inboundNonce` - Provide `payloadHash` OR `guid + message` (to derive the hash) - If payload account is missing, call `initVerify` first - Caller is the authorized delegate ```ts // Derive PDAs const [endpointPda] = endpoint.pda.setting() const [noncePda] = endpoint.pda.nonce(umiPublicKey(''), , senderBytes32) const [oappRegistryPda] = endpoint.pda.oappRegistry(umiPublicKey('')) const [payloadHashPda] = endpoint.pda.payloadHash(umiPublicKey(''), , senderBytes32, Number()) const clearIxn = EndpointProgram.instructions.clear( { programs: endpoint.programRepo }, { signer: umiWalletSigner, oappRegistry: oappRegistryPda, nonce: noncePda, payloadHash: payloadHashPda, endpoint: endpointPda, eventAuthority: endpoint.eventAuthority, program: endpoint.programId, }, { receiver: umiPublicKey(''), srcEid: , sender: senderBytes32, nonce: BigInt(), guid: guidBytes32, message: messageBytes, } ).items[0] await new TransactionBuilder([clearIxn]).sendAndConfirm(umi) ``` Example usage: https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/tasks/solana/endpoint/clear.ts --- --- id: endpoint-metadata title: 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. --- --- 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. --- 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)](../concepts/glossary#dead-dvn)**. These contracts are placeholders used when the default LayerZero config is inactive and will require the OApp owner to manually configure the contract's config to use the pathway. | ## 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 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. :::

### Next Steps Add these DVNs to your OApp configuration by following the CLI Guide or an appropriate quickstart: - [CLI Guide](../get-started/create-lz-oapp/start.md) - [OApp Quickstart](../developers/evm/oapp/overview.md) - [OFT Quickstart](../developers/evm/oft/quickstart.md) - [lzRead Quickstart](../developers/evm/lzread/overview.md) --- --- sidebar_label: LayerZero Read Data Channels title: Read Data Channels description: See a full list of all the blockchains LayerZero currently supports. --- 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. ::: ### Next Steps Add these DVNs to your OApp configuration by following the CLI Guide or an appropriate quickstart: - [CLI Guide](../get-started/create-lz-oapp/start.md) - [OApp Quickstart](../developers/evm/oapp/overview.md) - [OFT Quickstart](../developers/evm/oft/quickstart.md) - [lzRead Quickstart](../developers/evm/lzread/overview.md) --- --- title: Abstract Mainnet sidebar_label: Abstract Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Animechain Mainnet sidebar_label: Animechain Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Ape Mainnet sidebar_label: Ape Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Apex Fusion Nexus Mainnet sidebar_label: Apex Fusion Nexus Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Aptos sidebar_label: Aptos hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Arbitrum Mainnet sidebar_label: Arbitrum Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Arbitrum Nova Mainnet sidebar_label: Arbitrum Nova Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Astar Mainnet sidebar_label: Astar Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Astar zkEVM Mainnet sidebar_label: Astar zkEVM Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Avalanche Mainnet sidebar_label: Avalanche Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Bahamut Mainnet sidebar_label: Bahamut Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Base Mainnet sidebar_label: Base Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Beam Mainnet sidebar_label: Beam Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Berachain Mainnet sidebar_label: Berachain Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Bevm Mainnet sidebar_label: Bevm Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Bitlayer Mainnet sidebar_label: Bitlayer Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Blast Mainnet sidebar_label: Blast Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: BNB Smart Chain (BSC) Mainnet sidebar_label: BNB Smart Chain (BSC) Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: BOB Mainnet sidebar_label: BOB Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Bouncebit Mainnet sidebar_label: Bouncebit Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Botanix sidebar_label: Botanix hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Canto Mainnet sidebar_label: Canto Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Celo Mainnet sidebar_label: Celo Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Codex Mainnet sidebar_label: Codex Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Concrete sidebar_label: Concrete hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Conflux eSpace Mainnet sidebar_label: Conflux eSpace Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: CoreDAO Mainnet sidebar_label: CoreDAO Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Corn Mainnet sidebar_label: Corn Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Cronos EVM Mainnet sidebar_label: Cronos EVM Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Cronos zkEVM Mainnet sidebar_label: Cronos zkEVM Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Cyber Mainnet sidebar_label: Cyber Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Degen Mainnet sidebar_label: Degen Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Dexalot Subnet Mainnet sidebar_label: Dexalot Subnet Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: DFK Chain sidebar_label: DFK Chain hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: DM2 Verse Mainnet sidebar_label: DM2 Verse Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: DOS Chain Mainnet sidebar_label: DOS Chain Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: EDU Chain Mainnet sidebar_label: EDU Chain Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Ethereum Mainnet sidebar_label: Ethereum Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Etherlink Mainnet sidebar_label: Etherlink Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: EVM on Flow Mainnet sidebar_label: EVM on Flow Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Fantom Mainnet sidebar_label: Fantom Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Flare Mainnet sidebar_label: Flare Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Fraxtal Mainnet sidebar_label: Fraxtal Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Fuse Mainnet sidebar_label: Fuse Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Glue Mainnet sidebar_label: Glue Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Gnosis Mainnet sidebar_label: Gnosis Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Goat Mainnet sidebar_label: Goat Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Gravity Mainnet sidebar_label: Gravity Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Gunz Mainnet sidebar_label: Gunz Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Harmony Mainnet sidebar_label: Harmony Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Hedera Mainnet sidebar_label: Hedera Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Hemi Mainnet sidebar_label: Hemi Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Homeverse Mainnet sidebar_label: Homeverse Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Humanity Mainnet sidebar_label: Humanity Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Horizen EON Mainnet sidebar_label: Horizen EON Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Hubble Mainnet sidebar_label: Hubble Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: HyperEVM Mainnet sidebar_label: HyperEVM Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: inEVM Mainnet sidebar_label: inEVM Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Initia Mainnet sidebar_label: Initia Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Ink Mainnet sidebar_label: Ink Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Iota Mainnet sidebar_label: Iota Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Japan Open Chain Mainnet sidebar_label: Japan Open Chain Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Kaia Mainnet (formerly Klaytn) sidebar_label: Kaia Mainnet (formerly Klaytn) hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Kava Mainnet sidebar_label: Kava Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Katana sidebar_label: Katana hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Lens Mainnet sidebar_label: Lens Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Lightlink Mainnet sidebar_label: Lightlink Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Linea Mainnet sidebar_label: Linea Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Lisk Mainnet sidebar_label: Lisk Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Loot Mainnet sidebar_label: Loot Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Lyra Mainnet sidebar_label: Lyra Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Manta Pacific Mainnet sidebar_label: Manta Pacific Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Mantle Mainnet sidebar_label: Mantle Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Merlin Mainnet sidebar_label: Merlin Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Meter Mainnet sidebar_label: Meter Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Metis Mainnet sidebar_label: Metis Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Mode Mainnet sidebar_label: Mode Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Moonbeam Mainnet sidebar_label: Moonbeam Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Moonriver Mainnet sidebar_label: Moonriver Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Morph Mainnet sidebar_label: Morph Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Movement Mainnet sidebar_label: Movement Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Near Aurora Mainnet sidebar_label: Near Aurora Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Nibiru Mainnet sidebar_label: Nibiru Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: OKX Mainnet sidebar_label: OKX Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: opBNB Mainnet sidebar_label: opBNB Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Optimism Mainnet sidebar_label: Optimism Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Orderly Mainnet sidebar_label: Orderly Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Otherworld Space Mainnet sidebar_label: Otherworld Space Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Peaq Mainnet sidebar_label: Peaq Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Plume Mainnet sidebar_label: Plume Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Polygon Mainnet sidebar_label: Polygon Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Polygon zkEVM Mainnet sidebar_label: Polygon zkEVM Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Rari Chain Mainnet sidebar_label: Rari Chain Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: re.al Mainnet sidebar_label: re.al Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Reya Mainnet sidebar_label: Reya Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Rootstock Mainnet sidebar_label: Rootstock Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Sanko Mainnet sidebar_label: Sanko Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Scroll Mainnet sidebar_label: Scroll Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Sei Mainnet sidebar_label: Sei Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Shimmer Mainnet sidebar_label: Shimmer Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Silicon Mainnet sidebar_label: Silicon Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Skale Mainnet sidebar_label: Skale Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Solana Mainnet sidebar_label: Solana Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Soneium Mainnet sidebar_label: Soneium Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Sonic Mainnet sidebar_label: Sonic Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Sophon Mainnet sidebar_label: Sophon Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Somnia Mainnet sidebar_label: Somnia Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Story Mainnet sidebar_label: Story Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Subtensor EVM Mainnet sidebar_label: Subtensor EVM Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Superposition Mainnet sidebar_label: Superposition Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Swell Mainnet sidebar_label: Swell Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Tac sidebar_label: Tac hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Taiko Mainnet sidebar_label: Taiko Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: TelosEVM Mainnet sidebar_label: TelosEVM Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Tenet Mainnet sidebar_label: Tenet Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Tiltyard Mainnet sidebar_label: Tiltyard Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: TON Mainnet sidebar_label: TON Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Tron Mainnet sidebar_label: Tron Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Unichain Mainnet sidebar_label: Unichain Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Vana Mainnet sidebar_label: Vana Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Viction Mainnet sidebar_label: Viction Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Worldchain Mainnet sidebar_label: Worldchain Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: X Layer Mainnet sidebar_label: X Layer Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Xai Mainnet sidebar_label: Xai Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: XChain Mainnet sidebar_label: XChain Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: XDC Mainnet sidebar_label: XDC Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: XPLA Mainnet sidebar_label: XPLA Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Zircuit Mainnet sidebar_label: Zircuit Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: zkLink Mainnet sidebar_label: zkLink Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: zkSync Era Mainnet sidebar_label: zkSync Era Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Zora Mainnet sidebar_label: Zora Mainnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Aptos Testnet sidebar_label: Aptos Testnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Arbitrum Sepolia Testnet sidebar_label: Arbitrum Sepolia Testnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Avalanche Fuji Testnet sidebar_label: Avalanche Fuji Testnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Base Sepolia Testnet sidebar_label: Base Sepolia Testnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Berachain Bepolia Testnet sidebar_label: Berachain Bepolia Testnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: BNB Smart Chain (BSC) Testnet sidebar_label: BNB Smart Chain (BSC) Testnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Ethereum Holesky Testnet sidebar_label: Ethereum Holesky Testnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Ethereum Sepolia Testnet sidebar_label: Ethereum Sepolia Testnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Initia Testnet sidebar_label: Initia Testnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Monad Testnet sidebar_label: Monad Testnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Optimism Sepolia Testnet sidebar_label: Optimism Sepolia Testnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Polygon Amoy Testnet sidebar_label: Polygon Amoy Testnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Solana Devnet sidebar_label: Solana Devnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: Tron Testnet sidebar_label: Tron Testnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- title: zkSync Sepolia Testnet sidebar_label: zkSync Sepolia Testnet hide_title: true --- import ChainDetails from '../../../src/components/ChainDetails'; --- --- 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](../../get-started/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](../../get-started/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](../../get-started/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: Apex Fusion Nexus Mainnet OFT Quickstart sidebar_label: Apex Fusion Nexus Mainnet OFT Quickstart description: How to get started building on Apex Fusion Nexus 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 **Apex Fusion Nexus 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 // Apex Fusion Nexus Mainnet (EID=30384) 'apexfusionnexus-mainnet': { eid: EndpointId.APEXFUSIONNEXUS_V2_MAINNET, url: process.env.RPC_URL_APEXFUSIONNEXUS || '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 apexfusionnexusContract: OmniPointHardhat = { eid: EndpointId.APEXFUSIONNEXUS_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 <-> apexfusionnexus // apexfusionnexus <-> 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. apexfusionnexus) apexfusionnexusContract, // 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 → apexfusionnexus, confirmations for apexfusionnexus → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → apexfusionnexus, options for apexfusionnexus → 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: apexfusionnexusContract}], 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 apexfusionnexus ``` 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 **Apex Fusion Nexus 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]=apexfusionnexus&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](../../get-started/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=apexfusionnexus) and [Executor](../deployed-contracts.md?chains=apexfusionnexus) 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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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 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](../../get-started/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: 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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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 Testnet OFT Quickstart sidebar_label: EVM on Flow Testnet OFT Quickstart description: How to get started building on EVM on Flow 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 **EVM on Flow 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 // EVM on Flow Testnet (EID=40351) 'flow-testnet': { eid: EndpointId.FLOW_V2_TESTNET, url: process.env.RPC_URL_FLOW || 'https://testnet.evm.nodes.onflow.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 flowTestnetContract: OmniPointHardhat = { eid: EndpointId.FLOW_V2_TESTNET, contractName: 'MyOFT', }; const optsepTestnetContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism Sepolia <-> flow Testnet // flow Testnet <-> Optimism Sepolia // 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 Sepolia) optsepTestnetContract, // 2) Chain A's contract (e.g. flow Testnet) flowTestnetContract, // 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 → flow Testnet, confirmations for flow Testnet → Optimism Sepolia] [1, 1], // 5) Enforced execution options: // [options for Optimism Sepolia → flow Testnet, options for flow Testnet → Optimism Sepolia] [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: optsepTestnetContract}, {contract: flowTestnetContract}], 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-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 **EVM on Flow 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]=flow-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](../../get-started/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-testnet) and [Executor](../deployed-contracts.md?chains=flow-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: 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](../../get-started/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](../../get-started/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 Testnet OFT Quickstart sidebar_label: Flare Testnet OFT Quickstart description: How to get started building on Flare 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 **Flare 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 // Flare Testnet (EID=40294) 'flare-testnet': { eid: EndpointId.FLARE_V2_TESTNET, url: process.env.RPC_URL_FLARE || 'https://coston2-api.flare.network/ext/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 flareTestnetContract: OmniPointHardhat = { eid: EndpointId.FLARE_V2_TESTNET, contractName: 'MyOFT', }; const optsepTestnetContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism Sepolia <-> flare Testnet // flare Testnet <-> Optimism Sepolia // 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 Sepolia) optsepTestnetContract, // 2) Chain A's contract (e.g. flare Testnet) flareTestnetContract, // 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 → flare Testnet, confirmations for flare Testnet → Optimism Sepolia] [1, 1], // 5) Enforced execution options: // [options for Optimism Sepolia → flare Testnet, options for flare Testnet → Optimism Sepolia] [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: optsepTestnetContract}, {contract: flareTestnetContract}], 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-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 **Flare 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]=flare-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](../../get-started/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-testnet) and [Executor](../deployed-contracts.md?chains=flare-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: 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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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 Testnet OFT Quickstart sidebar_label: Hedera Testnet OFT Quickstart description: How to get started building on Hedera 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 **Hedera Testnet** and any other supported chain. :::caution The Hedera EVM has 8 decimals while their JSON RPC uses 18 decimals for `msg.value`, please take precaution when calling `quoteFee`. ::: ## 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 Testnet (EID=40285) 'hedera-testnet': { eid: EndpointId.HEDERA_V2_TESTNET, url: process.env.RPC_URL_HEDERA || 'https://testnet.hashio.io/api', 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 hederaTestnetContract: OmniPointHardhat = { eid: EndpointId.HEDERA_V2_TESTNET, contractName: 'MyOFT', }; const optsepTestnetContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism Sepolia <-> hedera Testnet // hedera Testnet <-> Optimism Sepolia // 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 Sepolia) optsepTestnetContract, // 2) Chain A's contract (e.g. hedera Testnet) hederaTestnetContract, // 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 → hedera Testnet, confirmations for hedera Testnet → Optimism Sepolia] [1, 1], // 5) Enforced execution options: // [options for Optimism Sepolia → hedera Testnet, options for hedera Testnet → Optimism Sepolia] [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: optsepTestnetContract}, {contract: hederaTestnetContract}], 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-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 **Hedera 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]=hedera-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](../../get-started/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-testnet) and [Executor](../deployed-contracts.md?chains=hedera-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: 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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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: Humanity Mainnet OFT Quickstart sidebar_label: Humanity Mainnet OFT Quickstart description: How to get started building on Humanity 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 **Humanity 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 // Humanity Mainnet (EID=30382) 'humanity-mainnet': { eid: EndpointId.HUMANITY_V2_MAINNET, url: process.env.RPC_URL_HUMANITY || 'https://humanity-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 humanityContract: OmniPointHardhat = { eid: EndpointId.HUMANITY_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 <-> Humanity Mainnet // Humanity Mainnet <-> 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. Humanity Mainnet) humanityContract, // 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 → Humanity Mainnet, confirmations for Humanity Mainnet → Optimism] [20, 80], // 5) Enforced execution options: // [options for Optimism → Humanity Mainnet, options for Humanity Mainnet → 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: humanityContract}], 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 humanity ``` 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 **Humanity 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]=humanity&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](../../get-started/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=humanity) and [Executor](../deployed-contracts.md?chains=humanity) 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://rpc.hyperliquid.xyz/evm', 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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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://rpc.lens.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 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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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://mainnet.mode.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 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](../../get-started/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: 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](../../get-started/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: 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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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://optimism-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 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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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] [1, 1], // 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](../../get-started/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] [1, 1], // 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](../../get-started/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](../../get-started/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: Silicon Mainnet OFT Quickstart sidebar_label: Silicon Mainnet OFT Quickstart description: How to get started building on Silicon 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 **Silicon 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 // Silicon Mainnet (EID=30379) 'silicon-mainnet': { eid: EndpointId.SILICON_V2_MAINNET, url: process.env.RPC_URL_SILICON || '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 siliconContract: OmniPointHardhat = { eid: EndpointId.SILICON_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 <-> silicon // silicon <-> 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. silicon) siliconContract, // 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 → silicon, confirmations for silicon → Optimism] [20, 5], // 5) Enforced execution options: // [options for Optimism → silicon, options for silicon → 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: siliconContract}], 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 silicon ``` 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 **Silicon 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]=silicon&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](../../get-started/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=silicon) and [Executor](../deployed-contracts.md?chains=silicon) 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](../../get-started/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: Somnia Mainnet OFT Quickstart sidebar_label: Somnia Mainnet OFT Quickstart description: How to get started building on Somnia 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 **Somnia 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 // Somnia Mainnet (EID=30380) 'somnia-mainnet': { eid: EndpointId.SOMNIA_V2_MAINNET, url: process.env.RPC_URL_SOMNIA || 'https://api.infra.mainnet.somnia.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 somniaContract: OmniPointHardhat = { eid: EndpointId.SOMNIA_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 <-> Somnia Mainnet // Somnia Mainnet <-> 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. Somnia Mainnet) somniaContract, // 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 → Somnia Mainnet, confirmations for Somnia Mainnet → Optimism] [20, 200], // 5) Enforced execution options: // [options for Optimism → Somnia Mainnet, options for Somnia Mainnet → 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: somniaContract}], 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 somnia ``` 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 **Somnia 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]=somnia&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](../../get-started/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=somnia) and [Executor](../deployed-contracts.md?chains=somnia) 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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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://lite.chain.opentensor.ai', 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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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 Testnet OFT Quickstart sidebar_label: Unichain Testnet OFT Quickstart description: How to get started building on Unichain 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 **Unichain 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 // Unichain Testnet (EID=40333) 'unichain-testnet': { eid: EndpointId.UNICHAIN_V2_TESTNET, url: process.env.RPC_URL_UNICHAIN || 'https://sepolia.unichain.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 unichainTestnetContract: OmniPointHardhat = { eid: EndpointId.UNICHAIN_V2_TESTNET, contractName: 'MyOFT', }; const optsepTestnetContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFT', }; // To connect all the above chains to each other, we need the following pathways: // Optimism Sepolia <-> unichain Testnet // unichain Testnet <-> Optimism Sepolia // 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 Sepolia) optsepTestnetContract, // 2) Chain A's contract (e.g. unichain Testnet) unichainTestnetContract, // 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 → unichain Testnet, confirmations for unichain Testnet → Optimism Sepolia] [1, 1], // 5) Enforced execution options: // [options for Optimism Sepolia → unichain Testnet, options for unichain Testnet → Optimism Sepolia] [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: optsepTestnetContract}, {contract: unichainTestnetContract}], 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-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 **Unichain 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]=unichain-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](../../get-started/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-testnet) and [Executor](../deployed-contracts.md?chains=unichain-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: 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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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://rpc.ankr.com/xai', 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](../../get-started/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.kuma.bid', 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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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](../../get-started/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: Workers in LayerZero V2 sidebar_label: Workers Overview --- 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. :::