diff --git a/.changeset/odd-carrots-stare.md b/.changeset/odd-carrots-stare.md new file mode 100644 index 0000000000..f0f8f13678 --- /dev/null +++ b/.changeset/odd-carrots-stare.md @@ -0,0 +1,5 @@ +--- +"@layerzerolabs/oapp-example": minor +--- + +reduce to 2 chains and revamp README diff --git a/.changeset/perfect-bobcats-complain.md b/.changeset/perfect-bobcats-complain.md new file mode 100644 index 0000000000..6ad46e4dca --- /dev/null +++ b/.changeset/perfect-bobcats-complain.md @@ -0,0 +1,5 @@ +--- +"@layerzerolabs/mint-burn-oft-adapter-example": patch +--- + +fix any type usage diff --git a/.changeset/serious-beds-fetch.md b/.changeset/serious-beds-fetch.md new file mode 100644 index 0000000000..c1d9a81e38 --- /dev/null +++ b/.changeset/serious-beds-fetch.md @@ -0,0 +1,5 @@ +--- +"@layerzerolabs/oft-example": minor +--- + +revamp README, add mock deploy script, reduce to 2 chains diff --git a/.changeset/tiny-books-drive.md b/.changeset/tiny-books-drive.md new file mode 100644 index 0000000000..6366dcacdd --- /dev/null +++ b/.changeset/tiny-books-drive.md @@ -0,0 +1,5 @@ +--- +"@layerzerolabs/oft-adapter-example": minor +--- + +revamp README, add mock deploy script, reduce to 2 chains diff --git a/.changeset/wise-numbers-hammer.md b/.changeset/wise-numbers-hammer.md new file mode 100644 index 0000000000..c84046aae6 --- /dev/null +++ b/.changeset/wise-numbers-hammer.md @@ -0,0 +1,5 @@ +--- +"@layerzerolabs/oft-upgradeable-example": minor +--- + +revamp README, add mock deploy script, reduce to 2 chains, add send scripts 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 diff --git a/docs/EXAMPLES_SPECS.md b/docs/EXAMPLES_SPECS.md new file mode 100644 index 0000000000..5067f23874 --- /dev/null +++ b/docs/EXAMPLES_SPECS.md @@ -0,0 +1,127 @@ +# Example Specs + +## Table of Contents + +- [Audience](#audience) +- [README Structure](#readme-structure) +- [Example README Principles](#example-readme-principles) +- [Example Code Principles](#example-code-principles) + +## Audience + +This guide is intended for both: +- **Developers** maintaining or contributing to `/examples` +- **Coding agents** (e.g. Cursor, Copilot, GPT) that assist with editing, reviewing, or scaffolding examples + +AI agents should be pointed to this file via `AGENTS.md` or `.cursor/rules`. + +Currently, this document will only detail the structure for the READMEs of the examples. + +## README Structure + +1. **Header** + - Goal: Branding + promote docs site + entrypoint + - Contents: LayerZero logo + links to docs and dev portal + +2. **Example Title** + - Goal: What the example will teach + - Contents: Title + 1–2 sentence goal-style description + +3. **Table of Contents** + - Goal: Allow user to easily navigate the README + - Contents: TOC of all headings + +4. **Prerequisite Knowledge** + - Goal: What to understand before running the example + - Contents: e.g., What is an OApp? What is an OFT? + +5. **Introduction** _(optional)_ + - 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: Tools + versions; optionally call out testnet funding needs + +7. **Scaffold this example** + - Goal: How to initialize the example + - Contents: `pnpm dlx create-lz-oapp@latest --example ` (Some examples require a feature flag. Refer to `packages/create-lz-oapp/src/config.ts` to verify) + +8. **Helper Tasks (inline notice)** + - Goal: Let users know helpers exist + - Contents: Single-line pointer to helper tasks section + +9. **Setup** + - Goal: What to configure before building + - Contents: `.env` setup, deployer account prep + +10. **Build** + - 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 wire/configure OApps for messaging + - Contents: LZ config, init, and wiring steps + +13. **Sending Message/OFT/ONFT** + - Goal: Trigger a cross-chain action + - Contents: CLI command to triffer send, both/all directions. E.g. for examples/oft, it is `pnpm hardhat lz:oft:send --src-eid 40232 --dst-eid 40231 --amount 1 --to ` + +14. **Next Steps** + - Goal: What to know after completing the deployment + - Contents: Production Deployment Checklist + links (Security Stack, Message Options) + +15. **Production Deployment Checklist** + - Goal: Prep for production usage + - Contents: Gas profiling, DVN config, confirmation settings + +16. **Appendix** + - Goal: Mark end of core deployment steps + - Contents: Additional topics and configuration + + 16.1. **Running tests** + - Goal: How to test contracts/programs + - Contents: Test commands + + 16.2. **Adding other chains** + - Goal: Expand the example to more networks + - Contents: Add logic, update `hardhat.config.ts` + + 16.3. **Using Multisigs** + - Goal: Deploy using a multisig wallet + - Contents: Command param diffs, multi-VM notes + + 16.4. **LayerZero Hardhat Helper Tasks (detailed)** + - Goal: Understand all helper tasks + - Contents: Link to docs + list of built-in and local tasks + + 16.5. **Contract/Program Verification** + - Goal: Verify deployments + - Contents: VM-specific verification docs + + 16.6. **Troubleshooting** + - Goal: Resolve errors and setup issues + - Contents: Link to general troubleshooting + local fixes + +Any sections that don't appear in the above list should be considered for removal. + + +--- + +## Example 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. + +--- + +## 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. diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000000..9f7fdf2e51 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,12 @@ +{ + "name": "@layerzerolabs/devtools-docs", + "version": "0.0.1", + "private": true, + "description": "Documentation for LayerZero devtools", + "scripts": { + "build": "echo 'No build needed for docs'", + "clean": "echo 'No clean needed for docs'", + "lint": "echo 'No lint needed for docs'", + "test": "echo 'No tests for docs'" + } +} 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..5a3ed942d6 100644 --- a/examples/oapp/README.md +++ b/examples/oapp/README.md @@ -1,32 +1,84 @@

- LayerZero + LayerZero

- 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) -

Template project for getting started with LayerZero's OApp contract development.

+## Prerequisite Knowledge -## 1) Developing Contracts +- [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) -#### Installing dependencies +## Requirements -We recommend using `pnpm` as a package manager (but you can of course use a package manager of your choice): +- `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 + +## Scaffold this example + +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: **Optimism 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,81 @@ pnpm compile:forge pnpm compile:hardhat ``` -Or adjust the `package.json` to for example remove `forge` build: +## Deploy -```diff -- "compile": "$npm_execpath run compile:forge && $npm_execpath run compile:hardhat", -- "compile:forge": "forge build", -- "compile:hardhat": "hardhat compile", -+ "compile": "hardhat compile" +To deploy the OApp contracts to your desired blockchains, run the following command: + +```bash +pnpm hardhat lz:deploy --tags MyOApp ``` -#### Running tests +Select all the chains you want to deploy the OApp to. + +## Enable Messaging -Similarly to the contract compilation, we support both `hardhat` and `forge` tests. By default, the `test` command will execute both: +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 **Optimism Sepolia** to **Arbitrum Sepolia**: + +```bash +pnpm hardhat lz:oapp:send --dst-eid 40231 --string 'Hello from Ethereum!' --network optimism-testnet +``` + +Send a message from **Arbitrum Sepolia** to **Optimism Sepolia**: + +```bash +pnpm hardhat lz:oapp:send --dst-eid 40161 --string 'Hello from Arbitrum!' --network arbitrum-testnet +``` + +> :information_source: `40161` and `40231` are the Endpoint IDs of Optimism Sepolia and Arbitrum Sepolia respectively. The source network is determined by the `--network` flag, not a separate `--src-eid` parameter. 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 +180,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 + +
-Wire your deployed contracts by running: +Calls the configuration functions between your deployed OApp contracts on every chain based on the provided `layerzero.config.ts`. + +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/hardhat.config.ts b/examples/oapp/hardhat.config.ts index 45ab695a6e..6750b79054 100644 --- a/examples/oapp/hardhat.config.ts +++ b/examples/oapp/hardhat.config.ts @@ -13,6 +13,8 @@ import { HardhatUserConfig, HttpNetworkAccountsUserConfig } from 'hardhat/types' import { EndpointId } from '@layerzerolabs/lz-definitions' +import './tasks/sendString' + // Set your preferred authentication method // // If you prefer using a mnemonic, set a MNEMONIC environment variable @@ -57,11 +59,6 @@ const config: HardhatUserConfig = { url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, - 'avalanche-testnet': { - eid: EndpointId.AVALANCHE_V2_TESTNET, - url: process.env.RPC_URL_FUJI || 'https://avalanche-fuji.drpc.org', - accounts, - }, 'arbitrum-testnet': { eid: EndpointId.ARBSEP_V2_TESTNET, url: process.env.RPC_URL_ARB_SEPOLIA || 'https://arbitrum-sepolia.gateway.tenderly.co', diff --git a/examples/oapp/layerzero.config.ts b/examples/oapp/layerzero.config.ts index 3700145155..844542b809 100644 --- a/examples/oapp/layerzero.config.ts +++ b/examples/oapp/layerzero.config.ts @@ -8,11 +8,6 @@ const optimismContract: OmniPointHardhat = { contractName: 'MyOApp', } -const avalancheContract: OmniPointHardhat = { - eid: EndpointId.AVALANCHE_V2_TESTNET, - contractName: 'MyOApp', -} - const arbitrumContract: OmniPointHardhat = { eid: EndpointId.ARBSEP_V2_TESTNET, contractName: 'MyOApp', @@ -31,41 +26,25 @@ const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ ] // 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 + arbitrumContract, // 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 }], + contracts: [{ contract: optimismContract }, { contract: arbitrumContract }], connections, } } 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/README.md b/examples/oft-adapter/README.md index 15f4be147d..6d70af5423 100644 --- a/examples/oft-adapter/README.md +++ b/examples/oft-adapter/README.md @@ -1,42 +1,100 @@

- LayerZero + LayerZero

- Homepage | Docs | Developers + LayerZero Docs

-

OFTAdapter Example

+

OFT Adapter Example

-

- Quickstart | Configuration | Message Execution Options | Endpoint Addresses -

+

Template project for converting an existing token into a cross-chain token (OFT) using 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.

-

Template project for getting started with LayerZero's OFTAdapter contract development.

+## Table of Contents -### OFTAdapter additional setup: +- [Prerequisite Knowledge](#prerequisite-knowledge) +- [Introduction](#introduction) +- [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) -- In your `hardhat.config.ts` file, add the following configuration to the network you want to deploy the OFTAdapter to: - ```typescript - // Replace `0x0` with the address of the ERC20 token you want to adapt to the OFT functionality. - oftAdapter: { - tokenAddress: '0x0', - } - ``` +## 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 + +**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. -## 1) Developing Contracts + -#### Installing dependencies +## Requirements -We recommend using `pnpm` as a package manager (but you can of course use a package manager of your choice): +- `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 + +## Scaffold this example + +Create your local copy of this example: ```bash -pnpm install +pnpm dlx create-lz-oapp@latest --example oft-adapter ``` -#### Compiling your contracts +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. + +## 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: **Optimism** and **Arbitrum**. + +## Build + +### Compiling your contracts + + This project supports both `hardhat` and `forge` compilation. By default, the `compile` command will execute both: @@ -51,18 +109,194 @@ pnpm compile:forge pnpm compile:hardhat ``` -Or adjust the `package.json` to for example remove `forge` build: +## Deploy + +First, deploy the inner token to (only) **Optimism Sepolia**. + +```bash +pnpm hardhat lz:deploy --tags MyERC20Mock --networks optimism-testnet +``` + +The deploy script for **MyERC20Mock** will also mint 10 tokens to the deployer address. + +On the `Deployed Contract` line, note the `address` logged (inner token's address) upon successful deployment as you need it for the next step. Else, you can also refer to `./deployments/optimism-testnet/MyERC20Mock.json`. + +> :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 +// Replace `0x0` with the address of the ERC20 token you want to adapt to the OFT functionality. +oftAdapter: { + tokenAddress: '', +} +``` + +Deploy an OFTAdapter to Optimism Sepolia: + +```bash +pnpm hardhat lz:deploy --tags MyOFTAdapter --networks optimism-testnet +``` + +Deploy the OFT to Arbitrum Sepolia: + +```bash +pnpm hardhat lz:deploy --tags MyOFT --networks arbitrum-testnet +``` + +## 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. + +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 OFTs + +With your OFTs wired, you can now send them cross chain. + +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. + +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 `MyOFTMock`, which has a public `mint` function + - In `layerzero.config.ts`, ensure you are not using `MyOFTMock` 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` -```diff -- "compile": "$npm_execpath run compile:forge && $npm_execpath run compile:hardhat", -- "compile:forge": "forge build", -- "compile:hardhat": "hardhat compile", -+ "compile": "hardhat compile" +To estimate the gas for the `lzCompose` function: + +```bash +pnpm gas:lzCompose + \ + \ + \ + \ + \ + \ + \ + \ + \ + ``` -#### Running tests +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) +

+ +# Appendix -Similarly to the contract compilation, we support both `hardhat` and `forge` tests. By default, the `test` command will execute both: +## 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 @@ -75,46 +309,427 @@ pnpm test:forge pnpm test:hardhat ``` -Or adjust the `package.json` to for example remove `hardhat` tests: +## 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: -```diff -- "test": "$npm_execpath test:forge && $npm_execpath test:hardhat", -- "test:forge": "forge test", -- "test:hardhat": "$npm_execpath hardhat test" -+ "test": "forge test" +- 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 ``` -## 2) Deploying Contracts +## Using Multisigs -Set up deployer wallet/account: +The wiring task supports the usage of Safe Multisigs. -- Rename `.env.example` -> `.env` -- Choose your preferred means of setting up your deployer wallet/account: +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' + } + } +} ``` -MNEMONIC="test test test test test test test test test test test junk" -or... -PRIVATE_KEY="0xabc...def" + +## LayerZero Hardhat Helper Tasks + +LayerZero Devtools provides several helper hardhat tasks to easily deploy, verify, configure, connect, and send OFTs 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`. + +```typescript +'arbitrum-sepolia': { + eid: EndpointId.ARBSEP_V2_TESTNET, + url: process.env.RPC_URL_ARBSEP_TESTNET, + accounts, +}, +'base-sepolia': { + eid: EndpointId.BASESEP_V2_TESTNET, + url: process.env.RPC_URL_BASE_TESTNET, + accounts, +}, +``` + +More information about available CLI arguments can be found using the `--help` flag: + +```bash +pnpm hardhat lz:deploy --help ``` -- Fund this address with the corresponding chain's native tokens you want to deploy to. +
+ +
+ pnpm hardhat lz:oapp:config:init --oapp-config YOUR_OAPP_CONFIG --contract-name CONTRACT_NAME + +
-To deploy your contracts to your desired blockchains, run the following command in your project's folder: +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 +pnpm hardhat lz:oapp:config:init --contract-name CONTRACT_NAME --oapp-config FILE_NAME ``` -More information about available CLI arguments can be found using the `--help` flag: +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: + +```typescript +import { EndpointId } from "@layerzerolabs/lz-definitions"; + +const arbsepContract = { + eid: EndpointId.ARBSEP_V2_TESTNET, + contractName: "MyOFT", +}; +const sepoliaContract = { + 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, + // }, + // }, + }, + }, + { + 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, + }, + }, + }, + }, + ], +}; +``` + +
+ +
+ 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`. + +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) isSendLib(\_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:deploy --help +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. -

+
+
+ pnpm hardhat lz:oapp:config:get --oapp-config YOUR_OAPP_CONFIG -

- Join our community! | Follow us on X (formerly Twitter) -

+
+ +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. + +```bash +┌────────────────────┬───────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────────────────────────┐ +│ │ Custom OApp Config │ Default OApp Config │ Active OApp Config │ +├────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤ +│ localNetworkName │ arbsep │ arbsep │ arbsep │ +├────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤ +│ remoteNetworkName │ sepolia │ sepolia │ sepolia │ +├────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤ +│ sendLibrary │ 0x4f7cd4DA19ABB31b0eC98b9066B9e857B1bf9C0E │ 0x4f7cd4DA19ABB31b0eC98b9066B9e857B1bf9C0E │ 0x4f7cd4DA19ABB31b0eC98b9066B9e857B1bf9C0E │ +├────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤ +│ receiveLibrary │ 0x75Db67CDab2824970131D5aa9CECfC9F69c69636 │ 0x75Db67CDab2824970131D5aa9CECfC9F69c69636 │ 0x75Db67CDab2824970131D5aa9CECfC9F69c69636 │ +├────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤ +│ sendUlnConfig │ ┌──────────────────────┬────────────────────────────────────────────────────┐ │ ┌──────────────────────┬────────────────────────────────────────────────────┐ │ ┌──────────────────────┬────────────────────────────────────────────────────┐ │ +│ │ │ confirmations │ 1 │ │ │ confirmations │ 1 │ │ │ confirmations │ 1 │ │ +│ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ +│ │ │ requiredDVNs │ ┌───┬────────────────────────────────────────────┐ │ │ │ requiredDVNs │ ┌───┬────────────────────────────────────────────┐ │ │ │ requiredDVNs │ ┌───┬────────────────────────────────────────────┐ │ │ +│ │ │ │ │ 0 │ 0x53f488E93b4f1b60E8E83aa374dBe1780A1EE8a8 │ │ │ │ │ │ 0 │ 0x53f488E93b4f1b60E8E83aa374dBe1780A1EE8a8 │ │ │ │ │ │ 0 │ 0x53f488E93b4f1b60E8E83aa374dBe1780A1EE8a8 │ │ │ +│ │ │ │ └───┴────────────────────────────────────────────┘ │ │ │ │ └───┴────────────────────────────────────────────┘ │ │ │ │ └───┴────────────────────────────────────────────┘ │ +│ │ │ │ │ │ │ │ │ │ │ │ │ │ +│ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ +│ │ │ optionalDVNs │ │ │ │ optionalDVNs │ │ │ │ optionalDVNs │ │ │ +│ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ +│ │ │ optionalDVNThreshold │ 0 │ │ │ optionalDVNThreshold │ 0 │ │ │ optionalDVNThreshold │ 0 │ │ +│ │ └──────────────────────┴────────────────────────────────────────────────────┘ │ └──────────────────────┴────────────────────────────────────────────────────┘ │ └──────────────────────┴────────────────────────────────────────────────────┘ │ +│ │ │ │ │ +├────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤ +│ sendExecutorConfig │ ┌────────────────┬────────────────────────────────────────────┐ │ ┌────────────────┬────────────────────────────────────────────┐ │ ┌────────────────┬────────────────────────────────────────────┐ │ +│ │ │ executor │ 0x5Df3a1cEbBD9c8BA7F8dF51Fd632A9aef8308897 │ │ │ executor │ 0x5Df3a1cEbBD9c8BA7F8dF51Fd632A9aef8308897 │ │ │ executor │ 0x5Df3a1cEbBD9c8BA7F8dF51Fd632A9aef8308897 │ │ +│ │ ├────────────────┼────────────────────────────────────────────┤ │ ├────────────────┼────────────────────────────────────────────┤ │ ├────────────────┼────────────────────────────────────────────┤ │ +│ │ │ maxMessageSize │ 10000 │ │ │ maxMessageSize │ 10000 │ │ │ maxMessageSize │ 10000 │ │ +│ │ └────────────────┴────────────────────────────────────────────┘ │ └────────────────┴────────────────────────────────────────────┘ │ └────────────────┴────────────────────────────────────────────┘ │ +│ │ │ │ │ +├────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤ +│ receiveUlnConfig │ ┌──────────────────────┬────────────────────────────────────────────────────┐ │ ┌──────────────────────┬────────────────────────────────────────────────────┐ │ ┌──────────────────────┬────────────────────────────────────────────────────┐ │ +│ │ │ confirmations │ 2 │ │ │ confirmations │ 2 │ │ │ confirmations │ 2 │ │ +│ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ +│ │ │ requiredDVNs │ ┌───┬────────────────────────────────────────────┐ │ │ │ requiredDVNs │ ┌───┬────────────────────────────────────────────┐ │ │ │ requiredDVNs │ ┌───┬────────────────────────────────────────────┐ │ │ +│ │ │ │ │ 0 │ 0x53f488E93b4f1b60E8E83aa374dBe1780A1EE8a8 │ │ │ │ │ │ 0 │ 0x53f488E93b4f1b60E8E83aa374dBe1780A1EE8a8 │ │ │ │ │ │ 0 │ 0x53f488E93b4f1b60E8E83aa374dBe1780A1EE8a8 │ │ │ +│ │ │ │ └───┴────────────────────────────────────────────┘ │ │ │ │ └───┴────────────────────────────────────────────┘ │ │ │ │ └───┴────────────────────────────────────────────┘ │ │ +│ │ │ │ │ │ │ │ │ │ │ │ │ │ +│ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ +│ │ │ optionalDVNs │ │ │ │ optionalDVNs │ │ │ │ optionalDVNs │ │ │ +│ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ ├──────────────────────┼────────────────────────────────────────────────────┤ │ +│ │ │ optionalDVNThreshold │ 0 │ │ │ optionalDVNThreshold │ 0 │ │ │ optionalDVNThreshold │ 0 │ │ +│ │ └──────────────────────┴────────────────────────────────────────────────────┘ │ └──────────────────────┴────────────────────────────────────────────────────┘ │ └──────────────────────┴────────────────────────────────────────────────────┘ │ +│ │ │ │ │ +└────────────────────┴───────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────┘ +``` + +
+
+ pnpm hardhat lz:oapp:config:get:executor --oapp-config YOUR_OAPP_CONFIG + +
+ +Returns the LayerZero Executor config for each network in your `hardhat.config.ts`. You can use this method to see the max destination gas in wei (`nativeCap`) you can request in your [`execution options`](https://docs.layerzero.network/v2/developers/evm/gas-settings/options). + +```bash +┌───────────────────┬────────────────────────────────────────────┐ +│ localNetworkName │ mantle │ +├───────────────────┼────────────────────────────────────────────┤ +│ remoteNetworkName │ polygon │ +├───────────────────┼────────────────────────────────────────────┤ +│ executorDstConfig │ ┌────────────────┬───────────────────────┐ │ +│ │ │ baseGas │ 85000 │ │ +│ │ ├────────────────┼───────────────────────┤ │ +│ │ │ multiplierBps │ 12000 │ │ +│ │ ├────────────────┼───────────────────────┤ │ +│ │ │ floorMarginUSD │ 5000000000000000000 │ │ +│ │ ├────────────────┼───────────────────────┤ │ +│ │ │ nativeCap │ 681000000000000000000 │ │ +│ │ └────────────────┴───────────────────────┘ │ +│ │ │ +└───────────────────┴────────────────────────────────────────────┘ +``` + +
+ +### Manual Configuration + + + +This section only applies if you would like to configure manually instead of using the Simple Config Generator. + +Define the pathway you want to create from and to each contract: + +```typescript +connections: [ + // ETH <--> ARB PATHWAY: START + { + from: ethereumContract, + to: arbitrumContract, + }, + { + from: arbitrumContract, + to: ethereumContract, + }, + // ETH <--> ARB PATHWAY: END +]; +``` + +Finally, define the config settings for each direction of the pathway: + +```typescript +connections: [ + // ETH <--> ARB PATHWAY: START + { + from: ethereumContract, + to: arbitrumContract, + config: { + sendLibrary: contractsConfig.ethereum.sendLib302, + receiveLibraryConfig: { + receiveLibrary: contractsConfig.ethereum.receiveLib302, + gracePeriod: BigInt(0), + }, + // Optional Receive Library Timeout for when the Old Receive Library Address will no longer be valid + receiveLibraryTimeoutConfig: { + lib: "0x0000000000000000000000000000000000000000", + expiry: BigInt(0), + }, + // Optional Send Configuration + // @dev Controls how the `from` chain sends messages to the `to` chain. + sendConfig: { + executorConfig: { + maxMessageSize: 10000, + // The configured Executor address + executor: contractsConfig.ethereum.executor, + }, + ulnConfig: { + // 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. + requiredDVNs: [ + contractsConfig.ethereum.horizenDVN, // Horizen + contractsConfig.ethereum.polyhedraDVN, // Polyhedra + contractsConfig.ethereum.animocaBlockdaemonDVN, // Animoca-Blockdaemon (only available on ETH <-> Arbitrum One) + contractsConfig.ethereum.lzDVN, // LayerZero Labs + ], + // The address of the DVNs you will pay to verify a sent message on the source chain ). + // The destination tx will wait until the configured threshold of `optionalDVNs` verify a message. + optionalDVNs: [], + // The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified. + optionalDVNThreshold: 0, + }, + }, + // 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. + confirmations: BigInt(20), + // The address of the DVNs your `receiveConfig` expects to receive verifications from on the `from` chain ). + // The `from` chain's OApp will wait until the configured threshold of `requiredDVNs` verify the message. + requiredDVNs: [ + contractsConfig.ethereum.lzDVN, // LayerZero Labs DVN + contractsConfig.ethereum.animocaBlockdaemonDVN, // Blockdaemon-Animoca + contractsConfig.ethereum.horizenDVN, // Horizen Labs + contractsConfig.ethereum.polyhedraDVN, // Polyhedra + ], + // The address of the `optionalDVNs` you expect to receive verifications from on the `from` chain ). + // The destination tx will wait until the configured threshold of `optionalDVNs` verify the message. + optionalDVNs: [], + // The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified. + optionalDVNThreshold: 0, + }, + }, + // Optional Enforced Options Configuration + // @dev Controls how much gas to use on the `to` chain, which the user pays for on the source `from` chain. + enforcedOptions: [ + { + msgType: 1, + optionType: ExecutorOptionType.LZ_RECEIVE, + gas: 65000, + value: 0, + }, + { + msgType: 2, + optionType: ExecutorOptionType.LZ_RECEIVE, + gas: 65000, + value: 0, + }, + { + msgType: 2, + optionType: ExecutorOptionType.COMPOSE, + index: 0, + gas: 50000, + value: 0, + }, + ], + }, + }, + { + from: arbitrumContract, + to: ethereumContract, + }, + // ETH <--> ARB PATHWAY: END +]; +``` + +### 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/oft-adapter/deploy/MyERC20Mock.ts b/examples/oft-adapter/deploy/MyERC20Mock.ts index bbb93605e0..916d453393 100644 --- a/examples/oft-adapter/deploy/MyERC20Mock.ts +++ b/examples/oft-adapter/deploy/MyERC20Mock.ts @@ -35,12 +35,12 @@ const deploy: DeployFunction = async (hre) => { // Mint initial tokens to the deployer const [signer] = await hre.ethers.getSigners() - const mintBurnToken = await hre.ethers.getContractAt(contractName, address, signer) + const innerToken = await hre.ethers.getContractAt(contractName, address, signer) - const mintTx = await mintBurnToken.mint(deployer, initialMintAmount) + const mintTx = await innerToken.mint(deployer, initialMintAmount) await mintTx.wait() - const balance = await mintBurnToken.balanceOf(deployer) + const balance = await innerToken.balanceOf(deployer) console.log(`Minted ${hre.ethers.utils.formatEther(balance)} ${tokenSymbol} tokens to deployer: ${deployer}`) } diff --git a/examples/oft-adapter/hardhat.config.ts b/examples/oft-adapter/hardhat.config.ts index 13fb2653a9..8824452c77 100644 --- a/examples/oft-adapter/hardhat.config.ts +++ b/examples/oft-adapter/hardhat.config.ts @@ -63,11 +63,6 @@ const config: HardhatUserConfig = { tokenAddress: '0x0', // Set the token address for the OFT adapter }, }, - 'avalanche-testnet': { - eid: EndpointId.AVALANCHE_V2_TESTNET, - url: process.env.RPC_URL_FUJI || 'https://avalanche-fuji.drpc.org', - accounts, - }, 'arbitrum-testnet': { eid: EndpointId.ARBSEP_V2_TESTNET, url: process.env.RPC_URL_ARB_SEPOLIA || 'https://arbitrum-sepolia.gateway.tenderly.co', diff --git a/examples/oft-adapter/layerzero.config.ts b/examples/oft-adapter/layerzero.config.ts index ae5eac4ccc..03ae1446e1 100644 --- a/examples/oft-adapter/layerzero.config.ts +++ b/examples/oft-adapter/layerzero.config.ts @@ -11,10 +11,10 @@ import type { OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat' * * for example: * - * sepolia: { - * eid: EndpointId.SEPOLIA_V2_TESTNET, - * url: process.env.RPC_URL_SEPOLIA || 'https://rpc.sepolia.org/', - * accounts, + * 'optimism-testnet': { + * eid: EndpointId.OPTSEP_V2_TESTNET, + * url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', + * accounts, * oftAdapter: { * tokenAddress: '0x0', // Set the token address for the OFT adapter * }, @@ -25,20 +25,13 @@ const optimismContract: OmniPointHardhat = { contractName: 'MyOFTAdapter', } -const avalancheContract: OmniPointHardhat = { - eid: EndpointId.AVALANCHE_V2_TESTNET, - contractName: 'MyOFT', -} - const arbitrumContract: OmniPointHardhat = { eid: EndpointId.ARBSEP_V2_TESTNET, contractName: 'MyOFT', } // To connect all the above chains to each other, we need the following pathways: -// Optimism <-> Avalanche // Optimism <-> Arbitrum -// Avalanche <-> Arbitrum // 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 @@ -55,13 +48,6 @@ const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ // 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 @@ -69,20 +55,13 @@ const pathways: TwoWayConfig[] = [ [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 }], + contracts: [{ contract: optimismContract }, { contract: arbitrumContract }], connections, } } 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/.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 }, }; diff --git a/examples/oft-upgradeable/README.md b/examples/oft-upgradeable/README.md index 9778e80bae..f220031929 100644 --- a/examples/oft-upgradeable/README.md +++ b/examples/oft-upgradeable/README.md @@ -1,21 +1,61 @@

- LayerZero + LayerZero

- Homepage | Docs | Developers + LayerZero Docs

-

Omnichain Fungible Token (OFT) Upgradeable Example

- -

- Quickstart | Configuration | Message Execution Options | Endpoint, MessageLib, & Executor Addresses | DVN Addresses -

- -

Template project for getting started with LayerZero's OFT contract standard.

+

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). :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,309 @@ 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 + +> 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 + +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. + +> 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 +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 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. -- [So What is an Omnichain Fungible Token?](#so-what-is-an-omnichain-fungible-token) -- [Available Helpers in this Repo](#layerzero-hardhat-helper-tasks) +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. + +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: + +```bash +pnpm test +``` + +If you prefer one over the other, you can use the tooling-specific commands: + +```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`. -The Omnichain Fungible Token (OFT) Standard is an ERC20 token that can be transferred across multiple blockchains without asset wrapping or middlechains. + -LayerZero +Then, modify `layerzero.config.ts` with the following changes: -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. +- 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 -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. +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 + +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 +378,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 +396,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 +503,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 +575,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 +602,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 -``` - -#### Compiling your contracts - -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 -``` - -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 +### Manual Configuration -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", -}; -``` +This section only applies if you would like to configure manually instead of using the Simple Config Generator. -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 +653,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 +724,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). 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..5e2fd9b3df --- /dev/null +++ b/examples/oft-upgradeable/tasks/utils.ts @@ -0,0 +1,48 @@ +import { endpointIdToNetwork } from '@layerzerolabs/lz-definitions' +import { Options } from '@layerzerolabs/lz-v2-utilities' + +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/examples/oft/README.md b/examples/oft/README.md index 0504b12757..9cde874f27 100644 --- a/examples/oft/README.md +++ b/examples/oft/README.md @@ -1,51 +1,350 @@

- LayerZero + LayerZero

- Homepage | Docs | Developers + LayerZero Docs

-

Omnichain Fungible Token (OFT) Example

+

EVM-to-EVM Omnichain Fungible Token (OFT) Example

+ +

Template project for a 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) + +## Requirements + +- `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 + +## Scaffold this example + +Create your local copy of this example: + +```bash +pnpm dlx create-lz-oapp@latest --example oft +``` + +Specify the directory, select `OFT` 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: **Optimism** and **Arbitrum**. + +## Build + +### Compiling your contracts + + + +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 MyOFTMock +``` + +> :information_source: MyOFTMock 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.ts +``` + +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/MyOFTMock.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. + +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 `MyOFTMock`, which has a public `mint` function + - In `layerzero.config.ts`, ensure you are not using `MyOFTMock` 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.

- Quickstart | Configuration | Message Execution Options | Endpoint, MessageLib, & Executor Addresses | DVN Addresses + Join our community! | Follow us on X (formerly Twitter)

-

Template project for getting started with LayerZero's OFT contract standard.

+# Appendix -

+## Running Tests -- [So What is an Omnichain Fungible Token?](#so-what-is-an-omnichain-fungible-token) -- [Available Helpers in this Repo](#layerzero-hardhat-helper-tasks) +Similar 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 +``` -## So what is an Omnichain Fungible Token? +## Adding other chains -The Omnichain Fungible Token (OFT) Standard is an ERC20 token that can be transferred across multiple blockchains without asset wrapping or middlechains. + -LayerZero +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`. -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. + -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. +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 + +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, @@ -58,10 +357,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
@@ -70,84 +375,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
@@ -168,32 +482,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
@@ -258,7 +554,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
@@ -285,231 +581,10 @@ 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 -``` - -#### Compiling your contracts - -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 -``` - -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. - -

- -## Estimating `lzReceive` and `lzCompose` Gas Usage - -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 - \ - \ - \ - \ - \ - \ - \ - \ - - -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 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. - -## Connecting Contracts - -This example uses the [Simple Config Generator](https://docs.layerzero.network/v2/developers/evm/technical-reference/simple-config), which is recommended over manual configuration. - -### Generate [LZ Config](https://docs.layerzero.network/v2/concepts/glossary#lz-config) file based on hardhat.config.ts - -Fill out your `layerzero.config.ts` with the contracts you want to connect. You can generate the default [LZ Config](https://docs.layerzero.network/v2/concepts/glossary#lz-config) file for your declared hardhat networks (in `hardhat.config.ts`) by running: - -```bash -npx hardhat lz:oapp:config:init --contract-name [YOUR_CONTRACT_NAME] --oapp-config [CONFIG_NAME] -``` - -### Customize values in the LZ Config - -> [!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 optimismContract: OmniPointHardhat = { - eid: EndpointId.OPTSEP_V2_TESTNET, - contractName: "MyOFTAdapter", -}; - -const avalancheContract: OmniPointHardhat = { - eid: EndpointId.AVALANCHE_V2_TESTNET, - contractName: "MyOFT", -}; -``` - -### Apply configurations - -After applying the desired settings, run: - -```bash -npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts -``` - -Congratulations! Your contracts are now wired and can begin sending messages to each other. - ### Manual Configuration + + This section only applies if you would like to configure manually instead of using the Simple Config Generator. Define the pathway you want to create from and to each contract: @@ -557,7 +632,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. @@ -628,6 +703,14 @@ connections: [ ]; ``` -

- Join our community! | Follow us on X (formerly Twitter) -

+### 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/oft/deploy/MyOFT.ts b/examples/oft/deploy/MyOFT.ts index bcbdeb19b1..8970e45917 100644 --- a/examples/oft/deploy/MyOFT.ts +++ b/examples/oft/deploy/MyOFT.ts @@ -26,9 +26,9 @@ const deploy: DeployFunction = async (hre) => { // For example: // // networks: { - // fuji: { + // 'optimism-testnet': { // ... - // eid: EndpointId.AVALANCHE_V2_TESTNET + // eid: EndpointId.OPTSEP_V2_TESTNET // } // } const endpointV2Deployment = await hre.deployments.get('EndpointV2') diff --git a/examples/oft/deploy/MyOFTMock.ts b/examples/oft/deploy/MyOFTMock.ts new file mode 100644 index 0000000000..cf4b6bb159 --- /dev/null +++ b/examples/oft/deploy/MyOFTMock.ts @@ -0,0 +1,53 @@ +import assert from 'assert' + +import { type DeployFunction } from 'hardhat-deploy/types' + +const contractName = 'MyOFTMock' + +const deploy: DeployFunction = async (hre) => { + const { getNamedAccounts, deployments } = hre + + const { deploy } = deployments + const { deployer } = await getNamedAccounts() + + assert(deployer, 'Missing named deployer account') + + console.log(`Network: ${hre.network.name}`) + console.log(`Deployer: ${deployer}`) + + // This is an external deployment pulled in from @layerzerolabs/lz-evm-sdk-v2 + // + // @layerzerolabs/toolbox-hardhat takes care of plugging in the external deployments + // from @layerzerolabs packages based on the configuration in your hardhat config + // + // For this to work correctly, your network config must define an eid property + // set to `EndpointId` as defined in @layerzerolabs/lz-definitions + // + // For example: + // + // networks: { + // 'optimism-testnet': { + // ... + // eid: EndpointId.OPTSEP_V2_TESTNET + // } + // } + const endpointV2Deployment = await hre.deployments.get('EndpointV2') + + const { address } = await deploy(contractName, { + from: deployer, + args: [ + 'MyOFT', // name + 'MOFT', // symbol + endpointV2Deployment.address, // LayerZero's EndpointV2 address + deployer, // owner + ], + log: true, + skipIfAlreadyDeployed: false, + }) + + console.log(`Deployed contract: ${contractName}, network: ${hre.network.name}, address: ${address}`) +} + +deploy.tags = [contractName] + +export default deploy diff --git a/examples/oft/hardhat.config.ts b/examples/oft/hardhat.config.ts index 44e95bde00..979e85c14d 100644 --- a/examples/oft/hardhat.config.ts +++ b/examples/oft/hardhat.config.ts @@ -59,11 +59,6 @@ const config: HardhatUserConfig = { url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, }, - 'avalanche-testnet': { - eid: EndpointId.AVALANCHE_V2_TESTNET, - url: process.env.RPC_URL_FUJI || 'https://avalanche-fuji.drpc.org', - accounts, - }, 'arbitrum-testnet': { eid: EndpointId.ARBSEP_V2_TESTNET, url: process.env.RPC_URL_ARB_SEPOLIA || 'https://arbitrum-sepolia.gateway.tenderly.co', diff --git a/examples/oft/layerzero.config.ts b/examples/oft/layerzero.config.ts index 1b924c79ce..34fd8de61e 100644 --- a/examples/oft/layerzero.config.ts +++ b/examples/oft/layerzero.config.ts @@ -7,23 +7,16 @@ import type { OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat' const optimismContract: OmniPointHardhat = { eid: EndpointId.OPTSEP_V2_TESTNET, - contractName: 'MyOFT', -} - -const avalancheContract: OmniPointHardhat = { - eid: EndpointId.AVALANCHE_V2_TESTNET, - contractName: 'MyOFT', + contractName: 'MyOFTMock', // Note: change this to 'MyOFT' or your production contract name } const arbitrumContract: OmniPointHardhat = { eid: EndpointId.ARBSEP_V2_TESTNET, - contractName: 'MyOFT', + contractName: 'MyOFTMock', // Note: change this to 'MyOFT' or your production contract name } // To connect all the above chains to each other, we need the following pathways: -// Optimism <-> Avalanche // Optimism <-> Arbitrum -// Avalanche <-> Arbitrum // 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 @@ -40,13 +33,6 @@ const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ // 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 @@ -54,20 +40,13 @@ const pathways: TwoWayConfig[] = [ [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 }], + contracts: [{ contract: optimismContract }, { contract: arbitrumContract }], connections, } } 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) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0bab7cd994..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 @@ -2533,6 +2538,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 +2559,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 +4045,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 +4230,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 +4758,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 +5385,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 +5496,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 +10326,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 +10357,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 +10465,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 +10792,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 +10823,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 +19755,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 +20195,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} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ff6d4059f6..5589d5e8ee 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,5 @@ packages: + - "docs" - "examples/*" - "packages/*" - "tests/*" diff --git a/tests-user/tests/create-lz-oapp.bats b/tests-user/tests/create-lz-oapp.bats index 29694a915a..0a33b3cc41 100644 --- a/tests-user/tests/create-lz-oapp.bats +++ b/tests-user/tests/create-lz-oapp.bats @@ -121,8 +121,8 @@ teardown() { cd "$DESTINATION" pnpm compile pnpm test - pnpm lint pnpm lint:fix + pnpm lint } @test "should work with pnpm & oft example in CI mode" { @@ -132,8 +132,8 @@ teardown() { cd "$DESTINATION" pnpm compile pnpm test - pnpm lint pnpm lint:fix + pnpm lint } @test "should work with pnpm & onft721 example in CI mode" { @@ -154,8 +154,8 @@ teardown() { cd "$DESTINATION" pnpm compile pnpm test - pnpm lint pnpm lint:fix + pnpm lint } @test "should work with pnpm & native-oft-adapter example in CI mode" {