Skip to content

hedera-dev/template-hedera-lz-app

Repository files navigation

Hedera-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 connecting EVM chains, like Hedera, to other EVM chains.

Table of Contents

Prerequisite Knowledge

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:

git clone https://github.com/hedera-dev/template-hedera-lz-app
cd template-hedera-lz-app
pnpm install

Setup

  1. Copy .env.example into a new .env
  2. Add your Hedera Portal or existing deployer address/account to the .env
  3. If using an existing account, fund it with the native tokens of the chains you want to deploy to e.g. the Hedera Faucet. This example by default will deploy to the following chains' testnets: Hedera and Base.

Build

Compiling your contracts

This project supports both hardhat and forge compilation. By default, the compile command will execute both:

pnpm compile

If you prefer one over the other, you can use the tooling-specific commands:

pnpm compile:forge
pnpm compile:hardhat

Deploy

To deploy the OFT contracts to your desired blockchains, run the following command:

pnpm hardhat lz:deploy --tags MyOFT

Select all the chains you want to deploy the OFT to.

Simple Workers (For Testnets Without Default Workers)

⚠️ Development Only: Simple Workers are mock implementations for testing on testnets. They should NEVER be used in production as they provide no security or service guarantees. For mainnet use the LayerZero worker addresses.

What Are Simple Workers?

Simple Workers consist of:

  • SimpleDVNMock: A minimal DVN that allows manual message verification
  • SimpleExecutorMock: A mock executor that charges zero fees and enables manual message execution

Deploying Simple Workers

Deploy the Simple Workers:

# Deploy SimpleDVNMock
pnpm hardhat lz:deploy --tags SimpleDVNMock

# Deploy SimpleExecutorMock
pnpm hardhat lz:deploy --tags SimpleExecutorMock

Configuring Simple Workers

You can now use custom DVNs and Executors with the standard lz:oapp:wire command by adding them to your metadata configuration.

  1. Get your deployed addresses from the deployment files:

    • SimpleDVNMock: ./deployments/<network-name>/SimpleDVNMock.json
    • SimpleExecutorMock: ./deployments/<network-name>/SimpleExecutorMock.json
  2. Update your layerzero.simple-worker.config.ts to include your deployed Simple Workers:

    • SECTION 4: Add your Simple Worker addresses:
// In layerzero.simple-worker.config.ts, SECTION 4: CUSTOM EXECUTOR AND DVN ADDRESSES
const customExecutorsByEid: Record<number, { address: string }> = {
  [EndpointId.BASESEP_V2_TESTNET]: { address: "0x..." }, // From deployments/base-sepolia/SimpleExecutorMock.json
  [EndpointId.HEDERA_V2_TESTNET]: { address: "0x..." }, // From deployments/hedera-testnet/SimpleExecutorMock.json
};

const customDVNsByEid: Record<number, { address: string }> = {
  [EndpointId.BASESEP_V2_TESTNET]: { address: "0x..." }, // From deployments/base-sepolia/SimpleDVNMock.json
  [EndpointId.HEDERA_V2_TESTNET]: { address: "0x..." }, // From deployments/hedera-testnet/SimpleDVNMock.json
};
  1. Wire normally using the custom configuration:
pnpm hardhat lz:oapp:wire --oapp-config layerzero.simple-worker.config.ts

This command will automatically:

  • Detect pathways without DVN configurations in your LayerZero config
  • Configure SimpleDVNMock and SimpleExecutorMock for those pathways
  • Set both send and receive configurations on the appropriate chains
  • Skip pathways that already have DVN configurations

ℹ️ The command only configures pathways with empty DVN arrays, preserving any existing configurations.

Sending OFTs with Simple Workers

When sending OFTs with Simple Workers, add the --simple-workers flag to enable the manual verification and execution flow:

# Hedera Testnet -> Base Sepolia
pnpm hardhat lz:oft:send --src-eid 40285 --dst-eid 40245 --amount 1 --to <RECIPIENT> --simple-workers

# Base Sepolia -> Hedera Testnet
pnpm hardhat lz:oft:send --src-eid 40245 --dst-eid 40285 --amount 1 --to <RECIPIENT> --simple-workers

With the --simple-workers flag, the task will:

  1. Send the OFT transaction as normal
  2. Automatically trigger the manual verification process on the destination chain
  3. Execute the message delivery through the Simple Workers

Simple Workers Architecture

The manual verification flow involves three steps on the destination chain:

  1. Verify: SimpleDVNMock verifies the message payload
  2. Commit: SimpleDVNMock commits the verification to the ULN
  3. Execute: SimpleExecutorMock executes the message delivery

Without the --simple-workers flag, you would need to manually call these steps using the provided tasks:

  • lz:oapp:wire:simple-workers - Configure Simple Workers for all pathways without DVN configurations
  • lz:simple-dvn:verify - Verify the message with SimpleDVNMock
  • lz:simple-dvn:commit - Commit the verification to ULN
  • lz:simple-workers:commit-and-execute - Execute the message delivery
  • lz:simple-workers:skip - Skip a stuck message (permanent action!)

Important Limitations

  • Zero Fees: Simple Workers charge no fees, breaking the economic security model
  • No Real Verification: Messages are manually verified without actual validation
  • Testnet Only: These mocks provide no security and must never be used on mainnet
  • Manual Process: Requires manual intervention or the --simple-workers flag for automation

Troubleshooting Simple Workers

Ordered Message Delivery

LayerZero enforces ordered message delivery per channel (source → destination). Messages must be processed in the exact order they were sent. If a message fails or is skipped, all subsequent messages on that channel will be blocked.

Common Error: "InvalidNonce"

warn: Lazy inbound nonce is not equal to inboundNonce + 1. You will run into an InvalidNonce error.

This means there are pending messages that must be processed first.

Recovery Options

When a message is stuck, you have two options:

Option 1: Process the Pending Message

# Find the pending nonce from the error message, then:
npx hardhat lz:simple-dvn:verify --src-eid <SRC_EID> --dst-eid <DST_EID> --nonce <PENDING_NONCE> --src-oapp <SRC_OAPP> --to-address <RECIPIENT> --amount <AMOUNT>
npx hardhat lz:simple-workers:commit-and-execute --src-eid <SRC_EID> --dst-eid <DST_EID> --nonce <PENDING_NONCE> ...

Option 2: Skip the Message (Cannot be undone!)

# Skip a stuck message on the destination chain
npx hardhat lz:simple-workers:skip --src-eid <SRC_EID> --src-oapp <SRC_OAPP> --nonce <NONCE_TO_SKIP> --receiver <RECEIVER_OAPP>

⚠️ Skipping is permanent! Once skipped, the message cannot be recovered. The tokens/value in that message will be permanently lost.

RPC Failures During Processing

If your RPC connection fails during --simple-workers processing:

  1. The outbound message may already be sent but not verified/executed
  2. You'll see detailed recovery information in the error output
  3. You must handle this nonce before sending new messages
  4. Either wait for RPC limits to reset and complete processing, or skip the message

Example: Multiple Pending Messages

If nonce 6 fails because nonce 4 is pending:

  1. First process or skip nonce 4
  2. Then process or skip nonce 5
  3. Finally, you can process nonce 6

Remember: All messages must be handled in order!

Next Steps

Now that you've gone through a simplified walkthrough, here are what you can do next.

Production Deployment Checklist

Before deploying, ensure the following:

  • (required) if you previously uncommented the testnet mint line in contracts/MyOFT.sol, ensure it is commented out for production
  • (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.

    "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.

    "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:

pnpm gas:lzReceive
  <rpcUrl> \
  <endpointAddress> \
  <srcEid> \
  <sender> \
  <dstEid> \
  <receiver> \
  <message> \
  <msg.value> \
  <numOfRuns>

Where:

  • rpcUrl: The RPC URL for the target blockchain (e.g., Hedera, Base 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:

pnpm gas:lzCompose
  <rpcUrl> \
  <endpointAddress> \
  <srcEid> \
  <sender> \
  <dstEid> \
  <receiver> \
  <composer> \
  <composeMsg> \
  <msg.value> \
  <numOfRuns>

Where:

  • rpcUrl: The RPC URL for the target blockchain (e.g. Hedera, Base, 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.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published