Date: Wed, 2 Jul 2025 21:04:17 +0200
Subject: [PATCH 26/57] README
---
examples/oft-upgradeable/README.md | 645 +++++++++++++++++++----------
1 file changed, 422 insertions(+), 223 deletions(-)
diff --git a/examples/oft-upgradeable/README.md b/examples/oft-upgradeable/README.md
index 9778e80bae..0a81490dbb 100644
--- a/examples/oft-upgradeable/README.md
+++ b/examples/oft-upgradeable/README.md
@@ -1,21 +1,61 @@
-
+
- Homepage | Docs | Developers
+ LayerZero Docs
-Omnichain Fungible Token (OFT) Upgradeable Example
+EVM-to-EVM Omnichain Fungible Token (OFT) Upgradeable Example
+
+Template project for an upgradeable cross-chain token (OFT) powered by the LayerZero protocol. This example's config involves EVM chains, but the same OFT can be extended to involve other VM chains such as Solana, Aptos and Hyperliquid.
+
+## Table of Contents
+
+- [Prerequisite Knowledge](#prerequisite-knowledge)
+- [Requirements](#requirements)
+- [Scaffold this example](#scaffold-this-example)
+- [Helper Tasks](#helper-tasks)
+- [Setup](#setup)
+- [Build](#build)
+ - [Compiling your contracts](#compiling-your-contracts)
+- [Deploy](#deploy)
+- [Enable Messaging](#enable-messaging)
+- [Sending OFTs](#sending-ofts)
+- [Next Steps](#next-steps)
+- [Production Deployment Checklist](#production-deployment-checklist)
+ - [Profiling `lzReceive` and `lzCompose` Gas Usage](#profiling-lzreceive-and-lzcompose-gas-usage)
+ - [Available Commands](#available-commands)
+ - [`lzReceive`](#lzreceive)
+ - [`lzCompose`](#lzcompose)
+ - [Usage Examples](#usage-examples)
+ - [Notes](#notes)
+- [Appendix](#appendix)
+ - [Running Tests](#running-tests)
+ - [Adding other chains](#adding-other-chains)
+ - [Using Multisigs](#using-multisigs)
+ - [LayerZero Hardhat Helper Tasks](#layerzero-hardhat-helper-tasks)
+ - [Manual Configuration](#manual-configuration)
+ - [Contract Verification](#contract-verification)
+ - [Troubleshooting](#troubleshooting)
+
+## Prerequisite Knowledge
+
+- [What is an OFT (Omnichain Fungible Token) ?](https://docs.layerzero.network/v2/concepts/applications/oft-standard)
+- [What is an OApp (Omnichain Application) ?](https://docs.layerzero.network/v2/concepts/applications/oapp-standard)
+
+## Introduction
+
+This example contains 4 OFT variations:
+- `MyOFTAdapterFeeUpgradeable`
+- `MyOFTAdapterUpgradeable`
+- `MyOFTFeeUpgradeable`
+- `MyOFTUpgradeable`
+
+The walkthrough will use `MyOFTUpgradeable` but you can as easily swap out the `contractName` (in the [Deploy](#deploy) step onwards).
-
- Quickstart | Configuration | Message Execution Options | Endpoint, MessageLib, & Executor Addresses | DVN Addresses
-
-
-Template project for getting started with LayerZero's OFT contract standard.
:warning: With great power comes great responsibility. Upgradeable contracts are powerful, but they also come with
risks. Please ensure you understand the risks before deploying an upgradeable contract. For more information on the
@@ -23,35 +63,305 @@ limitations of upgradeable contracts, please see the
[OpenZeppelin documentation](https://docs.openzeppelin.com/contracts/5.x/upgradeable). Further, consider fully testing
any and all upgrades thoroughly before deploying to production.
-
+
+This project supports both `hardhat` and `forge` compilation. By default, the `compile` command will execute both:
+
+```bash
+pnpm compile
+```
+
+If you prefer one over the other, you can use the tooling-specific commands:
+
+```bash
+pnpm compile:forge
+pnpm compile:hardhat
+```
+
+## Deploy
+
+To deploy the OFT contracts to your desired blockchains, run the following command:
+
+```bash
+pnpm hardhat lz:deploy --tags MyOFTUpgradeableMock
+```
+
+> :information_source: MyOFTUpgradeableMock will be used as it provides a public mint function which we require for testing
+
+Select all the chains you want to deploy the OFT to.
+
+## Enable Messaging
+
+The OFT standard builds on top of the OApp standard, which enables generic message-passing between chains. After deploying the OFT on the respective chains, you enable messaging by running the [wiring](https://docs.layerzero.network/v2/concepts/glossary#wire--wiring) task.
+
+> :information_source: This example uses the [Simple Config Generator](https://docs.layerzero.network/v2/developers/evm/technical-reference/simple-config), which is recommended over manual configuration.
+
+Run the wiring task:
+
+```bash
+pnpm hardhat lz:oapp:wire --oapp-config layerzero.config
+```
+
+Submit all the transactions to complete wiring. After all transactions confirm, your OApps are wired and can send messages to each other.
+
+## Sending OFTs
+
+With your OFTs wired, you can now send them cross chain.
+
+First, via the mock contract, let's mint on **Optimism Sepolia**:
+
+```
+cast send "mint(address,uint256)" 1000000000000000000 --private-key --rpc-url
+
+```
+
+> You can get the address of your OFT on Optimism Sepolia from the file at `./deployments/optimism-testnet/MyOFTUpgradeableMock.json`
+
+Send 1 OFT from **Optimism Sepolia** to **Arbitrum Sepolia**:
+
+```bash
+pnpm hardhat lz:oft:send --src-eid 40232 --dst-eid 40231 --amount 1 --to
+```
+
+> :information_source: `40232` and `40106` are the Endpoint IDs of Optimism Sepolia and Arbitrum Sepolia respectively. View the list of chains and their Endpoint IDs on the [Deployed Endpoints](https://docs.layerzero.network/v2/deployments/deployed-contracts) page.
+
+Upon a successful send, the script will provide you with the link to the message on LayerZero Scan.
+
+Once the message is delivered, you will be able to click on the destination transaction hash to verify that the OFT was sent.
-- [So What is an Omnichain Fungible Token?](#so-what-is-an-omnichain-fungible-token)
-- [Available Helpers in this Repo](#layerzero-hardhat-helper-tasks)
+Congratulations, you have now sent an OFT cross-chain!
+> If you run into any issues, refer to [Troubleshooting](#troubleshooting).
+
+## Next Steps
+
+Now that you've gone through a simplified walkthrough, here are what you can do next.
+
+- If you are planning to deploy to production, go through the [Production Deployment Checklist](#production-deployment-checklist).
+- Read on [DVNs / Security Stack](https://docs.layerzero.network/v2/concepts/modular-security/security-stack-dvns)
+- Read on [Message Execution Options](https://docs.layerzero.network/v2/concepts/technical-reference/options-reference)
+
+## Production Deployment Checklist
+
+
+
+Before deploying, ensure the following:
+
+- (required) you are not using `MyOFTUpgradeableMock`, which has a public `mint` function
+ - In `layerzero.config.ts`, ensure you are not using `MyOFTUpgradeableMock` as the `contractName` for any of the contract objects.
+- (recommended) you have profiled the gas usage of `lzReceive` on your destination chains
+
+
+### Profiling `lzReceive` and `lzCompose` Gas Usage
+
+The optimal values you should specify for the `gas` parameter in the LZ Config depends on the destination chain, and requires profiling. This section walks through how to estimate the optimal `gas` value.
+
+This guide explains how to use the `pnpm` commands to estimate gas usage for LayerZero's `lzReceive` and `lzCompose` functions. These commands wrap Foundry scripts for easier invocation and allow you to pass the required arguments dynamically.
+
+### Available Commands
+
+1. **`gas:lzReceive`**
+
+ This command profiles the `lzReceive` function for estimating gas usage across multiple runs.
+
+ ```json
+ "gas:lzReceive": "forge script scripts/GasProfiler.s.sol:GasProfilerScript --via-ir --sig 'run_lzReceive(string,address,uint32,address,uint32,address,bytes,uint256,uint256)'"
+ ```
+
+2. **`gas:lzCompose`**
+
+ This command profiles the `lzCompose` function for estimating gas usage across multiple runs.
+
+ ```json
+ "gas:lzCompose": "forge script scripts/GasProfiler.s.sol:GasProfilerScript --via-ir --sig 'run_lzCompose(string,address,uint32,address,uint32,address,address,bytes,uint256,uint256)'"
+ ```
+
+### Usage Examples
+
+#### `lzReceive`
+
+To estimate the gas for the `lzReceive` function:
+
+```bash
+pnpm gas:lzReceive
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+
+```
+
+Where:
+
+- `rpcUrl`: The RPC URL for the target blockchain (e.g., Optimism, Arbitrum, etc.).
+- `endpointAddress`: The deployed LayerZero EndpointV2 contract address.
+- `srcEid`: The source endpoint ID (uint32).
+- `sender`: The sender's address (OApp).
+- `dstEid`: The destination endpoint ID (uint32).
+- `receiver`: The address intended to receive the message (OApp).
+- `message`: The message payload as a `bytes` array.
+- `msg.value`: The amount of Ether sent with the message (in wei).
+- `numOfRuns`: The number of test runs to execute.
+
+#### `lzCompose`
+
+To estimate the gas for the `lzCompose` function:
+
+```bash
+pnpm gas:lzCompose
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+
+```
+
+Where:
+
+- `rpcUrl`: The RPC URL for the target blockchain (e.g., Optimism, Arbitrum, etc.).
+- `endpointAddress`: The deployed LayerZero EndpointV2 contract address.
+- `srcEid`: The source endpoint ID (uint32).
+- `sender`: The originating OApp address.
+- `dstEid`: The destination endpoint ID (uint32).
+- `receiver`: The address intended to receive the message (OApp).
+- `composer`: The LayerZero Composer contract address.
+- `composeMsg`: The compose message payload as a `bytes` array.
+- `msgValue`: The amount of Ether sent with the message (in wei).
+- `numOfRuns`: The number of test runs to execute.
+
+#### Notes
+
+- Modify `numOfRuns` based on the level of accuracy or performance you require for gas profiling.
+- Log outputs will provide metrics such as the **average**, **median**, **minimum**, and **maximum** gas usage across all successful runs.
+
+This approach simplifies repetitive tasks and ensures consistent testing across various configurations.
+
+
+ Join our community! | Follow us on X (formerly Twitter)
-## So what is an Omnichain Fungible Token?
+# Appendix
+
+## Running Tests
+
+Similar to the contract compilation, we support both `hardhat` and `forge` tests. By default, the `test` command will execute both:
-The Omnichain Fungible Token (OFT) Standard is an ERC20 token that can be transferred across multiple blockchains without asset wrapping or middlechains.
+```bash
+pnpm test
+```
-
+If you prefer one over the other, you can use the tooling-specific commands:
-This standard works by combining the LayerZero OApp Contract Standard with the ERC20 [`_burn`](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/oapp/contracts/oft/OFT.sol#L80) method, to initiate omnichain send transfers on the source chain, sending a message via the LayerZero protocol, and delivering a function call to the destination contract to [`_mint`](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/oapp/contracts/oft/OFT.sol#L96) the same number of tokens burned, creating a unified supply across all networks connected.
+```bash
+pnpm test:forge
+pnpm test:hardhat
+```
+
+## Adding other chains
+
+
+
+If you're adding another EVM chain, first, add it to the `hardhat.config.ts`. Adding non-EVM chains do not require modifying the `hardhat.config.ts`.
+
+
+
+Then, modify `layerzero.config.ts` with the following changes:
+
+- declare a new contract object (specifying the `eid` and `contractName`)
+- decide whether to use an existing EVM enforced options variable or declare a new one
+- create a new entry in the `pathways` variable
+- add the new contract into the `contracts` key of the `return` of the `export default` function
+
+After applying the desired changes, make sure you re-run the wiring task:
+
+```bash
+pnpm hardhat lz:oapp:wire --oapp-config layerzero.config.ts
+```
+
+## Using Multisigs
+
+The wiring task supports the usage of Safe Multisigs.
+
+To use a Safe multisig as the signer for these transactions, add the following to each network in your `hardhat.config.ts` and add the `--safe` flag to `lz:oapp:wire --safe`:
+
+```typescript
+// hardhat.config.ts
-Read more about what you can do with OFTs by reading the [OFT Quickstart](https://docs.layerzero.network/v2/developers/evm/oft/quickstart) in the LayerZero Documentation.
+networks: {
+ // Include configurations for other networks as needed
+ fuji: {
+ /* ... */
+ // Network-specific settings
+ safeConfig: {
+ safeUrl: 'http://something', // URL of the Safe API, not the Safe itself
+ safeAddress: 'address'
+ }
+ }
+}
+```
## LayerZero Hardhat Helper Tasks
LayerZero Devtools provides several helper hardhat tasks to easily deploy, verify, configure, connect, and send OFTs cross-chain.
- npx hardhat lz:deploy
+ pnpm hardhat lz:deploy
Deploys your contract to any of the available networks in your [`hardhat.config.ts`](./hardhat.config.ts) when given a deploy tag (by default contract name) and returns a list of available networks to select for the deployment. For specifics around all deployment options, please refer to the [Deploying Contracts](https://docs.layerzero.network/v2/developers/evm/create-lz-oapp/deploying) section of the documentation. LayerZero's `lz:deploy` utilizes `hardhat-deploy`.
-```yml
+```typescript
'arbitrum-sepolia': {
eid: EndpointId.ARBSEP_V2_TESTNET,
url: process.env.RPC_URL_ARBSEP_TESTNET,
@@ -64,10 +374,16 @@ Deploys your contract to any of the available networks in your [`hardhat.config.
},
```
+More information about available CLI arguments can be found using the `--help` flag:
+
+```bash
+pnpm hardhat lz:deploy --help
+```
+
- npx hardhat lz:oapp:config:init --oapp-config YOUR_OAPP_CONFIG --contract-name CONTRACT_NAME
+ pnpm hardhat lz:oapp:config:init --oapp-config YOUR_OAPP_CONFIG --contract-name CONTRACT_NAME
@@ -76,84 +392,93 @@ Initializes a `layerzero.config.ts` file for all available pathways between your
You can run this task by providing the `contract-name` you want to set for the config and `file-name` you want to generate:
```bash
-npx hardhat lz:oapp:config:init --contract-name CONTRACT_NAME --oapp-config FILE_NAME
+pnpm hardhat lz:oapp:config:init --contract-name CONTRACT_NAME --oapp-config FILE_NAME
```
This will create a `layerzero.config.ts` in your working directory populated with your contract name and connections for every pathway possible between your hardhat networks:
-```yml
-import { EndpointId } from '@layerzerolabs/lz-definitions'
+```typescript
+import { EndpointId } from "@layerzerolabs/lz-definitions";
const arbsepContract = {
- eid: EndpointId.ARBSEP_V2_TESTNET,
- contractName: 'MyOFT',
-}
+ eid: EndpointId.ARBSEP_V2_TESTNET,
+ contractName: "MyOFT",
+};
const sepoliaContract = {
- eid: EndpointId.SEPOLIA_V2_TESTNET,
- contractName: 'MyOFT',
-}
+ eid: EndpointId.SEPOLIA_V2_TESTNET,
+ contractName: "MyOFT",
+};
export default {
- contracts: [{ contract: arbsepContract }, { contract: sepoliaContract }],
- connections: [
- {
- from: arbsepContract,
- to: sepoliaContract,
- config: {
- sendLibrary: '0x4f7cd4DA19ABB31b0eC98b9066B9e857B1bf9C0E',
- receiveLibraryConfig: { receiveLibrary: '0x75Db67CDab2824970131D5aa9CECfC9F69c69636', gracePeriod: 0 },
- sendConfig: {
- executorConfig: { maxMessageSize: 10000, executor: '0x5Df3a1cEbBD9c8BA7F8dF51Fd632A9aef8308897' },
- ulnConfig: {
- confirmations: 1,
- requiredDVNs: ['0x53f488E93b4f1b60E8E83aa374dBe1780A1EE8a8'],
- optionalDVNs: [],
- optionalDVNThreshold: 0,
- },
- },
- // receiveConfig: {
- // ulnConfig: {
- // confirmations: 2,
- // requiredDVNs: ['0x53f488E93b4f1b60E8E83aa374dBe1780A1EE8a8'],
- // optionalDVNs: [],
- // optionalDVNThreshold: 0,
- // },
- // },
- },
+ contracts: [{ contract: arbsepContract }, { contract: sepoliaContract }],
+ connections: [
+ {
+ from: arbsepContract,
+ to: sepoliaContract,
+ config: {
+ sendLibrary: "0x4f7cd4DA19ABB31b0eC98b9066B9e857B1bf9C0E",
+ receiveLibraryConfig: {
+ receiveLibrary: "0x75Db67CDab2824970131D5aa9CECfC9F69c69636",
+ gracePeriod: 0,
},
- {
- from: sepoliaContract,
- to: arbsepContract,
- config: {
- sendLibrary: '0xcc1ae8Cf5D3904Cef3360A9532B477529b177cCE',
- receiveLibraryConfig: { receiveLibrary: '0xdAf00F5eE2158dD58E0d3857851c432E34A3A851', gracePeriod: 0 },
- // sendConfig: {
- // executorConfig: { maxMessageSize: 10000, executor: '0x718B92b5CB0a5552039B593faF724D182A881eDA' },
- // ulnConfig: {
- // confirmations: 2,
- // requiredDVNs: ['0x8eebf8b423B73bFCa51a1Db4B7354AA0bFCA9193'],
- // optionalDVNs: [],
- // optionalDVNThreshold: 0,
- // },
- // },
- receiveConfig: {
- ulnConfig: {
- confirmations: 1,
- requiredDVNs: ['0x8eebf8b423B73bFCa51a1Db4B7354AA0bFCA9193'],
- optionalDVNs: [],
- optionalDVNThreshold: 0,
- },
- },
- },
+ sendConfig: {
+ executorConfig: {
+ maxMessageSize: 10000,
+ executor: "0x5Df3a1cEbBD9c8BA7F8dF51Fd632A9aef8308897",
+ },
+ ulnConfig: {
+ confirmations: 1,
+ requiredDVNs: ["0x53f488E93b4f1b60E8E83aa374dBe1780A1EE8a8"],
+ optionalDVNs: [],
+ optionalDVNThreshold: 0,
+ },
},
- ],
-}
+ // receiveConfig: {
+ // ulnConfig: {
+ // confirmations: 2,
+ // requiredDVNs: ['0x53f488E93b4f1b60E8E83aa374dBe1780A1EE8a8'],
+ // optionalDVNs: [],
+ // optionalDVNThreshold: 0,
+ // },
+ // },
+ },
+ },
+ {
+ from: sepoliaContract,
+ to: arbsepContract,
+ config: {
+ sendLibrary: "0xcc1ae8Cf5D3904Cef3360A9532B477529b177cCE",
+ receiveLibraryConfig: {
+ receiveLibrary: "0xdAf00F5eE2158dD58E0d3857851c432E34A3A851",
+ gracePeriod: 0,
+ },
+ // sendConfig: {
+ // executorConfig: { maxMessageSize: 10000, executor: '0x718B92b5CB0a5552039B593faF724D182A881eDA' },
+ // ulnConfig: {
+ // confirmations: 2,
+ // requiredDVNs: ['0x8eebf8b423B73bFCa51a1Db4B7354AA0bFCA9193'],
+ // optionalDVNs: [],
+ // optionalDVNThreshold: 0,
+ // },
+ // },
+ receiveConfig: {
+ ulnConfig: {
+ confirmations: 1,
+ requiredDVNs: ["0x8eebf8b423B73bFCa51a1Db4B7354AA0bFCA9193"],
+ optionalDVNs: [],
+ optionalDVNThreshold: 0,
+ },
+ },
+ },
+ },
+ ],
+};
```
- npx hardhat lz:oapp:config:wire --oapp-config YOUR_OAPP_CONFIG
+ pnpm hardhat lz:oapp:config:wire --oapp-config YOUR_OAPP_CONFIG
@@ -174,32 +499,14 @@ Running `lz:oapp:wire` will make the following function calls per pathway connec
To use this task, run:
```bash
-npx hardhat lz:oapp:wire --oapp-config YOUR_LAYERZERO_CONFIG_FILE
+pnpm hardhat lz:oapp:wire --oapp-config YOUR_LAYERZERO_CONFIG_FILE
```
Whenever you make changes to the configuration, run `lz:oapp:wire` again. The task will check your current configuration, and only apply NEW changes.
-To use a Gnosis Safe multisig as the signer for these transactions, add the following to each network in your `hardhat.config.ts` and add the `--safe` flag to `lz:oapp:wire --safe`:
-
-```yml
-// hardhat.config.ts
-
-networks: {
- // Include configurations for other networks as needed
- fuji: {
- /* ... */
- // Network-specific settings
- safeConfig: {
- safeUrl: 'http://something', // URL of the Safe API, not the Safe itself
- safeAddress: 'address'
- }
- }
-}
-```
-
- npx hardhat lz:oapp:config:get --oapp-config YOUR_OAPP_CONFIG
+ pnpm hardhat lz:oapp:config:get --oapp-config YOUR_OAPP_CONFIG
@@ -264,7 +571,7 @@ If you do NOT explicitly set each configuration parameter, your OApp will fallba
- npx hardhat lz:oapp:config:get:executor --oapp-config YOUR_OAPP_CONFIG
+ pnpm hardhat lz:oapp:config:get:executor --oapp-config YOUR_OAPP_CONFIG
@@ -291,123 +598,13 @@ Returns the LayerZero Executor config for each network in your `hardhat.config.t
-## Developing Contracts
-
-#### Installing dependencies
-
-We recommend using `pnpm` as a package manager (but you can of course use a package manager of your choice):
-
-```bash
-pnpm install
-```
+### Manual Configuration
-#### Compiling your contracts
-
-This project supports both `hardhat` and `forge` compilation. By default, the `compile` command will execute both:
+
-```bash
-pnpm compile
-```
+This section only applies if you would like to configure manually instead of using the Simple Config Generator.
-If you prefer one over the other, you can use the tooling-specific commands:
-
-```bash
-pnpm compile:forge
-pnpm compile:hardhat
-```
-
-Or adjust the `package.json` to for example remove `forge` build:
-
-```diff
-- "compile": "$npm_execpath run compile:forge && $npm_execpath run compile:hardhat",
-- "compile:forge": "forge build",
-- "compile:hardhat": "hardhat compile",
-+ "compile": "hardhat compile"
-```
-
-#### Running tests
-
-Similarly to the contract compilation, we support both `hardhat` and `forge` tests. By default, the `test` command will execute both:
-
-```bash
-pnpm test
-```
-
-If you prefer one over the other, you can use the tooling-specific commands:
-
-```bash
-pnpm test:forge
-pnpm test:hardhat
-```
-
-Or adjust the `package.json` to for example remove `hardhat` tests:
-
-```diff
-- "test": "$npm_execpath test:forge && $npm_execpath test:hardhat",
-- "test:forge": "forge test",
-- "test:hardhat": "$npm_execpath hardhat test"
-+ "test": "forge test"
-```
-
-## Deploying Contracts
-
-Set up deployer wallet/account:
-
-- Rename `.env.example` -> `.env`
-- Choose your preferred means of setting up your deployer wallet/account:
-
-```
-MNEMONIC="test test test test test test test test test test test junk"
-or...
-PRIVATE_KEY="0xabc...def"
-```
-
-- Fund this address with the corresponding chain's native tokens you want to deploy to.
-
-To deploy your contracts to your desired blockchains, run the following command in your project's folder:
-
-```bash
-npx hardhat lz:deploy
-```
-
-More information about available CLI arguments can be found using the `--help` flag:
-
-```bash
-npx hardhat lz:deploy --help
-```
-
-By following these steps, you can focus more on creating innovative omnichain solutions and less on the complexities of cross-chain communication.
-
-
-
-## Connecting Contracts
-
-### Ethereum Configurations
-
-Fill out your `layerzero.config.ts` with the contracts you want to connect. You can generate the default config file for your declared hardhat networks by running:
-
-```bash
-npx hardhat lz:oapp:config:init --contract-name [YOUR_CONTRACT_NAME] --oapp-config [CONFIG_NAME]
-```
-
-> [!NOTE]
-> You may need to change the contract name if you're deploying multiple OApp contracts on different chains (e.g., OFT and OFT Adapter).
-
-
-
-```typescript
-const ethereumContract: OmniPointHardhat = {
- eid: EndpointId.ETHEREUM_V2_MAINNET,
- contractName: "MyOFTAdapter",
-};
-
-const arbitrumContract: OmniPointHardhat = {
- eid: EndpointId.ARBITRUM_V2_MAINNET,
- contractName: "MyOFT",
-};
-```
-
-Then define the pathway you want to create from and to each contract:
+Define the pathway you want to create from and to each contract:
```typescript
connections: [
@@ -452,7 +649,7 @@ connections: [
executor: contractsConfig.ethereum.executor,
},
ulnConfig: {
- // The number of block confirmations to wait on BSC before emitting the message from the source chain.
+ // The number of block confirmations to wait on Ethereum before emitting the message from the source chain.
confirmations: BigInt(15),
// The address of the DVNs you will pay to verify a sent message on the source chain ).
// The destination tx will wait until ALL `requiredDVNs` verify the message.
@@ -523,12 +720,14 @@ connections: [
];
```
-To set these config settings, run:
+### Contract Verification
+
+You can verify EVM chain contracts using the LayerZero helper package:
```bash
-npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts
+pnpm dlx @layerzerolabs/verify-contract -n -u -k --contracts
```
-
- Join our community! | Follow us on X (formerly Twitter)
-
+### Troubleshooting
+
+Refer to [Debugging Messages](https://docs.layerzero.network/v2/developers/evm/troubleshooting/debugging-messages) or [Error Codes & Handling](https://docs.layerzero.network/v2/developers/evm/troubleshooting/error-messages).
From 27028846df227dd7859894ea00ea909318c4d9d2 Mon Sep 17 00:00:00 2001
From: nazreen
Date: Wed, 2 Jul 2025 21:33:31 +0200
Subject: [PATCH 27/57] oft-upgradeable send tasks and simple config generator
---
examples/oft-upgradeable/README.md | 2 +-
.../deploy/MyOFTUpgradeableMock.ts | 39 +++
examples/oft-upgradeable/hardhat.config.ts | 19 +-
examples/oft-upgradeable/layerzero.config.ts | 92 +++----
examples/oft-upgradeable/package.json | 2 +
examples/oft-upgradeable/tasks/sendEvm.ts | 242 ++++++++++++++++++
examples/oft-upgradeable/tasks/sendOFT.ts | 108 ++++++++
examples/oft-upgradeable/tasks/types.ts | 4 +
examples/oft-upgradeable/tasks/utils.ts | 51 ++++
pnpm-lock.yaml | 83 +++++-
10 files changed, 566 insertions(+), 76 deletions(-)
create mode 100644 examples/oft-upgradeable/deploy/MyOFTUpgradeableMock.ts
create mode 100644 examples/oft-upgradeable/tasks/sendEvm.ts
create mode 100644 examples/oft-upgradeable/tasks/sendOFT.ts
create mode 100644 examples/oft-upgradeable/tasks/types.ts
create mode 100644 examples/oft-upgradeable/tasks/utils.ts
diff --git a/examples/oft-upgradeable/README.md b/examples/oft-upgradeable/README.md
index 0a81490dbb..323f0a6c95 100644
--- a/examples/oft-upgradeable/README.md
+++ b/examples/oft-upgradeable/README.md
@@ -49,6 +49,7 @@
## Introduction
This example contains 4 OFT variations:
+
- `MyOFTAdapterFeeUpgradeable`
- `MyOFTAdapterUpgradeable`
- `MyOFTFeeUpgradeable`
@@ -56,7 +57,6 @@ This example contains 4 OFT variations:
The walkthrough will use `MyOFTUpgradeable` but you can as easily swap out the `contractName` (in the [Deploy](#deploy) step onwards).
-
:warning: With great power comes great responsibility. Upgradeable contracts are powerful, but they also come with
risks. Please ensure you understand the risks before deploying an upgradeable contract. For more information on the
limitations of upgradeable contracts, please see the
diff --git a/examples/oft-upgradeable/deploy/MyOFTUpgradeableMock.ts b/examples/oft-upgradeable/deploy/MyOFTUpgradeableMock.ts
new file mode 100644
index 0000000000..2da2943f83
--- /dev/null
+++ b/examples/oft-upgradeable/deploy/MyOFTUpgradeableMock.ts
@@ -0,0 +1,39 @@
+import { type DeployFunction } from 'hardhat-deploy/types'
+
+import { EndpointId, endpointIdToNetwork } from '@layerzerolabs/lz-definitions'
+import { getDeploymentAddressAndAbi } from '@layerzerolabs/lz-evm-sdk-v2'
+
+const contractName = 'MyOFTUpgradeableMock'
+
+const deploy: DeployFunction = async (hre) => {
+ const { deploy } = hre.deployments
+ const signer = (await hre.ethers.getSigners())[0]
+ console.log(`deploying ${contractName} on network: ${hre.network.name} with ${signer.address}`)
+
+ const eid = hre.network.config.eid as EndpointId
+ const lzNetworkName = endpointIdToNetwork(eid)
+
+ const { address } = getDeploymentAddressAndAbi(lzNetworkName, 'EndpointV2')
+
+ await deploy(contractName, {
+ from: signer.address,
+ args: [address],
+ log: true,
+ waitConfirmations: 1,
+ skipIfAlreadyDeployed: false,
+ proxy: {
+ proxyContract: 'OpenZeppelinTransparentProxy',
+ owner: signer.address,
+ execute: {
+ init: {
+ methodName: 'initialize',
+ args: ['MyOFT', 'MOFT', signer.address], // TODO: add name/symbol
+ },
+ },
+ },
+ })
+}
+
+deploy.tags = [contractName]
+
+export default deploy
diff --git a/examples/oft-upgradeable/hardhat.config.ts b/examples/oft-upgradeable/hardhat.config.ts
index 9c2972f3bd..8191c4cfa1 100644
--- a/examples/oft-upgradeable/hardhat.config.ts
+++ b/examples/oft-upgradeable/hardhat.config.ts
@@ -17,6 +17,8 @@ import { HardhatUserConfig, HttpNetworkAccountsUserConfig } from 'hardhat/types'
import { EndpointId } from '@layerzerolabs/lz-definitions'
+import './tasks/sendOFT'
+
// Set your preferred authentication method
//
// If you prefer using a mnemonic, set a MNEMONIC environment variable
@@ -56,19 +58,14 @@ const config: HardhatUserConfig = {
],
},
networks: {
- 'sepolia-testnet': {
- eid: EndpointId.SEPOLIA_V2_TESTNET,
- url: process.env.RPC_URL_SEPOLIA || 'https://rpc.sepolia.org/',
- accounts,
- },
- 'avalanche-testnet': {
- eid: EndpointId.AVALANCHE_V2_TESTNET,
- url: process.env.RPC_URL_FUJI || 'https://rpc.ankr.com/avalanche_fuji',
+ 'optimism-testnet': {
+ eid: EndpointId.OPTSEP_V2_TESTNET,
+ url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co',
accounts,
},
- 'amoy-testnet': {
- eid: EndpointId.AMOY_V2_TESTNET,
- url: process.env.RPC_URL_AMOY || 'https://polygon-amoy-bor-rpc.publicnode.com',
+ 'arbitrum-testnet': {
+ eid: EndpointId.ARBSEP_V2_TESTNET,
+ url: process.env.RPC_URL_ARB_SEPOLIA || 'https://arbitrum-sepolia.gateway.tenderly.co',
accounts,
},
hardhat: {
diff --git a/examples/oft-upgradeable/layerzero.config.ts b/examples/oft-upgradeable/layerzero.config.ts
index 077fe6d4b3..baa1c7a4a3 100644
--- a/examples/oft-upgradeable/layerzero.config.ts
+++ b/examples/oft-upgradeable/layerzero.config.ts
@@ -1,60 +1,52 @@
import { EndpointId } from '@layerzerolabs/lz-definitions'
+import { ExecutorOptionType } from '@layerzerolabs/lz-v2-utilities'
+import { TwoWayConfig, generateConnectionsConfig } from '@layerzerolabs/metadata-tools'
+import { OAppEnforcedOption } from '@layerzerolabs/toolbox-hardhat'
-import type { OAppOmniGraphHardhat, OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat'
+import type { OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat'
-const sepoliaContract: OmniPointHardhat = {
- eid: EndpointId.SEPOLIA_V2_TESTNET,
- contractName: 'MyOFTUpgradeable',
+const optimismContract: OmniPointHardhat = {
+ eid: EndpointId.OPTSEP_V2_TESTNET,
+ contractName: 'MyOFTUpgradeableMock', // Note: change this to 'MyOFTUpgradeable' or your production contract name
}
-const fujiContract: OmniPointHardhat = {
- eid: EndpointId.AVALANCHE_V2_TESTNET,
- contractName: 'MyOFTUpgradeable',
+const arbitrumContract: OmniPointHardhat = {
+ eid: EndpointId.ARBSEP_V2_TESTNET,
+ contractName: 'MyOFTUpgradeableMock', // Note: change this to 'MyOFTUpgradeable' or your production contract name
}
-const amoyContract: OmniPointHardhat = {
- eid: EndpointId.AMOY_V2_TESTNET,
- contractName: 'MyOFTUpgradeable',
-}
+// To connect all the above chains to each other, we need the following pathways:
+// Optimism <-> Arbitrum
-const config: OAppOmniGraphHardhat = {
- contracts: [
- {
- contract: fujiContract,
- },
- {
- contract: sepoliaContract,
- },
- {
- contract: amoyContract,
- },
- ],
- connections: [
- {
- from: fujiContract,
- to: sepoliaContract,
- },
- {
- from: fujiContract,
- to: amoyContract,
- },
- {
- from: sepoliaContract,
- to: fujiContract,
- },
- {
- from: sepoliaContract,
- to: amoyContract,
- },
- {
- from: amoyContract,
- to: sepoliaContract,
- },
- {
- from: amoyContract,
- to: fujiContract,
- },
+// 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 OFT._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,
+ },
+]
+// 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
],
-}
+]
+// Note: you should not use the values 1, 1 for confirmations. Choose the right number of confirmations based on the finalization that you require from the source/destination chains.
-export default config
+export default async function () {
+ // Generate the connections config based on the pathways
+ const connections = await generateConnectionsConfig(pathways)
+ return {
+ contracts: [{ contract: optimismContract }, { contract: arbitrumContract }],
+ connections,
+ }
+}
diff --git a/examples/oft-upgradeable/package.json b/examples/oft-upgradeable/package.json
index a63a412c3f..4dcb885ac1 100644
--- a/examples/oft-upgradeable/package.json
+++ b/examples/oft-upgradeable/package.json
@@ -24,12 +24,14 @@
"@babel/core": "^7.23.9",
"@layerzerolabs/devtools-evm-hardhat": "^3.0.0",
"@layerzerolabs/eslint-config-next": "~2.3.39",
+ "@layerzerolabs/io-devtools": "~0.2.0",
"@layerzerolabs/lz-definitions": "^3.0.75",
"@layerzerolabs/lz-evm-messagelib-v2": "^3.0.75",
"@layerzerolabs/lz-evm-protocol-v2": "^3.0.75",
"@layerzerolabs/lz-evm-sdk-v2": "^3.0.75",
"@layerzerolabs/lz-evm-v1-0.7": "^3.0.75",
"@layerzerolabs/lz-v2-utilities": "^3.0.75",
+ "@layerzerolabs/metadata-tools": "^2.0.0",
"@layerzerolabs/oapp-evm": "^0.3.2",
"@layerzerolabs/oapp-evm-upgradeable": "^0.1.2",
"@layerzerolabs/oft-evm": "^3.1.3",
diff --git a/examples/oft-upgradeable/tasks/sendEvm.ts b/examples/oft-upgradeable/tasks/sendEvm.ts
new file mode 100644
index 0000000000..7a9cf98959
--- /dev/null
+++ b/examples/oft-upgradeable/tasks/sendEvm.ts
@@ -0,0 +1,242 @@
+import path from 'path'
+
+import { BigNumber, ContractTransaction } from 'ethers'
+import { parseUnits } from 'ethers/lib/utils'
+import { HardhatRuntimeEnvironment } from 'hardhat/types'
+
+import { OmniPointHardhat, createGetHreByEid } from '@layerzerolabs/devtools-evm-hardhat'
+import { createLogger } from '@layerzerolabs/io-devtools'
+import { ChainType, endpointIdToChainType, endpointIdToNetwork } from '@layerzerolabs/lz-definitions'
+import { Options, addressToBytes32 } from '@layerzerolabs/lz-v2-utilities'
+
+import { SendResult } from './types'
+import { DebugLogger, KnownErrors, getLayerZeroScanLink } from './utils'
+
+const logger = createLogger()
+
+export interface EvmArgs {
+ srcEid: number
+ dstEid: number
+ amount: string
+ to: string
+ oappConfig: string
+ minAmount?: string
+ extraLzReceiveOptions?: string[]
+ extraLzComposeOptions?: string[]
+ extraNativeDropOptions?: string[]
+ composeMsg?: string
+ oftAddress?: string
+}
+
+export async function sendEvm(
+ {
+ srcEid,
+ dstEid,
+ amount,
+ to,
+ oappConfig,
+ minAmount,
+ extraLzReceiveOptions,
+ extraLzComposeOptions,
+ extraNativeDropOptions,
+ composeMsg,
+ oftAddress,
+ }: EvmArgs,
+ hre: HardhatRuntimeEnvironment
+): Promise {
+ if (endpointIdToChainType(srcEid) !== ChainType.EVM) {
+ throw new Error(`non-EVM srcEid (${srcEid}) not supported here`)
+ }
+
+ const getHreByEid = createGetHreByEid(hre)
+ let srcEidHre: HardhatRuntimeEnvironment
+ try {
+ srcEidHre = await getHreByEid(srcEid)
+ } catch (error) {
+ DebugLogger.printErrorAndFixSuggestion(
+ KnownErrors.ERROR_GETTING_HRE,
+ `For network: ${endpointIdToNetwork(srcEid)}, OFT: ${oftAddress}`
+ )
+ throw error
+ }
+ const signer = (await srcEidHre.ethers.getSigners())[0]
+
+ // 1️⃣ resolve the OFT wrapper address
+ let wrapperAddress: string
+ if (oftAddress) {
+ wrapperAddress = oftAddress
+ } else {
+ const layerZeroConfig = (await import(path.resolve('./', oappConfig))).default
+ const { contracts } = typeof layerZeroConfig === 'function' ? await layerZeroConfig() : layerZeroConfig
+ const wrapper = contracts.find((c: { contract: OmniPointHardhat }) => c.contract.eid === srcEid)
+ if (!wrapper) throw new Error(`No config for EID ${srcEid}`)
+ wrapperAddress = wrapper.contract.contractName
+ ? (await srcEidHre.deployments.get(wrapper.contract.contractName)).address
+ : wrapper.contract.address || ''
+ }
+
+ // 2️⃣ load IOFT ABI, extend it with token()
+ const ioftArtifact = await srcEidHre.artifacts.readArtifact('IOFT')
+
+ // now attach
+ const oft = await srcEidHre.ethers.getContractAt(ioftArtifact.abi, wrapperAddress, signer)
+
+ // 3️⃣ fetch the underlying ERC-20
+ const underlying = await oft.token()
+
+ // 4️⃣ fetch decimals from the underlying token
+ const erc20 = await srcEidHre.ethers.getContractAt('ERC20', underlying, signer)
+ const decimals: number = await erc20.decimals()
+
+ // 5️⃣ normalize the user-supplied amount
+ const amountUnits: BigNumber = parseUnits(amount, decimals)
+
+ // 6️⃣ Check if approval is required (for OFT Adapters) and handle approval
+ try {
+ const approvalRequired = await oft.approvalRequired()
+ if (approvalRequired) {
+ logger.info('OFT Adapter detected - checking ERC20 allowance...')
+
+ // Check current allowance
+ const currentAllowance = await erc20.allowance(signer.address, wrapperAddress)
+ logger.info(`Current allowance: ${currentAllowance.toString()}`)
+ logger.info(`Required amount: ${amountUnits.toString()}`)
+
+ if (currentAllowance.lt(amountUnits)) {
+ logger.info('Insufficient allowance - approving ERC20 tokens...')
+ const approveTx = await erc20.approve(wrapperAddress, amountUnits)
+ logger.info(`Approval transaction hash: ${approveTx.hash}`)
+ await approveTx.wait()
+ logger.info('ERC20 approval confirmed')
+ } else {
+ logger.info('Sufficient allowance already exists')
+ }
+ }
+ } catch (error) {
+ // If approvalRequired() doesn't exist or fails, assume it's a regular OFT (not an adapter)
+ logger.info('No approval required (regular OFT detected)')
+ }
+
+ // 7️⃣ hex string → Uint8Array → zero-pad to 32 bytes
+ const toBytes = addressToBytes32(to)
+
+ // 8️⃣ Build options dynamically using Options.newOptions()
+ let options = Options.newOptions()
+
+ // Add lzReceive options
+ if (extraLzReceiveOptions && extraLzReceiveOptions.length > 0) {
+ // Handle case where Hardhat's CSV parsing splits "gas,value" into separate elements
+ if (extraLzReceiveOptions.length % 2 !== 0) {
+ throw new Error(
+ `Invalid lzReceive options: received ${extraLzReceiveOptions.length} values, but expected pairs of gas,value`
+ )
+ }
+
+ for (let i = 0; i < extraLzReceiveOptions.length; i += 2) {
+ const gas = Number(extraLzReceiveOptions[i])
+ const value = Number(extraLzReceiveOptions[i + 1]) || 0
+ options = options.addExecutorLzReceiveOption(gas, value)
+ logger.info(`Added lzReceive option: ${gas} gas, ${value} value`)
+ }
+ }
+
+ // Add lzCompose options
+ if (extraLzComposeOptions && extraLzComposeOptions.length > 0) {
+ // Handle case where Hardhat's CSV parsing splits "index,gas,value" into separate elements
+ if (extraLzComposeOptions.length % 3 !== 0) {
+ throw new Error(
+ `Invalid lzCompose options: received ${extraLzComposeOptions.length} values, but expected triplets of index,gas,value`
+ )
+ }
+
+ for (let i = 0; i < extraLzComposeOptions.length; i += 3) {
+ const index = Number(extraLzComposeOptions[i])
+ const gas = Number(extraLzComposeOptions[i + 1])
+ const value = Number(extraLzComposeOptions[i + 2]) || 0
+ options = options.addExecutorComposeOption(index, gas, value)
+ logger.info(`Added lzCompose option: index ${index}, ${gas} gas, ${value} value`)
+ }
+ }
+
+ // Add native drop options
+ if (extraNativeDropOptions && extraNativeDropOptions.length > 0) {
+ // Handle case where Hardhat's CSV parsing splits "amount,recipient" into separate elements
+ if (extraNativeDropOptions.length % 2 !== 0) {
+ throw new Error(
+ `Invalid native drop options: received ${extraNativeDropOptions.length} values, but expected pairs of amount,recipient`
+ )
+ }
+
+ for (let i = 0; i < extraNativeDropOptions.length; i += 2) {
+ const amountStr = extraNativeDropOptions[i]
+ const recipient = extraNativeDropOptions[i + 1]
+
+ if (!amountStr || !recipient) {
+ throw new Error(
+ `Invalid native drop option: Both amount and recipient must be provided. Got amount="${amountStr}", recipient="${recipient}"`
+ )
+ }
+
+ try {
+ options = options.addExecutorNativeDropOption(amountStr.trim(), recipient.trim())
+ logger.info(`Added native drop option: ${amountStr.trim()} wei to ${recipient.trim()}`)
+ } catch (error) {
+ // Provide helpful context if the amount exceeds protocol limits
+ const maxUint128 = BigInt('340282366920938463463374607431768211455') // 2^128 - 1
+ const maxUint128Ether = Number(maxUint128) / 1e18 // Convert to ETH for readability
+
+ throw new Error(
+ `Failed to add native drop option with amount ${amountStr.trim()} wei. ` +
+ `LayerZero protocol constrains native drop amounts to uint128 maximum ` +
+ `(${maxUint128.toString()} wei ≈ ${maxUint128Ether.toFixed(2)} ETH). ` +
+ `Original error: ${error instanceof Error ? error.message : String(error)}`
+ )
+ }
+ }
+ }
+
+ const extraOptions = options.toHex()
+
+ // 9️⃣ build sendParam and dispatch
+ const sendParam = {
+ dstEid,
+ to: toBytes,
+ amountLD: amountUnits.toString(),
+ minAmountLD: minAmount ? parseUnits(minAmount, decimals).toString() : amountUnits.toString(),
+ extraOptions: extraOptions,
+ composeMsg: composeMsg ? composeMsg.toString() : '0x',
+ oftCmd: '0x',
+ }
+
+ // 10️⃣ Quote (MessagingFee = { nativeFee, lzTokenFee })
+ logger.info('Quoting the native gas cost for the send transaction...')
+ let msgFee: { nativeFee: BigNumber; lzTokenFee: BigNumber }
+ try {
+ msgFee = await oft.quoteSend(sendParam, false)
+ } catch (error) {
+ DebugLogger.printErrorAndFixSuggestion(
+ KnownErrors.ERROR_QUOTING_NATIVE_GAS_COST,
+ `For network: ${endpointIdToNetwork(srcEid)}, OFT: ${oftAddress}`
+ )
+ throw error
+ }
+ logger.info('Sending the transaction...')
+ let tx: ContractTransaction
+ try {
+ tx = await oft.send(sendParam, msgFee, signer.address, {
+ value: msgFee.nativeFee,
+ })
+ } catch (error) {
+ DebugLogger.printErrorAndFixSuggestion(
+ KnownErrors.ERROR_SENDING_TRANSACTION,
+ `For network: ${endpointIdToNetwork(srcEid)}, OFT: ${oftAddress}`
+ )
+ throw error
+ }
+ const receipt = await tx.wait()
+
+ const txHash = receipt.transactionHash
+ const scanLink = getLayerZeroScanLink(txHash, srcEid >= 40_000 && srcEid < 50_000)
+
+ return { txHash, scanLink }
+}
diff --git a/examples/oft-upgradeable/tasks/sendOFT.ts b/examples/oft-upgradeable/tasks/sendOFT.ts
new file mode 100644
index 0000000000..32101a05af
--- /dev/null
+++ b/examples/oft-upgradeable/tasks/sendOFT.ts
@@ -0,0 +1,108 @@
+import { task, types } from 'hardhat/config'
+import { HardhatRuntimeEnvironment } from 'hardhat/types'
+
+import { types as cliTypes } from '@layerzerolabs/devtools-evm-hardhat'
+import { ChainType, endpointIdToChainType, endpointIdToNetwork } from '@layerzerolabs/lz-definitions'
+
+import { EvmArgs, sendEvm } from './sendEvm'
+import { SendResult } from './types'
+import { DebugLogger, KnownOutputs, KnownWarnings, getBlockExplorerLink } from './utils'
+
+interface MasterArgs {
+ srcEid: number
+ dstEid: number
+ amount: string
+ to: string
+ oappConfig: string
+ /** Minimum amount to receive in case of custom slippage or fees (human readable units, e.g. "1.5") */
+ minAmount?: string
+ /** Array of lzReceive options as comma-separated values "gas,value" - e.g. --extra-lz-receive-options "200000,0" */
+ extraLzReceiveOptions?: string[]
+ /** Array of lzCompose options as comma-separated values "index,gas,value" - e.g. --extra-lz-compose-options "0,500000,0" */
+ extraLzComposeOptions?: string[]
+ /** Array of native drop options as comma-separated values "amount,recipient" - e.g. --extra-native-drop-options "1000000000000000000,0x1234..." */
+ extraNativeDropOptions?: string[]
+ /** Arbitrary bytes message to deliver alongside the OFT */
+ composeMsg?: string
+ /** EVM: 20-byte hex address */
+ oftAddress?: string
+}
+
+task('lz:oft:send', 'Sends OFT tokens cross‐chain from EVM chains')
+ .addParam('srcEid', 'Source endpoint ID', undefined, types.int)
+ .addParam('dstEid', 'Destination endpoint ID', undefined, types.int)
+ .addParam('amount', 'Amount to send (human readable units, e.g. "1.5")', undefined, types.string)
+ .addParam('to', 'Recipient address (20-byte hex for EVM)', undefined, types.string)
+ .addOptionalParam('oappConfig', 'Path to the LayerZero config file', 'layerzero.config.ts', types.string)
+ .addOptionalParam(
+ 'minAmount',
+ 'Minimum amount to receive in case of custom slippage or fees (human readable units, e.g. "1.5")',
+ undefined,
+ types.string
+ )
+ .addOptionalParam(
+ 'extraLzReceiveOptions',
+ 'Array of extra lzReceive options in format "gas,value" (e.g. ["200000,0", "100000,1000000000000000000"])',
+ undefined,
+ cliTypes.csv
+ )
+ .addOptionalParam(
+ 'extraLzComposeOptions',
+ 'Array of extra lzCompose options in format "index,gas,value" (e.g. ["0,500000,0", "1,300000,1000000000000000000"])',
+ undefined,
+ cliTypes.csv
+ )
+ .addOptionalParam(
+ 'extraNativeDropOptions',
+ 'Array of extra native drop options in format "amount,recipient" (e.g. ["1000000000000000000,0x1234..."])',
+ undefined,
+ cliTypes.csv
+ )
+ .addOptionalParam('composeMsg', 'Arbitrary bytes message to deliver alongside the OFT', undefined, types.string)
+ .addOptionalParam(
+ 'oftAddress',
+ 'Override the source local deployment OFT address (20-byte hex for EVM)',
+ undefined,
+ types.string
+ )
+ .setAction(async (args: MasterArgs, hre: HardhatRuntimeEnvironment) => {
+ const chainType = endpointIdToChainType(args.srcEid)
+ let result: SendResult
+
+ if (args.oftAddress) {
+ DebugLogger.printWarning(
+ KnownWarnings.USING_OVERRIDE_OFT,
+ `For network: ${endpointIdToNetwork(args.srcEid)}, OFT: ${args.oftAddress}`
+ )
+ }
+
+ // Only support EVM chains in this example
+ if (chainType === ChainType.EVM) {
+ result = await sendEvm(args as EvmArgs, hre)
+ } else {
+ throw new Error(
+ `The chain type ${chainType} is not supported in this OFT example. Only EVM chains are supported.`
+ )
+ }
+
+ DebugLogger.printLayerZeroOutput(
+ KnownOutputs.SENT_VIA_OFT,
+ `Successfully sent ${args.amount} tokens from ${endpointIdToNetwork(args.srcEid)} to ${endpointIdToNetwork(args.dstEid)}`
+ )
+
+ // print the explorer link for the srcEid from metadata
+ const explorerLink = await getBlockExplorerLink(args.srcEid, result.txHash)
+ // if explorer link is available, print the tx hash link
+ if (explorerLink) {
+ DebugLogger.printLayerZeroOutput(
+ KnownOutputs.TX_HASH,
+ `Explorer link for source chain ${endpointIdToNetwork(args.srcEid)}: ${explorerLink}`
+ )
+ }
+
+ // print the LayerZero Scan link from metadata
+ DebugLogger.printLayerZeroOutput(
+ KnownOutputs.EXPLORER_LINK,
+ `LayerZero Scan link for tracking all cross-chain transaction details: ${result.scanLink}`
+ )
+ })
diff --git a/examples/oft-upgradeable/tasks/types.ts b/examples/oft-upgradeable/tasks/types.ts
new file mode 100644
index 0000000000..8d2b681692
--- /dev/null
+++ b/examples/oft-upgradeable/tasks/types.ts
@@ -0,0 +1,4 @@
+export interface SendResult {
+ txHash: string // EVM: receipt.transactionHash
+ scanLink: string // LayerZeroScan link for cross-chain tracking
+}
diff --git a/examples/oft-upgradeable/tasks/utils.ts b/examples/oft-upgradeable/tasks/utils.ts
new file mode 100644
index 0000000000..934c9a046d
--- /dev/null
+++ b/examples/oft-upgradeable/tasks/utils.ts
@@ -0,0 +1,51 @@
+import { createLogger } from '@layerzerolabs/io-devtools'
+import { endpointIdToNetwork } from '@layerzerolabs/lz-definitions'
+import { Options } from '@layerzerolabs/lz-v2-utilities'
+
+const logger = createLogger()
+
+export const deploymentMetadataUrl = 'https://metadata.layerzero-api.com/v1/metadata/deployments'
+
+/**
+ * Given a srcEid and on-chain tx hash, return
+ * `https://…blockExplorers[0].url/tx/`, or undefined.
+ */
+export async function getBlockExplorerLink(srcEid: number, txHash: string): Promise {
+ const network = endpointIdToNetwork(srcEid) // e.g. "ethereum-mainnet"
+ const res = await fetch(deploymentMetadataUrl)
+ if (!res.ok) return
+ const all = (await res.json()) as Record
+ const meta = all[network]
+ const explorer = meta?.blockExplorers?.[0]?.url
+ if (explorer) {
+ // many explorers use `/tx/`
+ return `${explorer.replace(/\/+$/, '')}/tx/${txHash}`
+ }
+ return
+}
+
+function formatBigIntForDisplay(n: bigint) {
+ return n.toLocaleString().replace(/,/g, '_')
+}
+
+export function decodeLzReceiveOptions(hex: string): string {
+ try {
+ // Handle empty/undefined values first
+ if (!hex || hex === '0x') return 'No options set'
+ const options = Options.fromOptions(hex)
+ const lzReceiveOpt = options.decodeExecutorLzReceiveOption()
+ return lzReceiveOpt
+ ? `gas: ${formatBigIntForDisplay(lzReceiveOpt.gas)} , value: ${formatBigIntForDisplay(lzReceiveOpt.value)} wei`
+ : 'No executor options'
+ } catch (e) {
+ return `Invalid options (${hex.slice(0, 12)}...)`
+ }
+}
+
+// Get LayerZero scan link
+export function getLayerZeroScanLink(txHash: string, isTestnet = false): string {
+ const baseUrl = isTestnet ? 'https://testnet.layerzeroscan.com' : 'https://layerzeroscan.com'
+ return `${baseUrl}/tx/${txHash}`
+}
+
+export { DebugLogger, KnownErrors, KnownOutputs, KnownWarnings } from '@layerzerolabs/io-devtools'
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0bab7cd994..85a852ae4c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2533,6 +2533,9 @@ importers:
'@layerzerolabs/eslint-config-next':
specifier: ~2.3.39
version: 2.3.44(typescript@5.5.3)
+ '@layerzerolabs/io-devtools':
+ specifier: ~0.2.0
+ version: link:../../packages/io-devtools
'@layerzerolabs/lz-definitions':
specifier: ^3.0.75
version: 3.0.75
@@ -2551,6 +2554,9 @@ importers:
'@layerzerolabs/lz-v2-utilities':
specifier: ^3.0.75
version: 3.0.75
+ '@layerzerolabs/metadata-tools':
+ specifier: ^2.0.0
+ version: link:../../packages/metadata-tools
'@layerzerolabs/oapp-evm':
specifier: ^0.3.2
version: link:../../packages/oapp-evm
@@ -4034,7 +4040,7 @@ importers:
version: 2.16.2
jest:
specifier: ^29.6.2
- version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2)
+ version: 29.7.0(@types/node@18.18.14)
tsup:
specifier: ~8.0.1
version: 8.0.1(@swc/core@1.4.0)(typescript@5.5.3)
@@ -4219,7 +4225,7 @@ importers:
version: 29.5.12
jest:
specifier: ^29.7.0
- version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2)
+ version: 29.7.0(@types/node@18.18.14)
tslib:
specifier: ~2.6.2
version: 2.6.3
@@ -4747,13 +4753,13 @@ importers:
version: 3.0.75
'@layerzerolabs/lz-solana-sdk-v2':
specifier: ^3.0.0
- version: 3.0.0(typescript@5.5.3)
+ version: 3.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3)
'@layerzerolabs/lz-v2-utilities':
specifier: ^3.0.75
version: 3.0.75
'@layerzerolabs/oft-v2-solana-sdk':
specifier: ^3.0.38
- version: 3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3)
+ version: 3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3)
'@layerzerolabs/protocol-devtools':
specifier: ~2.0.0
version: link:../protocol-devtools
@@ -5374,13 +5380,13 @@ importers:
version: 3.0.75
'@layerzerolabs/lz-solana-sdk-v2':
specifier: ^3.0.59
- version: 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3)
+ version: 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3)
'@layerzerolabs/lz-v2-utilities':
specifier: ^3.0.75
version: 3.0.75
'@layerzerolabs/oft-v2-solana-sdk':
specifier: ^3.0.59
- version: 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3)
+ version: 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3)
'@layerzerolabs/protocol-devtools':
specifier: ~2.0.0
version: link:../protocol-devtools
@@ -5485,7 +5491,7 @@ importers:
version: 12.6.1
jest:
specifier: ^29.7.0
- version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2)
+ version: 29.7.0(@types/node@18.18.14)
tsup:
specifier: ^8.0.1
version: 8.0.1(@swc/core@1.4.0)(typescript@5.5.3)
@@ -10315,7 +10321,7 @@ packages:
- typescript
- utf-8-validate
- /@layerzerolabs/lz-solana-sdk-v2@3.0.0(typescript@5.5.3):
+ /@layerzerolabs/lz-solana-sdk-v2@3.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3):
resolution: {integrity: sha512-sPvLXeQUO9QLpjOuWE7V+V8yfoI4E/NBYsH9lO2aPx0LYkQa+88ACgPq43B/zFROUD8238WuSb+doGrn3PKtJQ==}
dependencies:
'@ethersproject/abi': 5.7.0
@@ -10346,7 +10352,7 @@ packages:
- utf-8-validate
dev: true
- /@layerzerolabs/lz-solana-sdk-v2@3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3):
+ /@layerzerolabs/lz-solana-sdk-v2@3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3):
resolution: {integrity: sha512-zyuqBYxaVtSa+STdcbO/uzzQV2kyxmBX3flNbvOq7kS2QHBivuaYd/XDbNLE54/egQ63yMtFcilqwy3VzNpclw==}
dependencies:
'@layerzerolabs/lz-corekit-solana': 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3)
@@ -10454,7 +10460,7 @@ packages:
- utf-8-validate
dev: true
- /@layerzerolabs/lz-solana-sdk-v2@3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3):
+ /@layerzerolabs/lz-solana-sdk-v2@3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3):
resolution: {integrity: sha512-FfZLkFHIOPIFLkevQD0QSrfVR2UgREG6YF6oSR92XPViuVpAq0LAyCTXQi915bdiSdQ/Mwd+9eU/9ZPlk9f6sA==}
dependencies:
'@layerzerolabs/lz-corekit-solana': 3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3)
@@ -10781,12 +10787,12 @@ packages:
'@layerzerolabs/lz-definitions': 3.0.75
dev: true
- /@layerzerolabs/oft-v2-solana-sdk@3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3):
+ /@layerzerolabs/oft-v2-solana-sdk@3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3):
resolution: {integrity: sha512-P06/a5+ixph0u1AQkDZ0P0oFaIAdfGPl/UezMfWXUpiWLth428RT0rrMR6qI7z6X1uxqlUFNIotz2ET1fyFcpQ==}
dependencies:
'@ethersproject/bytes': 5.7.0
'@layerzerolabs/lz-foundation': 3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3)
- '@layerzerolabs/lz-solana-sdk-v2': 3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3)
+ '@layerzerolabs/lz-solana-sdk-v2': 3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3)
'@layerzerolabs/lz-v2-utilities': 3.0.86
'@metaplex-foundation/beet': 0.7.2
'@metaplex-foundation/beet-solana': 0.4.1
@@ -10812,12 +10818,12 @@ packages:
- utf-8-validate
dev: true
- /@layerzerolabs/oft-v2-solana-sdk@3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3):
+ /@layerzerolabs/oft-v2-solana-sdk@3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3):
resolution: {integrity: sha512-ijbvj6/Gc4O4WLHfqnrBuKUtIhpaYws/ORj2apZFT1RKSgAX8CCJ9aZmn0ClamEG98i+PpXoroPPU46LMOMZyA==}
dependencies:
'@ethersproject/bytes': 5.7.0
'@layerzerolabs/lz-foundation': 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3)
- '@layerzerolabs/lz-solana-sdk-v2': 3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3)
+ '@layerzerolabs/lz-solana-sdk-v2': 3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3)
'@layerzerolabs/lz-v2-utilities': 3.0.86
'@metaplex-foundation/beet': 0.7.2
'@metaplex-foundation/beet-solana': 0.4.1
@@ -19744,6 +19750,34 @@ packages:
- babel-plugin-macros
- supports-color
+ /jest-cli@29.7.0(@types/node@18.18.14):
+ resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ hasBin: true
+ peerDependencies:
+ node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
+ peerDependenciesMeta:
+ node-notifier:
+ optional: true
+ dependencies:
+ '@jest/core': 29.7.0(ts-node@10.9.2)
+ '@jest/test-result': 29.7.0
+ '@jest/types': 29.6.3
+ chalk: 4.1.2
+ create-jest: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2)
+ exit: 0.1.2
+ import-local: 3.1.0
+ jest-config: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2)
+ jest-util: 29.7.0
+ jest-validate: 29.7.0
+ yargs: 17.7.2
+ transitivePeerDependencies:
+ - '@types/node'
+ - babel-plugin-macros
+ - supports-color
+ - ts-node
+ dev: true
+
/jest-cli@29.7.0(@types/node@18.18.14)(ts-node@10.9.2):
resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -20156,6 +20190,27 @@ packages:
merge-stream: 2.0.0
supports-color: 8.1.1
+ /jest@29.7.0(@types/node@18.18.14):
+ resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ hasBin: true
+ peerDependencies:
+ node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
+ peerDependenciesMeta:
+ node-notifier:
+ optional: true
+ dependencies:
+ '@jest/core': 29.7.0(ts-node@10.9.2)
+ '@jest/types': 29.6.3
+ import-local: 3.1.0
+ jest-cli: 29.7.0(@types/node@18.18.14)
+ transitivePeerDependencies:
+ - '@types/node'
+ - babel-plugin-macros
+ - supports-color
+ - ts-node
+ dev: true
+
/jest@29.7.0(@types/node@18.18.14)(ts-node@10.9.2):
resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
From 4cc8d3cf3630c4df526fbfa35d5750655dd04100 Mon Sep 17 00:00:00 2001
From: nazreen
Date: Wed, 2 Jul 2025 21:36:47 +0200
Subject: [PATCH 28/57] update
---
examples/oft-upgradeable/README.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/examples/oft-upgradeable/README.md b/examples/oft-upgradeable/README.md
index 323f0a6c95..994253ec7a 100644
--- a/examples/oft-upgradeable/README.md
+++ b/examples/oft-upgradeable/README.md
@@ -129,6 +129,8 @@ pnpm hardhat lz:deploy --tags MyOFTUpgradeableMock
> :information_source: MyOFTUpgradeableMock will be used as it provides a public mint function which we require for testing
+> If you would like to try any of the other 3 variants (`MyOFTAdapterFeeUpgradeable`, `MyOFTAdapterUpgradeable`, `MyOFTFeeUpgradeable`), replace `MyOFTUpgradeableMock` with the name of the variant you'd like to deploy.
+
Select all the chains you want to deploy the OFT to.
## Enable Messaging
@@ -137,6 +139,8 @@ The OFT standard builds on top of the OApp standard, which enables generic messa
> :information_source: This example uses the [Simple Config Generator](https://docs.layerzero.network/v2/developers/evm/technical-reference/simple-config), which is recommended over manual configuration.
+> If you are deploying a variant other than `MyOFTUpgradeableMock`, you would need to update the `layerzero.config.ts` so that the contract object uses the correct `contractName` value.
+
Run the wiring task:
```bash
From d9ab744468c567b7ea2dab86b7940c5e8b945178 Mon Sep 17 00:00:00 2001
From: nazreen
Date: Thu, 3 Jul 2025 00:05:38 +0200
Subject: [PATCH 29/57] rm unneeded
---
examples/oft-upgradeable/tasks/utils.ts | 3 ---
1 file changed, 3 deletions(-)
diff --git a/examples/oft-upgradeable/tasks/utils.ts b/examples/oft-upgradeable/tasks/utils.ts
index 934c9a046d..1a322be4d9 100644
--- a/examples/oft-upgradeable/tasks/utils.ts
+++ b/examples/oft-upgradeable/tasks/utils.ts
@@ -1,9 +1,6 @@
-import { createLogger } from '@layerzerolabs/io-devtools'
import { endpointIdToNetwork } from '@layerzerolabs/lz-definitions'
import { Options } from '@layerzerolabs/lz-v2-utilities'
-const logger = createLogger()
-
export const deploymentMetadataUrl = 'https://metadata.layerzero-api.com/v1/metadata/deployments'
/**
From 0df58e9a9716fb51f4853e9977c91b7f45244b1a Mon Sep 17 00:00:00 2001
From: nazreen
Date: Thu, 3 Jul 2025 00:24:35 +0200
Subject: [PATCH 30/57] lint
---
README.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 644cdd5f85..c529d3e099 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,7 @@ Welcome to the **LayerZero Developer Tools Hub**. This repository houses everyth
Visit our developer docs to get started building omnichain applications.
## Repository Structure
+
The primary folders that smart contract developers will find most useful are:
`examples/`: Contains various example projects demonstrating how to build with `OApp.sol` (Omnichain App Standard), `OFT.sol` (Omnichain Fungible Tokens), `ONFT.sol` (Omnichain Non-Fungible Tokens), and more. These examples serve as templates and learning resources.
@@ -88,7 +89,7 @@ pnpm build
This will build all the packages and examples in the repository.
-Review the README for each individual `examples/` project to learn how to interact with and use each sample project.
+Review the README for each individual `examples/` project to learn how to interact with and use each sample project.
## Contributing
From ba6cc3bf0a6e4bb781c236ae2441985cb464038e Mon Sep 17 00:00:00 2001
From: nazreen
Date: Thu, 3 Jul 2025 00:34:55 +0200
Subject: [PATCH 31/57] lint
---
examples/oft-adapter/README.md | 11 +++--------
1 file changed, 3 insertions(+), 8 deletions(-)
diff --git a/examples/oft-adapter/README.md b/examples/oft-adapter/README.md
index 709389af92..6d70af5423 100644
--- a/examples/oft-adapter/README.md
+++ b/examples/oft-adapter/README.md
@@ -47,9 +47,9 @@
- [What is an OFT (Omnichain Fungible Token) ?](https://docs.layerzero.network/v2/concepts/applications/oft-standard)
- [What is an OApp (Omnichain Application) ?](https://docs.layerzero.network/v2/concepts/applications/oapp-standard)
-
## Introduction
-**OFT Adapter** - while a regular OFT uses the mint/burn mechanism, an OFT adapter uses lock/unlock. The OFT Adapter contract functions as a lockbox for the existing token (referred to as the *inner token*). Given the inner token's chain, transfers to outside the inner token's chain will require locking and transfers to the inner token's chain will result in unlocking.
+
+**OFT Adapter** - while a regular OFT uses the mint/burn mechanism, an OFT adapter uses lock/unlock. The OFT Adapter contract functions as a lockbox for the existing token (referred to as the _inner token_). Given the inner token's chain, transfers to outside the inner token's chain will require locking and transfers to the inner token's chain will result in unlocking.
@@ -59,7 +59,6 @@
- `pnpm` (recommended) - or another package manager of your choice (npm, yarn)
- `forge` (optional) - `>=0.2.0` for testing, and if not using Hardhat for compilation
-
## Scaffold this example
Create your local copy of this example:
@@ -72,7 +71,6 @@ Specify the directory, select `OFTAdapter` and proceed with the installation.
Note that `create-lz-oapp` will also automatically run the dependencies install step for you.
-
## Helper Tasks
Throughout this walkthrough, helper tasks will be used. For the full list of available helper tasks, refer to the [LayerZero Hardhat Helper Tasks section](#layerzero-hardhat-helper-tasks). All commands can be run at the project root.
@@ -92,7 +90,6 @@ Throughout this walkthrough, helper tasks will be used. For the full list of ava
- Fund this deployer address/account with the native tokens of the chains you want to deploy to. This example by default will deploy to the following chains' testnets: **Optimism** and **Arbitrum**.
-
## Build
### Compiling your contracts
@@ -126,7 +123,6 @@ On the `Deployed Contract` line, note the `address` logged (inner token's addres
> :information_source: MyERC20Mock will be used as it provides a public mint function which we require for testing. Ensure you do not use this for production.
-
In the `hardhat.config.ts` file, add the inner token's address to the network you want to deploy the OFTAdapter to:
```typescript
@@ -152,7 +148,6 @@ pnpm hardhat lz:deploy --tags MyOFT --networks arbitrum-testnet
The OFT standard builds on top of the OApp standard, which enables generic message-passing between chains. After deploying the OFT on the respective chains, you enable messaging by running the [wiring](https://docs.layerzero.network/v2/concepts/glossary#wire--wiring) task.
-
Run the wiring task:
```bash
@@ -737,4 +732,4 @@ pnpm dlx @layerzerolabs/verify-contract -n -u -k
Date: Thu, 3 Jul 2025 12:09:52 +0200
Subject: [PATCH 32/57] eslint error
---
examples/oft-upgradeable/.eslintrc.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/examples/oft-upgradeable/.eslintrc.js b/examples/oft-upgradeable/.eslintrc.js
index 8ef2c6dbe9..32352e104c 100644
--- a/examples/oft-upgradeable/.eslintrc.js
+++ b/examples/oft-upgradeable/.eslintrc.js
@@ -7,5 +7,6 @@ module.exports = {
// @layerzerolabs/eslint-config-next defines rules for turborepo-based projects
// that are not relevant for this particular project
'turbo/no-undeclared-env-vars': 'off',
+ 'import/no-unresolved': 'warn', // lint runs before workspace packages are built; missing dist/ folders cause false unresolved errors
},
};
From a8f72a07eb8b04fcfe1ebced1fc2128969b693be Mon Sep 17 00:00:00 2001
From: nazreen
Date: Thu, 3 Jul 2025 13:06:56 +0200
Subject: [PATCH 33/57] rename to SPECS
---
docs/{EXAMPLES_DEVELOPMENT.md => EXAMPLES_SPECS.md} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename docs/{EXAMPLES_DEVELOPMENT.md => EXAMPLES_SPECS.md} (100%)
diff --git a/docs/EXAMPLES_DEVELOPMENT.md b/docs/EXAMPLES_SPECS.md
similarity index 100%
rename from docs/EXAMPLES_DEVELOPMENT.md
rename to docs/EXAMPLES_SPECS.md
From 61518a807f830e5582ac5aa0ff0487c8e980e3ab Mon Sep 17 00:00:00 2001
From: nazreen
Date: Thu, 3 Jul 2025 13:07:19 +0200
Subject: [PATCH 34/57] fix lint
---
examples/mint-burn-oft-adapter/tasks/utils.ts | 5 +-
examples/oapp/README.md | 282 +++++++++++++++---
examples/oapp/package.json | 1 +
examples/oft-adapter/tasks/utils.ts | 5 +-
examples/oft-upgradeable/tasks/utils.ts | 2 +-
examples/oft/tasks/utils.ts | 5 +-
6 files changed, 241 insertions(+), 59 deletions(-)
diff --git a/examples/mint-burn-oft-adapter/tasks/utils.ts b/examples/mint-burn-oft-adapter/tasks/utils.ts
index 934c9a046d..5e2fd9b3df 100644
--- a/examples/mint-burn-oft-adapter/tasks/utils.ts
+++ b/examples/mint-burn-oft-adapter/tasks/utils.ts
@@ -1,9 +1,6 @@
-import { createLogger } from '@layerzerolabs/io-devtools'
import { endpointIdToNetwork } from '@layerzerolabs/lz-definitions'
import { Options } from '@layerzerolabs/lz-v2-utilities'
-const logger = createLogger()
-
export const deploymentMetadataUrl = 'https://metadata.layerzero-api.com/v1/metadata/deployments'
/**
@@ -14,7 +11,7 @@ export async function getBlockExplorerLink(srcEid: number, txHash: string): Prom
const network = endpointIdToNetwork(srcEid) // e.g. "ethereum-mainnet"
const res = await fetch(deploymentMetadataUrl)
if (!res.ok) return
- const all = (await res.json()) as Record
+ const all = (await res.json()) as Record
const meta = all[network]
const explorer = meta?.blockExplorers?.[0]?.url
if (explorer) {
diff --git a/examples/oapp/README.md b/examples/oapp/README.md
index 33386e81ca..749ac33450 100644
--- a/examples/oapp/README.md
+++ b/examples/oapp/README.md
@@ -1,32 +1,84 @@
-
+
- Homepage | Docs | Developers
+ LayerZero Docs
-OApp Example
+Omnichain Application (OApp) Example
-
- Quickstart | Configuration | Message Execution Options | Endpoint Addresses
-
+Template project for creating custom omnichain applications (OApp) powered by the LayerZero protocol. This example demonstrates how to build applications that can send and receive arbitrary messages across different blockchains.
+
+## Table of Contents
+
+- [Prerequisite Knowledge](#prerequisite-knowledge)
+- [Requirements](#requirements)
+- [Scaffold this example](#scaffold-this-example)
+- [Helper Tasks](#helper-tasks)
+- [Setup](#setup)
+- [Build](#build)
+ - [Compiling your contracts](#compiling-your-contracts)
+- [Deploy](#deploy)
+- [Enable Messaging](#enable-messaging)
+- [Sending Messages](#sending-messages)
+- [Next Steps](#next-steps)
+- [Production Deployment Checklist](#production-deployment-checklist)
+- [Appendix](#appendix)
+ - [Running Tests](#running-tests)
+ - [Adding other chains](#adding-other-chains)
+ - [Using Multisigs](#using-multisigs)
+ - [LayerZero Hardhat Helper Tasks](#layerzero-hardhat-helper-tasks)
+ - [Contract Verification](#contract-verification)
+ - [Troubleshooting](#troubleshooting)
+
+## Prerequisite Knowledge
+
+- [What is an OApp (Omnichain Application)?](https://docs.layerzero.network/v2/concepts/applications/oapp-standard)
+- [How does LayerZero work?](https://docs.layerzero.network/v2/concepts/protocol/core-concepts)
-Template project for getting started with LayerZero's OApp contract development.
+## Requirements
-## 1) Developing Contracts
+- `Node.js` - `>=18.16.0`
+- `pnpm` (recommended) - or another package manager of your choice (npm, yarn)
+- `forge` (optional) - `>=0.2.0` for testing, and if not using Hardhat for compilation
-#### Installing dependencies
+## Scaffold this example
-We recommend using `pnpm` as a package manager (but you can of course use a package manager of your choice):
+Create your local copy of this example:
```bash
-pnpm install
+pnpm dlx create-lz-oapp@latest --example oapp
```
-#### Compiling your contracts
+Specify the directory, select `OApp` and proceed with the installation.
+
+Note that `create-lz-oapp` will also automatically run the dependencies install step for you.
+
+## Helper Tasks
+
+Throughout this walkthrough, helper tasks will be used. For the full list of available helper tasks, refer to the [LayerZero Hardhat Helper Tasks section](#layerzero-hardhat-helper-tasks). All commands can be run at the project root.
+
+## Setup
+
+- Copy `.env.example` into a new `.env`
+- Set up your deployer address/account via the `.env`
+
+ - You can specify either `MNEMONIC` or `PRIVATE_KEY`:
+
+ ```
+ MNEMONIC="test test test test test test test test test test test junk"
+ or...
+ PRIVATE_KEY="0xabc...def"
+ ```
+
+- Fund this deployer address/account with the native tokens of the chains you want to deploy to. This example by default will deploy to the following chains' testnets: **Ethereum Sepolia** and **Arbitrum Sepolia**.
+
+## Build
+
+### Compiling your contracts
This project supports both `hardhat` and `forge` compilation. By default, the `compile` command will execute both:
@@ -41,18 +93,75 @@ pnpm compile:forge
pnpm compile:hardhat
```
-Or adjust the `package.json` to for example remove `forge` build:
+## Deploy
+
+To deploy the OApp contracts to your desired blockchains, run the following command:
-```diff
-- "compile": "$npm_execpath run compile:forge && $npm_execpath run compile:hardhat",
-- "compile:forge": "forge build",
-- "compile:hardhat": "hardhat compile",
-+ "compile": "hardhat compile"
+```bash
+pnpm hardhat lz:deploy --tags MyOApp
```
-#### Running tests
+Select all the chains you want to deploy the OApp to.
-Similarly to the contract compilation, we support both `hardhat` and `forge` tests. By default, the `test` command will execute both:
+## Enable Messaging
+
+After deploying the OApp on the respective chains, you enable messaging by running the [wiring](https://docs.layerzero.network/v2/concepts/glossary#wire--wiring) task.
+
+> :information_source: This example uses the [Simple Config Generator](https://docs.layerzero.network/v2/developers/evm/technical-reference/simple-config), which is recommended over manual configuration.
+
+Run the wiring task:
+
+```bash
+pnpm hardhat lz:oapp:wire --oapp-config layerzero.config.ts
+```
+
+Submit all the transactions to complete wiring. After all transactions confirm, your OApps are wired and can send messages to each other.
+
+## Sending Messages
+
+With your OApps wired, you can now send messages cross-chain.
+
+Send a message from **Ethereum Sepolia** to **Arbitrum Sepolia**:
+
+```bash
+pnpm hardhat lz:oapp:send --src-eid 40161 --dst-eid 40231 --msg "Hello from Ethereum!"
+```
+
+> :information_source: `40161` and `40231` are the Endpoint IDs of Ethereum Sepolia and Arbitrum Sepolia respectively. View the list of chains and their Endpoint IDs on the [Deployed Endpoints](https://docs.layerzero.network/v2/deployments/deployed-contracts) page.
+
+Upon a successful send, the script will provide you with the link to the message on LayerZero Scan.
+
+Once the message is delivered, you will be able to click on the destination transaction hash to verify that the message was received.
+
+Congratulations, you have now sent a message cross-chain!
+
+> If you run into any issues, refer to [Troubleshooting](#troubleshooting).
+
+## Next Steps
+
+Now that you've gone through a simplified walkthrough, here are what you can do next.
+
+- If you are planning to deploy to production, go through the [Production Deployment Checklist](#production-deployment-checklist).
+- Read on [DVNs / Security Stack](https://docs.layerzero.network/v2/concepts/modular-security/security-stack-dvns)
+- Read on [Message Execution Options](https://docs.layerzero.network/v2/concepts/technical-reference/options-reference)
+
+## Production Deployment Checklist
+
+Before deploying, ensure the following:
+
+- (recommended) you have profiled the gas usage of `lzReceive` on your destination chains
+- (recommended) you have configured appropriate DVNs for your security requirements
+- (recommended) you have tested your application thoroughly on testnets
+
+
+ Join our community! | Follow us on X (formerly Twitter)
+
+
+# Appendix
+
+## Running Tests
+
+Similar to the contract compilation, we support both `hardhat` and `forge` tests. By default, the `test` command will execute both:
```bash
pnpm test
@@ -65,52 +174,133 @@ pnpm test:forge
pnpm test:hardhat
```
-Or adjust the `package.json` to for example remove `hardhat` tests:
+## Adding other chains
-```diff
-- "test": "$npm_execpath test:forge && $npm_execpath test:hardhat",
-- "test:forge": "forge test",
-- "test:hardhat": "$npm_execpath hardhat test"
-+ "test": "forge test"
-```
+If you're adding another EVM chain, first, add it to the `hardhat.config.ts`. Adding non-EVM chains do not require modifying the `hardhat.config.ts`.
-## 2) Deploying Contracts
+Then, modify `layerzero.config.ts` with the following changes:
-Set up deployer wallet/account:
+- declare a new contract object (specifying the `eid` and `contractName`)
+- decide whether to use an existing EVM enforced options variable or declare a new one
+- create a new entry in the `connections` array
+- add the new contract into the `contracts` array of the `export default` function
-- Rename `.env.example` -> `.env`
-- Choose your preferred means of setting up your deployer wallet/account:
+After applying the desired changes, make sure you re-run the wiring task:
+```bash
+pnpm hardhat lz:oapp:wire --oapp-config layerzero.config.ts
```
-MNEMONIC="test test test test test test test test test test test junk"
-or...
-PRIVATE_KEY="0xabc...def"
+
+## Using Multisigs
+
+The wiring task supports the usage of Safe Multisigs.
+
+To use a Safe multisig as the signer for these transactions, add the following to each network in your `hardhat.config.ts` and add the `--safe` flag to `lz:oapp:wire --safe`:
+
+```typescript
+// hardhat.config.ts
+
+networks: {
+ // Include configurations for other networks as needed
+ fuji: {
+ /* ... */
+ // Network-specific settings
+ safeConfig: {
+ safeUrl: 'http://something', // URL of the Safe API, not the Safe itself
+ safeAddress: 'address'
+ }
+ }
+}
```
-To deploy your contracts to your desired blockchains, run the following command in your project's folder:
+## LayerZero Hardhat Helper Tasks
+
+LayerZero Devtools provides several helper hardhat tasks to easily deploy, verify, configure, connect, and interact with OApps cross-chain.
+
+
+ pnpm hardhat lz:deploy
+
+
+
+Deploys your contract to any of the available networks in your [`hardhat.config.ts`](./hardhat.config.ts) when given a deploy tag (by default contract name) and returns a list of available networks to select for the deployment. For specifics around all deployment options, please refer to the [Deploying Contracts](https://docs.layerzero.network/v2/developers/evm/create-lz-oapp/deploying) section of the documentation. LayerZero's `lz:deploy` utilizes `hardhat-deploy`.
+
+More information about available CLI arguments can be found using the `--help` flag:
```bash
-npx hardhat lz:deploy
+pnpm hardhat lz:deploy --help
```
-More information about available CLI arguments can be found using the `--help` flag:
+
+
+
+ pnpm hardhat lz:oapp:config:init --oapp-config YOUR_OAPP_CONFIG --contract-name CONTRACT_NAME
+
+
+
+Initializes a `layerzero.config.ts` file for all available pathways between your hardhat networks with the current LayerZero default placeholder settings. This task can be incredibly useful for correctly formatting your config file.
+
+You can run this task by providing the `contract-name` you want to set for the config and `file-name` you want to generate:
```bash
-npx hardhat lz:deploy --help
+pnpm hardhat lz:oapp:config:init --contract-name CONTRACT_NAME --oapp-config FILE_NAME
```
-## 3) Connecting Contracts
+
+
+
+ pnpm hardhat lz:oapp:config:wire --oapp-config YOUR_OAPP_CONFIG
+
+
+
+Calls the configuration functions between your deployed OApp contracts on every chain based on the provided `layerzero.config.ts`.
-Wire your deployed contracts by running:
+Running `lz:oapp:wire` will make the following function calls per pathway connection for a fully defined config file using your specified settings and your environment variables (Private Keys and RPCs):
+
+- function setPeer(uint32 \_eid, bytes32 \_peer) public virtual onlyOwner {}
+
+- function setConfig(address \_oapp, address \_lib, SetConfigParam[] calldata \_params) external onlyRegistered(\_lib) {}
+
+- function setEnforcedOptions(EnforcedOptionParam[] calldata \_enforcedOptions) public virtual onlyOwner {}
+
+- function setSendLibrary(address \_oapp, uint32 \_eid, address \_newLib) external onlyRegisteredOrDefault(\_newLib) onlySupportedEid(\_newLib, \_eid) {}
+
+- function setReceiveLibrary(address \_oapp, uint32 \_eid, address \_newLib, uint256 \_gracePeriod) external onlyRegisteredOrDefault(\_newLib) isReceiveLib(\_newLib) onlySupportedEid(\_newLib, \_eid) {}
+
+To use this task, run:
```bash
-npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts
+pnpm hardhat lz:oapp:wire --oapp-config YOUR_LAYERZERO_CONFIG_FILE
```
-By following these steps, you can focus more on creating innovative omnichain solutions and less on the complexities of cross-chain communication.
+Whenever you make changes to the configuration, run `lz:oapp:wire` again. The task will check your current configuration, and only apply NEW changes.
-
+
-
- Join our community! | Follow us on X (formerly Twitter)
-
+
+ pnpm hardhat lz:oapp:config:get --oapp-config YOUR_OAPP_CONFIG
+
+
+
+Returns your current OApp's configuration for each chain and pathway in 3 columns:
+
+- **Custom Configuration**: the changes that your `layerzero.config.ts` currently has set
+
+- **Default Configuration**: the default placeholder configuration that LayerZero provides
+
+- **Active Configuration**: the active configuration that applies to the message pathway (Defaults + Custom Values)
+
+If you do NOT explicitly set each configuration parameter, your OApp will fallback to the placeholder parameters in the default config.
+
+
+
+### Contract Verification
+
+You can verify EVM chain contracts using the LayerZero helper package:
+
+```bash
+pnpm dlx @layerzerolabs/verify-contract -n -u -k --contracts
+```
+
+### Troubleshooting
+
+Refer to [Debugging Messages](https://docs.layerzero.network/v2/developers/evm/troubleshooting/debugging-messages) or [Error Codes & Handling](https://docs.layerzero.network/v2/developers/evm/troubleshooting/error-messages).
diff --git a/examples/oapp/package.json b/examples/oapp/package.json
index baf749f054..b02ca964db 100644
--- a/examples/oapp/package.json
+++ b/examples/oapp/package.json
@@ -23,6 +23,7 @@
"devDependencies": {
"@babel/core": "^7.23.9",
"@layerzerolabs/eslint-config-next": "~2.3.39",
+ "@layerzerolabs/io-devtools": "~0.2.0",
"@layerzerolabs/lz-definitions": "^3.0.75",
"@layerzerolabs/lz-evm-messagelib-v2": "^3.0.75",
"@layerzerolabs/lz-evm-protocol-v2": "^3.0.75",
diff --git a/examples/oft-adapter/tasks/utils.ts b/examples/oft-adapter/tasks/utils.ts
index 934c9a046d..5e2fd9b3df 100644
--- a/examples/oft-adapter/tasks/utils.ts
+++ b/examples/oft-adapter/tasks/utils.ts
@@ -1,9 +1,6 @@
-import { createLogger } from '@layerzerolabs/io-devtools'
import { endpointIdToNetwork } from '@layerzerolabs/lz-definitions'
import { Options } from '@layerzerolabs/lz-v2-utilities'
-const logger = createLogger()
-
export const deploymentMetadataUrl = 'https://metadata.layerzero-api.com/v1/metadata/deployments'
/**
@@ -14,7 +11,7 @@ export async function getBlockExplorerLink(srcEid: number, txHash: string): Prom
const network = endpointIdToNetwork(srcEid) // e.g. "ethereum-mainnet"
const res = await fetch(deploymentMetadataUrl)
if (!res.ok) return
- const all = (await res.json()) as Record
+ const all = (await res.json()) as Record
const meta = all[network]
const explorer = meta?.blockExplorers?.[0]?.url
if (explorer) {
diff --git a/examples/oft-upgradeable/tasks/utils.ts b/examples/oft-upgradeable/tasks/utils.ts
index 1a322be4d9..5e2fd9b3df 100644
--- a/examples/oft-upgradeable/tasks/utils.ts
+++ b/examples/oft-upgradeable/tasks/utils.ts
@@ -11,7 +11,7 @@ export async function getBlockExplorerLink(srcEid: number, txHash: string): Prom
const network = endpointIdToNetwork(srcEid) // e.g. "ethereum-mainnet"
const res = await fetch(deploymentMetadataUrl)
if (!res.ok) return
- const all = (await res.json()) as Record
+ const all = (await res.json()) as Record
const meta = all[network]
const explorer = meta?.blockExplorers?.[0]?.url
if (explorer) {
diff --git a/examples/oft/tasks/utils.ts b/examples/oft/tasks/utils.ts
index 934c9a046d..5e2fd9b3df 100644
--- a/examples/oft/tasks/utils.ts
+++ b/examples/oft/tasks/utils.ts
@@ -1,9 +1,6 @@
-import { createLogger } from '@layerzerolabs/io-devtools'
import { endpointIdToNetwork } from '@layerzerolabs/lz-definitions'
import { Options } from '@layerzerolabs/lz-v2-utilities'
-const logger = createLogger()
-
export const deploymentMetadataUrl = 'https://metadata.layerzero-api.com/v1/metadata/deployments'
/**
@@ -14,7 +11,7 @@ export async function getBlockExplorerLink(srcEid: number, txHash: string): Prom
const network = endpointIdToNetwork(srcEid) // e.g. "ethereum-mainnet"
const res = await fetch(deploymentMetadataUrl)
if (!res.ok) return
- const all = (await res.json()) as Record
+ const all = (await res.json()) as Record
const meta = all[network]
const explorer = meta?.blockExplorers?.[0]?.url
if (explorer) {
From 42f18f4b2507b8502a35f016273104fd9cb5d77e Mon Sep 17 00:00:00 2001
From: nazreen
Date: Thu, 3 Jul 2025 13:14:47 +0200
Subject: [PATCH 35/57] lockfile
---
pnpm-lock.yaml | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 85a852ae4c..a65fd2ff80 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -77,6 +77,8 @@ importers:
specifier: 1.11.0
version: 1.11.0
+ docs: {}
+
examples/lzapp-migration:
dependencies:
'@layerzerolabs/devtools':
@@ -515,6 +517,9 @@ importers:
'@layerzerolabs/eslint-config-next':
specifier: ~2.3.39
version: 2.3.44(typescript@5.5.3)
+ '@layerzerolabs/io-devtools':
+ specifier: ~0.2.0
+ version: link:../../packages/io-devtools
'@layerzerolabs/lz-definitions':
specifier: ^3.0.75
version: 3.0.75
From 941b7653fd747a6a749205304447322910fa091f Mon Sep 17 00:00:00 2001
From: nazreen
Date: Thu, 3 Jul 2025 13:18:11 +0200
Subject: [PATCH 36/57] update specs
---
docs/EXAMPLES_SPECS.md | 82 +++++++++++++++++++++++++-----------------
1 file changed, 49 insertions(+), 33 deletions(-)
diff --git a/docs/EXAMPLES_SPECS.md b/docs/EXAMPLES_SPECS.md
index c4d03dce00..31f91fe2d6 100644
--- a/docs/EXAMPLES_SPECS.md
+++ b/docs/EXAMPLES_SPECS.md
@@ -2,7 +2,7 @@ This document is intended for the maintainers of the examples that are in `/exam
Currently, this document will only detail the structure for the READMEs of the examples.
-## Structure for examples' READMEs
+## 1. README Structure
1. **Header**
- Goal: Branding + promote docs site + entrypoint
@@ -10,7 +10,7 @@ Currently, this document will only detail the structure for the READMEs of the e
2. **Example Title**
- Goal: What the example will teach
- - Contents: Title + 1–2 sentence description (goal-oriented preferred)
+ - Contents: Title + 1–2 sentence goal-style description
3. **Table of Contents**
- Goal: Allow user to easily navigate the README
@@ -21,76 +21,92 @@ Currently, this document will only detail the structure for the READMEs of the e
- Contents: e.g., What is an OApp? What is an OFT?
5. **Introduction** _(optional)_
- - Goal: What is this example about
- - Contents: Brief explanation if needed; otherwise rely on title + prerequisites
+ - Goal: High-level context on what this example covers
+ - Contents: Brief explanation; skip if title + prerequisites suffice
6. **Requirements**
- Goal: What needs to be installed
- - Contents: List tools + version numbers; consider noting testnet token needs
+ - Contents: Tools + versions; optionally call out testnet funding needs
7. **Scaffold this example**
- - Goal: How to init the example
+ - Goal: How to initialize the example
- Contents: `pnpm dlx create-lz-oapp@latest --example `
8. **Helper Tasks (inline notice)**
- - Goal: Know that helper tasks exist
- - Contents: Single line pointing to the detailed section
+ - Goal: Let users know helpers exist
+ - Contents: Single-line pointer to helper tasks section
9. **Setup**
- - Goal: What to configure before running
- - Contents: `.env` instructions, deployer account setup
+ - Goal: What to configure before building
+ - Contents: `.env` setup, deployer account prep
10. **Build**
- - Goal: How to build contracts/programs/modules
- - Contents: Build command(s)
+ - Goal: How to compile contracts/programs/modules
+ - Contents: Build commands
11. **Deploy**
- Goal: How to deploy contracts/programs/modules
- Contents: Deploy command + minting instructions (if applicable)
12. **Enable Messaging**
- - Goal: How to set up OApps for use
- - Contents: LZ config, init step, wiring step
+ - Goal: How to wire/configure OApps for messaging
+ - Contents: LZ config, init, and wiring steps
13. **Sending Message/OFT/ONFT**
- - Goal: How to trigger cross-chain actions
- - Contents: Send command(s) for both/all directions
+ - Goal: Trigger a cross-chain action
+ - Contents: Send command(s), both/all directions
14. **Next Steps**
- - Goal: What to know after basic deployment
- - Contents: Production Checklist + links to Security Stack, Message Options
+ - Goal: What to know after completing the deployment
+ - Contents: Production Deployment Checklist + links (Security Stack, Message Options)
15. **Production Deployment Checklist**
- - Goal: Prepare for production
- - Contents: Gas profiling, DVNs, confirmations
+ - Goal: Prep for production usage
+ - Contents: Gas profiling, DVN config, confirmation settings
16. **Appendix**
- - Goal: Mark end of main build steps
- - Contents: Additional configuration, testing, and advanced info
+ - Goal: Mark end of core deployment steps
+ - Contents: Additional topics and configuration
16.1. **Running tests**
- Goal: How to test contracts/programs
- - Contents: Test command(s)
+ - Contents: Test commands
16.2. **Adding other chains**
- - Goal: Support more networks
- - Contents: Add chain logic; update `hardhat.config.ts`
+ - Goal: Expand the example to more networks
+ - Contents: Add logic, update `hardhat.config.ts`
16.3. **Using Multisigs**
- - Goal: Deploy using multisig
- - Contents: Command param diffs; multi-VM notes
+ - Goal: Deploy using a multisig wallet
+ - Contents: Command param diffs, multi-VM notes
16.4. **LayerZero Hardhat Helper Tasks (detailed)**
- - Goal: Know all available helpers
- - Contents: Link to docs + built-in + local tasks
+ - Goal: Understand all helper tasks
+ - Contents: Link to docs + list of built-in and local tasks
16.5. **Contract/Program Verification**
- - Goal: How to verify deployments
- - Contents: Per-VM verification doc links
+ - Goal: Verify deployments
+ - Contents: VM-specific verification docs
16.6. **Troubleshooting**
- - Goal: Solve common issues
- - Contents: Link to global troubleshooting + local fixes
+ - Goal: Resolve errors and setup issues
+ - Contents: Link to general troubleshooting + local fixes
+
+---
+
+## 2. README Principles
+
+1. Example READMEs should focus on required commands, with elaborations linked to docs.
+2. Avoid duplicating explanations of general concepts (e.g., OFTs)—link to docs instead.
+3. The first mention of concepts like Endpoint IDs, Wiring, etc. should link to the glossary: https://docs.layerzero.network/v2/home/glossary
+4. TODO: Every README should invite partners to provide feedback to drive improvements.
+
+---
+
+## 3. Example Code Principles
+
+1. **Options-first**: Enforced Options implementation and instructions should be included by default (e.g. in `layerzero.config.ts`).
+2. **Two chains only**: Examples should use only 2 chains by default to reduce testnet setup friction; use “Add other chains” section to scale up if needed.
From 8c6a47f2b70cf8f754a8e70b816dfe0c8efc5df7 Mon Sep 17 00:00:00 2001
From: nazreen
Date: Thu, 3 Jul 2025 13:19:52 +0200
Subject: [PATCH 37/57] fix
---
examples/oapp/README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/examples/oapp/README.md b/examples/oapp/README.md
index 749ac33450..0e6a207056 100644
--- a/examples/oapp/README.md
+++ b/examples/oapp/README.md
@@ -293,7 +293,7 @@ If you do NOT explicitly set each configuration parameter, your OApp will fallba