> ## Documentation Index
> Fetch the complete documentation index at: https://seilabs-docs-evm-cookbook.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# LayerZero V2 on SEI - Complete Integration Guide

> Step-by-step guide to building cross-chain applications on SEI using LayerZero V2

## Overview

This step-by-step guide shows how to integrate LayerZero V2 with Sei. By the end of the guide, you will know how to create, deploy, and manage omnichain tokens. These tokens can move between Sei and any other LayerZero-supported blockchain.

### What this guide covers

* Core concepts and architecture of LayerZero
* Creating a LayerZero-enabled project from scratch
* Building an Omnichain Fungible Token (OFT), with detailed explanations
* Deploying and configuring the contracts on Sei and other chains, step by step
* Creating tasks and scripts for cross-chain token transfers

### Prerequisites

* Basic knowledge of Solidity and smart contracts
* Node.js and npm installed
* A wallet with SEI and other chain tokens for gas
* Familiarity with Hardhat or Foundry

## What is LayerZero?

LayerZero is an omnichain interoperability protocol for secure, permissionless communication between different blockchains. Think of it as a universal translator that lets blockchains talk to each other.

### Core concepts

* **Endpoints**: Immutable smart contracts on each blockchain that are the entry and exit points for messages
* **Messages**: Data packets, sent between blockchains, that contain instructions or information
* **Security Stack**: A customizable verification system that makes sure messages are authentic
* **Omnichain Applications**: Smart contracts that can operate across multiple blockchains

### Key applications

* **Omnichain Fungible Tokens (OFT)**: Tokens that exist on multiple chains with a unified supply
* **Omnichain NFTs (ONFT)**: NFTs that can move between blockchains
* **Cross-chain DeFi**: Protocols that operate across multiple networks
* **Unified governance**: DAOs that function across chains
* **Message passing**: Send arbitrary data between blockchains

## Sei network information

<Info>
  **Sei Mainnet configuration**

  * Network Name: SEI EVM
  * Chain ID: 1329
  * RPC URL: [https://evm-rpc.sei-apis.com](https://evm-rpc.sei-apis.com)
  * Explorer: [https://seiscan.io](https://seiscan.io)
  * Currency: SEI
  * LayerZero Endpoint ID: 30280
</Info>

### Contract deployments

These are the addresses of the official LayerZero V2 protocol contracts on Sei networks.

<Accordion title="Sei Mainnet (EID: 30280) — protocol contracts">
  | Contract | Address |
  | - | - |
  | EndpointV2 | [`0x1a44076050125825900e736c501f859c50fE728c`](https://seiscan.io/address/0x1a44076050125825900e736c501f859c50fE728c) |
  | SendUln302 | [`0xC39161c743D0307EB9BCc9FEF03eeb9Dc4802de7`](https://seiscan.io/address/0xC39161c743D0307EB9BCc9FEF03eeb9Dc4802de7) |
  | ReceiveUln302 | [`0xe1844c5D63a9543023008D332Bd3d2e6f1FE1043`](https://seiscan.io/address/0xe1844c5D63a9543023008D332Bd3d2e6f1FE1043) |
  | SendUln301 | [`0x37aaaf95887624a363effB7762D489E3C05c2a02`](https://seiscan.io/address/0x37aaaf95887624a363effB7762D489E3C05c2a02) |
  | ReceiveUln301 | [`0x15e51701F245F6D5bd0FEE87bCAf55B0841451B3`](https://seiscan.io/address/0x15e51701F245F6D5bd0FEE87bCAf55B0841451B3) |
  | Executor | [`0xc097ab8CD7b053326DFe9fB3E3a31a0CCe3B526f`](https://seiscan.io/address/0xc097ab8CD7b053326DFe9fB3E3a31a0CCe3B526f) |
  | LZ Executor | [`0x4208D6E27538189bB48E603D6123A94b8Abe0A0b`](https://seiscan.io/address/0x4208D6E27538189bB48E603D6123A94b8Abe0A0b) |
  | EndpointV2View | [`0xAaB5A48CFC03Efa9cC34A2C1aAcCCB84b4b770e4`](https://seiscan.io/address/0xAaB5A48CFC03Efa9cC34A2C1aAcCCB84b4b770e4) |
  | ReadLib1002 | [`0x6A6C548058094e6dFaF95Bb0b5d04e1dd8ec0870`](https://seiscan.io/address/0x6A6C548058094e6dFaF95Bb0b5d04e1dd8ec0870) |
  | BlockedMessageLib | [`0x1ccbf0db9c192d969de57e25b3ff09a25bb1d862`](https://seiscan.io/address/0x1ccbf0db9c192d969de57e25b3ff09a25bb1d862) |
</Accordion>

<Accordion title="Sei Mainnet — DVNs (decentralized verifier networks)">
  | DVN | Address |
  | - | - |
  | LayerZero Labs | [`0x6788f52439aca6bff597d3eec2dc9a44b8fee842`](https://seiscan.io/address/0x6788f52439aca6bff597d3eec2dc9a44b8fee842) |
  | Nethermind | [`0xa93d4490444eba60839742859cfde53bf0967dc9`](https://seiscan.io/address/0xa93d4490444eba60839742859cfde53bf0967dc9) |
  | Horizen | [`0x96ca3420bedd887cabdaba29c7dce6bad57af98b`](https://seiscan.io/address/0x96ca3420bedd887cabdaba29c7dce6bad57af98b) |
  | BitGo | [`0x26cd5abadf7ec3f0f02b48314bfca6b2342cddd4`](https://seiscan.io/address/0x26cd5abadf7ec3f0f02b48314bfca6b2342cddd4) |
  | StakingCabin | [`0x93d2d7aadc9f2cf5ebc88e9703e06db09b8fd85b`](https://seiscan.io/address/0x93d2d7aadc9f2cf5ebc88e9703e06db09b8fd85b) |
  | Luganodes | [`0x6e01aa282f058873d28055e07d85f4197e8db261`](https://seiscan.io/address/0x6e01aa282f058873d28055e07d85f4197e8db261) |
  | P2P | [`0xa83a87a0bdce466edfbb6794404e1d7f556b8f20`](https://seiscan.io/address/0xa83a87a0bdce466edfbb6794404e1d7f556b8f20) |
  | BCW Group | [`0x1feb08b1a53a9710afce82d380b8c2833c69a37e`](https://seiscan.io/address/0x1feb08b1a53a9710afce82d380b8c2833c69a37e) |
  | Nansen | [`0xf85f51c1d5b4de2446d99b104acfca7ff63bd3ad`](https://seiscan.io/address/0xf85f51c1d5b4de2446d99b104acfca7ff63bd3ad) |
  | Stargate | [`0xbd00c87850416db0995ef8030b104f875e1bdd15`](https://seiscan.io/address/0xbd00c87850416db0995ef8030b104f875e1bdd15) |
  | Frax | [`0xe016f0f39fb7dcf14e9412d92f2049668d4d2612`](https://seiscan.io/address/0xe016f0f39fb7dcf14e9412d92f2049668d4d2612) |
</Accordion>

<Accordion title="Sei Testnet (EID: 40455) — protocol contracts">
  | Contract | Address |
  | - | - |
  | EndpointV2 | [`0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff`](https://testnet.seiscan.io/address/0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff) |
  | SendUln302 | [`0xd682ECF100f6F4284138AA925348633B0611Ae21`](https://testnet.seiscan.io/address/0xd682ECF100f6F4284138AA925348633B0611Ae21) |
  | ReceiveUln302 | [`0xcF1B0F4106B0324F96fEfcC31bA9498caa80701C`](https://testnet.seiscan.io/address/0xcF1B0F4106B0324F96fEfcC31bA9498caa80701C) |
  | BlockedMessageLib | [`0x926984a57b10a3a5c4cfdbac04daaa0309e78932`](https://testnet.seiscan.io/address/0x926984a57b10a3a5c4cfdbac04daaa0309e78932) |
  | Executor | [`0x9dB9Ca3305B48F196D18082e91cB64663b13d014`](https://testnet.seiscan.io/address/0x9dB9Ca3305B48F196D18082e91cB64663b13d014) |
  | DeadDVN | [`0xF49d162484290EAeAd7bb8C2c7E3a6f8f52e32d6`](https://testnet.seiscan.io/address/0xF49d162484290EAeAd7bb8C2c7E3a6f8f52e32d6) |
</Accordion>

## Step 1: Project setup

### Project scaffold

LayerZero's CLI lets you create an OFT workspace in seconds:

```bash theme={null}
npx create-lz-oapp@latest # choose → "OFT example"
```

The wizard creates a repo with a Hardhat and Foundry setup, sample contracts, tests, and LayerZero helper scripts.

### Add private keys

Rename the `.env.example` file to `.env`. Then add the configuration values that you need:

```bash title=".env" theme={null}
PRIVATE_KEY=your_private_key
```

At a minimum, you need the `PRIVATE_KEY`. RPC URLs are optional, but strongly recommended. If you do not set them, the project uses public RPCs. Public RPCs can be unreliable or slow. Your transactions can then take a long time to confirm or, at worst, fail.

## Step 2: Configure networks

### Hardhat network config

In `hardhat.config.ts`, add the networks that you want to deploy your contracts to:

```typescript title="hardhat.config.ts" theme={null}
networks: {
  // the network you are deploying to or are already on
  // Sei Mainnet (EID=30280)
  'sei-mainnet': {
    eid: EndpointId.SEI_V2_MAINNET,
    url: process.env.RPC_URL_SEI || 'https://evm-rpc.sei-apis.com',
    accounts,
  },
  // another network you want to connect to
  'optimism-mainnet': {
    eid: EndpointId.OPTIMISM_V2_MAINNET,
    url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io',
    accounts,
  },
}
```

### LayerZero wiring config

In `layerzero.config.ts`, add the chains and the channel security settings that you want for each connection:

```typescript title="layerzero.config.ts" theme={null}
import { EndpointId } from '@layerzerolabs/lz-definitions';
import type { OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat';
import { OAppEnforcedOption } from '@layerzerolabs/toolbox-hardhat';
import { ExecutorOptionType } from '@layerzerolabs/lz-v2-utilities';
import { TwoWayConfig, generateConnectionsConfig } from '@layerzerolabs/metadata-tools';

const seiContract: OmniPointHardhat = {
  eid: EndpointId.SEI_V2_MAINNET,
  contractName: 'MyOFT'
};

const optimismContract: OmniPointHardhat = {
  eid: EndpointId.OPTIMISM_V2_MAINNET,
  contractName: 'MyOFT'
};

// To connect all the above chains to each other, we need the following pathways:
// Optimism <-> sei
// sei <-> Optimism

// For this example's simplicity, we will use the same enforced options values for sending to all chains
// To learn more, read https://docs.layerzero.network/v2/concepts/applications/oapp-standard#execution-options-and-enforced-settings
const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [
  {
    msgType: 1,
    optionType: ExecutorOptionType.LZ_RECEIVE,
    gas: 80000,
    value: 0
  }
];

const pathways: TwoWayConfig[] = [
  [
    // 1) Chain B's contract (e.g. Optimism)
    optimismContract,
    // 2) Chain A's contract (e.g. sei)
    seiContract,
    // 3) Channel security settings:
    // • first array = "required" DVN names
    // • second array = "optional" DVN names array + threshold
    // • third value = threshold (i.e., number of optionalDVNs that must sign)
    // [ requiredDVN[], [ optionalDVN[], threshold ] ]
    [['LayerZero Labs' /* ← add more DVN names here */], []],
    // 4) Block confirmations:
    // [confirmations for Optimism → sei, confirmations for sei → Optimism]
    [1, 1],
    // 5) Enforced execution options:
    // [options for Optimism → sei, options for sei → Optimism]
    [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS]
  ]
];

export default async function () {
  // Generate the connections config based on the pathways
  const connections = await generateConnectionsConfig(pathways);
  return {
    contracts: [{ contract: optimismContract }, { contract: seiContract }],
    connections
  };
}
```

<Info>
  Review [LayerZero's Channel Security Model](https://docs.layerzero.network/v2/concepts/protocol/message-security#layerzero's-channel-security-model) to understand the impact of each of these configuration settings.

  To review the available providers and security settings, see [Next steps](#next-steps).
</Info>

## Step 3: Create utility tasks

Before you deploy, create a utility task that mints tokens. You need this task later.

### Create minting task

Create `tasks/mint.ts`:

```typescript title="tasks/mint.ts" theme={null}
import { task } from 'hardhat/config';
import { types } from '@layerzerolabs/devtools-evm-hardhat';

task('mint', 'Mint tokens to an address')
  .addParam('to', 'Address to mint tokens to', undefined, types.string)
  .addParam('amount', 'Amount to mint (without decimals)', undefined, types.string)
  .setAction(async ({ to, amount }, { ethers, deployments }) => {
    // Get the deployed contract
    const deployment = await deployments.get('MyOFT');
    const [signer] = await ethers.getSigners();

    // Create contract instance
    const oft = new ethers.Contract(deployment.address, deployment.abi, signer);

    // Get decimals
    const decimals = await oft.decimals();

    // Convert amount to proper decimals
    const amountWithDecimals = ethers.utils.parseUnits(amount, decimals);

    // Mint tokens
    console.log(`Minting ${amount} tokens to ${to}...`);
    const tx = await oft.mint(to, amountWithDecimals);
    await tx.wait();

    console.log(`✅ Minted ${amount} tokens to ${to}`);
    console.log(`Transaction hash: ${tx.hash}`);
  });
```

### Update Hardhat configuration

Update your `hardhat.config.ts` file to import the mint task:

```typescript title="hardhat.config.ts" theme={null}
// Get the environment configuration from .env file
//
// To make use of automatic environment setup:
// - Duplicate .env.example file and name it .env
// - Fill in the environment variables
import 'dotenv/config';

import 'hardhat-deploy';
import 'hardhat-contract-sizer';
import '@nomiclabs/hardhat-ethers';
import '@layerzerolabs/toolbox-hardhat';
import { HardhatUserConfig, HttpNetworkAccountsUserConfig } from 'hardhat/types';

import { EndpointId } from '@layerzerolabs/lz-definitions';

import './tasks/sendOFT';
import './tasks/mint';

// Set your preferred authentication method
//
// If you prefer using a mnemonic, set a MNEMONIC environment variable
// to a valid mnemonic
const MNEMONIC = process.env.MNEMONIC;

// If you prefer to be authenticated using a private key, set a PRIVATE_KEY environment variable
const PRIVATE_KEY = process.env.PRIVATE_KEY;

const accounts: HttpNetworkAccountsUserConfig | undefined = MNEMONIC ? { mnemonic: MNEMONIC } : PRIVATE_KEY ? [PRIVATE_KEY] : undefined;

if (accounts == null) {
  console.warn('Could not find MNEMONIC or PRIVATE_KEY environment variables. It will not be possible to execute transactions in your example.');
}

const config: HardhatUserConfig = {
  paths: {
    cache: 'cache/hardhat'
  },
  solidity: {
    compilers: [
      {
        version: '0.8.22',
        settings: {
          optimizer: {
            enabled: true,
            runs: 200
          }
        }
      }
    ]
  },
  networks: {
    // the network you are deploying to or are already on
    // Sei Mainnet (EID=30280)
    'sei-mainnet': {
      eid: EndpointId.SEI_V2_MAINNET,
      url: process.env.RPC_URL_SEI || 'https://evm-rpc.sei-apis.com',
      accounts
    },
    // another network you want to connect to
    'optimism-mainnet': {
      eid: EndpointId.OPTIMISM_V2_MAINNET,
      url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io',
      accounts
    }
  },
  namedAccounts: {
    deployer: {
      default: 0 // wallet address of index[0], of the mnemonic in .env
    }
  }
};

export default config;
```

<Warning>Create and import the mint task before you deploy. Otherwise, Hardhat throws an error when it tries to resolve the task dependencies during compilation.</Warning>

## Step 4: Smart contract development

### The token contract

<Info>
  The OFT contract handles all cross-chain logic automatically. When you send tokens:

  1. **Source chain**: Burns the tokens from the sender's balance
  2. **LayerZero protocol**: Verifies and relays the message
  3. **Destination chain**: Mints equivalent tokens to the recipient
</Info>

```solidity title="contracts/MyOFT.sol" theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol";

contract MyOFT is OFT {
    constructor(string memory name, string memory symbol, address endpoint, address owner)
        OFT(name, symbol, endpoint, owner) Ownable(owner) {}

    /**
     * @notice Mint new tokens (only owner)
     * @param to Address to receive the minted tokens
     * @param amount Amount of tokens to mint (with decimals)
     */
    function mint(address to, uint256 amount) external onlyOwner {
        _mint(to, amount);
    }
}
```

<Warning>
  The OFT contract uses the ERC-20 token standard. If this is your first time deploying an OFT, you may want to add a `mint(...)` function in the `constructor(...)` or contract body. If you have an existing ERC-20 token, use an OFT Adapter contract.

  To learn more about how OFTs work and which contracts to use, you can read the general [OFT Quickstart](https://docs.layerzero.network/v2/developers/evm/oft/quickstart).
</Warning>

### Understanding OFT functions

Your contract inherits these main functions from the OFT contract:

| Function | Description |
| - | - |
| `send()` | Starts a cross-chain token transfer |
| `quoteSend()` | Estimates the fees for a transfer |
| `_lzSend()` | Internal function that sends the message |
| `_lzReceive()` | Internal function that receives messages |

## Step 5: Deployment process

### Deploy

```bash theme={null}
npx hardhat lz:deploy # choose sei
```

The command shows a list of networks to deploy to.

<Warning>Fund your deployer with native gas tokens before you deploy.</Warning>

## Step 6: Wire the contracts

After deployment, connect the contracts:

### Connect the chains

```bash theme={null}
npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts
```

### Verify configuration

Verify the peers:

```bash theme={null}
npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts
```

This command shows the current LayerZero wiring configuration, including the connected chains and their peer addresses.

## Step 7: Mint initial tokens

Before you transfer, you need tokens to send. Mint some tokens on Sei:

```bash theme={null}
# Mint 10,000 tokens to your address on SEI
npx hardhat mint --to 0xYourAddress --amount 10000 --network sei-mainnet
```

## Step 8: Transfer tokens cross-chain

### Calling send

The OFT contract already defines the `send` logic, so this section shows how to call the function.

#### Option 1: Hardhat task

Create `tasks/sendOFT.ts`:

```typescript title="tasks/sendOFT.ts" theme={null}
import { task } from 'hardhat/config';
import { getNetworkNameForEid, types } from '@layerzerolabs/devtools-evm-hardhat';
import { EndpointId } from '@layerzerolabs/lz-definitions';
import { addressToBytes32 } from '@layerzerolabs/lz-v2-utilities';
import { Options } from '@layerzerolabs/lz-v2-utilities';
import { BigNumberish, BytesLike } from 'ethers';

interface Args {
  amount: string;
  to: string;
  toEid: EndpointId;
}

interface SendParam {
  dstEid: EndpointId; // Destination endpoint ID, represented as a number.
  to: BytesLike; // Recipient address, represented as bytes.
  amountLD: BigNumberish; // Amount to send in local decimals.
  minAmountLD: BigNumberish; // Minimum amount to send in local decimals.
  extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message.
  composeMsg: BytesLike; // The composed message for the send() operation.
  oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations.
}

// send tokens from a contract on one network to another
task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter')
  .addParam('to', 'contract address on network B', undefined, types.string)
  .addParam('toEid', 'destination endpoint ID', undefined, types.eid)
  .addParam('amount', 'amount to transfer in token decimals', undefined, types.string)
  .setAction(async (taskArgs: Args, { ethers, deployments }) => {
    const toAddress = taskArgs.to;
    const eidB = taskArgs.toEid;

    // Get the contract factories
    const oftDeployment = await deployments.get('MyOFT');

    const [signer] = await ethers.getSigners();

    // Create contract instances
    const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer);

    const decimals = await oftContract.decimals();
    const amount = ethers.utils.parseUnits(taskArgs.amount, decimals);
    let options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes();

    // Now you can interact with the correct contract
    const oft = oftContract;

    const sendParam: SendParam = {
      dstEid: eidB,
      to: addressToBytes32(toAddress),
      amountLD: amount,
      minAmountLD: amount,
      extraOptions: options,
      composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message
      oftCmd: ethers.utils.arrayify('0x') // Assuming no OFT command is needed
    };
    // Get the quote for the send operation
    const feeQuote = await oft.quoteSend(sendParam, false);
    const nativeFee = feeQuote.nativeFee;

    console.log(`sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`);

    const ERC20Factory = await ethers.getContractFactory('ERC20');
    const innerTokenAddress = await oft.token();

    // // If the token address !== address(this), then this is an OFT Adapter
    // if (innerTokenAddress !== oft.address) {
    //     // If the contract is OFT Adapter, get decimals from the inner token
    //     const innerToken = ERC20Factory.attach(innerTokenAddress);

    //     // Approve the amount to be spent by the oft contract
    //     await innerToken.approve(oftDeployment.address, amount);
    // }

    const r = await oft.send(sendParam, { nativeFee: nativeFee, lzTokenFee: 0 }, signer.address, {
      value: nativeFee
    });
    console.log(`Send tx initiated. See: https://layerzeroscan.com/tx/${r.hash}`);
  });
```

If you have not already done so, import the task in your `hardhat.config.ts` file:

```typescript theme={null}
// Add this import to hardhat.config.ts (should already be there from Step 3)
import './tasks/sendOFT';
```

Send the tokens:

```bash theme={null}
# Send 100 tokens from SEI to Optimism (EID: 30111)
npx hardhat lz:oft:send \
  --to 0xRecipientAddress \
  --toEid 30111 \
  --amount 100 \
  --network sei-mainnet
```

#### Option 2: Foundry script

Create `script/SendOFT.s.sol`:

```solidity title="script/SendOFT.s.sol" theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";
import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol";
import { SendParam, OFTReceipt } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol";
import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol";
import { MessagingFee } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
import { MyOFT } from "../contracts/MyOFT.sol";

contract SendOFT is Script {
    using OptionsBuilder for bytes;

    /**
     * @dev Converts an address to bytes32.
     * @param _addr The address to convert.
     * @return The bytes32 representation of the address.
     */
    function addressToBytes32(address _addr) internal pure returns (bytes32) {
        return bytes32(uint256(uint160(_addr)));
    }

    function run() public {
        // Fetching environment variables
        address oftAddress = vm.envAddress("OFT_ADDRESS");
        address toAddress = vm.envAddress("TO_ADDRESS");
        uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND");

        // Fetch the private key from environment variable
        uint256 privateKey = vm.envUint("PRIVATE_KEY");

        // Start broadcasting with the private key
        vm.startBroadcast(privateKey);

        MyOFT sourceOFT = MyOFT(oftAddress);

        bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0);

        SendParam memory sendParam = SendParam(
            30111, // You can also make this dynamic if needed
            addressToBytes32(toAddress),
            _tokensToSend,
            _tokensToSend * 9 / 10,
            _extraOptions,
            "",
            ""
        );

        MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false);
        console.log("Fee amount: ", fee.nativeFee);

        sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender);

        // Stop broadcasting
        vm.stopBroadcast();
    }
}
```

Set the environment variables and run the script:

```bash theme={null}
# Set environment variables
export OFT_ADDRESS=0xYourOFTAddress
export TO_ADDRESS=0xRecipientAddress
export TOKENS_TO_SEND=100000000000000000000  # 100 tokens with 18 decimals

# Run the script
forge script script/SendOFT.s.sol:SendOFT --rpc-url $RPC_URL_SEI --broadcast
```

## Step 9: Done!

You issued an omnichain token and bridged it from Sei Mainnet to Optimism. To customize the supply logic or fees, or to add more chains, change the core contract. Then redeploy it and repeat the wiring step.

### Track your transaction

1. Go to [https://layerzeroscan.com/tx/YOUR\_TX\_HASH](https://layerzeroscan.com/tx/YOUR_TX_HASH)
2. The page shows:
   * Source transaction
   * Message status
   * Destination transaction
   * Time elapsed
3. Check the balances on both chains to confirm the transfer

## Troubleshooting

### Common issues and solutions

<Danger>
  **Quote send reverts**

  If your `quoteSend` call reverts, there are two usual causes. Your LayerZero wiring is not fully configured, or there is no default pathway for the chains that you are trying to bridge. Diagnose and fix the problem as follows:

  **1. Wiring did not succeed:**

  Run this command to inspect your on-chain wiring configuration:

  ```bash theme={null}
  npx hardhat lz:oapp:config:get --oapp-config layerzero.config.ts
  ```

  Check that your source configuration has a valid send library, DVN address, and target EID.

  **2. No default pathway:**

  Treat the LayerZero default settings as placeholders. Sometimes the LayerZero defaults contain a `LzDeadDVN`. These entries show that a default pathway setting does not exist.

  * **Check**: To see if your configuration contains a LzDeadDVN, open the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey%5B0%5D=sei\&version=V2\&dstChainKey%5B0%5D=optimism) on LayerZero Scan.

  * **Fix**: Open your `layerzero.config.ts` file. Under the relevant `pathways` entry, add working DVN providers in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section.

  * Run the wiring command for the connections again so that the wiring is live on both chains.

  ```typescript theme={null}
  [
    optimismContract,
    currentContract,
    [['LayerZero Labs'], []], // required & optional DVNs
    [1, 1], // required block confirmations
    [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS],
  ],
  ```

  After you update your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting.
</Danger>

<Tip>
  **Insufficient gas**

  If the transaction fails on the destination chain, try to increase the gas:

  ```typescript theme={null}
  // Increase gas in options
  let options = Options.newOptions().addExecutorLzReceiveOption(150000, 0).toBytes(); // Increase from 65000 to 150000
  ```
</Tip>

## Next steps

* Add more chains, such as Arbitrum, Base, and Polygon.
* Implement advanced features, such as rate limiting, pausable transfers, and fee collection.
* Use composed messages to run actions after the tokens arrive.
* Build a frontend UI that makes transfers easy.

### Learn more

* To learn more about how to manage your OFTs across the entire network mesh, see the [Configuring Pathways](https://docs.layerzero.network/v2/get-started/create-lz-oapp/configuring-pathways) section.
* To choose which DVNs and executor to configure, see [Available DVNs](https://docs.layerzero.network/v2/deployments/dvn-addresses?chains=sei) and [Executor](https://docs.layerzero.network/v2/deployments/deployed-contracts?chains=sei).
* To learn how the protocol works, read the [Core Concepts](https://docs.layerzero.network/v2/concepts/getting-started/what-is-layerzero) section.
* To learn more about how the OFT contract works on the EVM, read the [OFT Quickstart](https://docs.layerzero.network/v2/developers/evm/oft/quickstart) in the EVM section.

## Resources

* **LayerZero documentation**: [https://docs.layerzero.network/v2](https://docs.layerzero.network/v2)
* **LayerZero Scan**: [https://layerzeroscan.com/](https://layerzeroscan.com/)
* **Sei explorer**: [https://seiscan.io](https://seiscan.io)

## Summary

In this guide, you:

* Created an Omnichain Fungible Token project
* Configured networks and LayerZero pathways
* Deployed contracts to multiple chains
* Connected the deployments through LayerZero wiring
* Created tasks for cross-chain token transfers
* Learned how LayerZero enables omnichain applications

Your tokens can now move between Sei and any connected chain, and the total supply stays unified.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.