Skip to content

Latest commit

 

History

History

layerzero-oft

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

README.md

LayerZero OFT — Bridge Tokens from Sei

Bridge tokens between Sei and Ethereum / BSC using LayerZero V2 Omnichain Fungible Tokens (OFT).

This folder contains two projects — pick the one that matches your use case:

Project Use case How it works
oft-token/ Launch a new token that's natively omnichain Same OFT contract deployed on every chain. Burn on source, mint on destination.
oft-adapter/ Bridge an existing ERC-20 token from Sei OFTAdapter on Sei (lock/unlock) + OFT on spokes (mint/burn).

Table of Contents

  1. Prerequisites
  2. Option A — New OFT Token (oft-token)
  3. Option B — OFT Adapter for Existing Token (oft-adapter)
  4. How setPeer Works
  5. Sending Tokens Cross-Chain
  6. LayerZero V2 Endpoint Reference
  7. Switching to Mainnet
  8. Troubleshooting

1. Prerequisites

  • Node.js >= 18
  • A funded wallet on Sei testnet + destination testnet (Sepolia or BSC testnet)

2. Option A — New OFT Token (oft-token)

Use this if you want to create a brand-new token that exists natively on multiple chains. The same SeiOFT contract is deployed on every chain. Tokens are burned on the source chain and minted on the destination chain.

Architecture

Sei (SeiOFT) <── burn/mint ──> Sepolia (SeiOFT)
                <── burn/mint ──> BSC Testnet (SeiOFT)

All chains use the same contract. Initial supply (1B tokens) is minted to the deployer on the first chain you deploy to.

Step 1: Install dependencies

cd oft-token
npm install

Step 2: Configure environment

cp .env.example .env

Edit .env and fill in:

PRIVATE_KEY=your_private_key_here
TOKEN_NAME="My Token"
TOKEN_SYMBOL="MTK"

Step 3: Compile contracts

npm run compile

Step 4: Deploy on Sei testnet (initial supply minted here)

npm run deploy:sei-testnet

Save the deployed address — you'll need it for the .env file.

Step 5: Deploy on spoke chains

Note: The same SeiOFT contract is deployed on spoke chains too. It mints 1B tokens to the deployer by default. If you only want supply to originate from Sei, comment out the _mint line in SeiOFT.sol before deploying on spoke chains.

npm run deploy:sepolia
npm run deploy:bsc-testnet

Step 6: Fill in deployed addresses

Update your .env with all three addresses:

OFT_SEI_ADDRESS=0x...
OFT_SEPOLIA_ADDRESS=0x...
OFT_BSC_TESTNET_ADDRESS=0x...

Step 7: Set peers (bidirectional)

Each OFT contract must know about its peers on other chains. You need to run set-peers on every chain:

# Tell Sei about Sepolia and BSC Testnet
npm run set-peers:sei-testnet

# Tell Sepolia about Sei
npm run set-peers:sepolia

# Tell BSC Testnet about Sei
npm run set-peers:bsc-testnet

Step 8: Send tokens

npm run send:sei-testnet

This sends 1 token from Sei testnet to Sepolia by default. Edit scripts/send-tokens.ts to change the destination EID or amount.

Track your transfer on LayerZero Scan.


3. Option B — OFT Adapter for Existing Token (oft-adapter)

Use this if you already have an ERC-20 token deployed on Sei and want to bridge it to other chains. The adapter locks your tokens on Sei and a paired OFT contract mints wrapped tokens on the destination.

Architecture

Sei (SeiOFTAdapter) <── lock/unlock ──> Sepolia (SeiOFTRemote)
     wraps ERC-20      <── lock/unlock ──> BSC Testnet (SeiOFTRemote)
  • Sei: SeiOFTAdapter — wraps your existing token. Tokens are locked when bridging out, unlocked when bridging back.
  • Spokes: SeiOFTRemote — an OFT with no initial supply. Tokens are minted when receiving from Sei, burned when sending back.

Step 1: Install dependencies

cd oft-adapter
npm install

Step 2: Configure environment

cp .env.example .env

Edit .env and fill in:

PRIVATE_KEY=your_private_key_here
EXISTING_TOKEN_ADDRESS=0x_your_sei_token_address
TOKEN_NAME="My Bridged Token"
TOKEN_SYMBOL="mBTK"

EXISTING_TOKEN_ADDRESS is the ERC-20 token on Sei that you want to bridge.

Step 3: Compile contracts

npm run compile

Step 4: Deploy the OFTAdapter on Sei testnet

npm run deploy:sei-testnet

The deploy script detects the network — on Sei it deploys SeiOFTAdapter, on other chains it deploys SeiOFTRemote.

Step 5: Deploy SeiOFTRemote on spoke chains

npm run deploy:sepolia
npm run deploy:bsc-testnet

Step 6: Fill in deployed addresses

Update your .env:

ADAPTER_SEI_ADDRESS=0x...
REMOTE_SEPOLIA_ADDRESS=0x...
REMOTE_BSC_TESTNET_ADDRESS=0x...

Step 7: Set peers (bidirectional)

Wire the adapter to its remotes and vice versa:

# Tell the Sei adapter about Sepolia and BSC remotes
npm run set-peers:sei-testnet

# Tell Sepolia remote about the Sei adapter
npm run set-peers:sepolia

# Tell BSC Testnet remote about the Sei adapter
npm run set-peers:bsc-testnet

Step 8: Send tokens (Sei -> Sepolia)

The send script handles ERC-20 approval automatically:

npm run send:sei-testnet

This will:

  1. Check allowance and approve the adapter to spend your tokens (if needed)
  2. Get a fee quote from LayerZero
  3. Lock tokens in the adapter and send the cross-chain message
  4. Mint tokens on the destination chain

Track your transfer on LayerZero Scan.


4. How setPeer Works

LayerZero V2 requires every OApp (including OFT) to register its peers — the contract addresses it trusts on other chains. This is a bidirectional setup:

Chain A: contract.setPeer(chainB_eid, chainB_address_as_bytes32)
Chain B: contract.setPeer(chainA_eid, chainA_address_as_bytes32)

If you skip a direction, cross-chain messages will be rejected. The set-peers scripts handle the bytes32 conversion automatically.

Manual setPeer (via cast)

If you prefer to set peers manually:

# On Sei: tell the adapter about Sepolia remote
cast send $ADAPTER_SEI_ADDRESS \
  "setPeer(uint32,bytes32)" \
  40161 \
  $(cast --to-bytes32 $REMOTE_SEPOLIA_ADDRESS) \
  --private-key $PRIVATE_KEY \
  --rpc-url https://evm-rpc-testnet.sei-apis.com

# On Sepolia: tell the remote about the Sei adapter
cast send $REMOTE_SEPOLIA_ADDRESS \
  "setPeer(uint32,bytes32)" \
  40280 \
  $(cast --to-bytes32 $ADAPTER_SEI_ADDRESS) \
  --private-key $PRIVATE_KEY \
  --rpc-url https://ethereum-sepolia-rpc.publicnode.com

5. Sending Tokens Cross-Chain

Using the provided script

Both projects include a send-tokens.ts script. By default it sends 1 token from Sei testnet to Sepolia.

To change the destination, edit the DESTINATION_EID constant in scripts/send-tokens.ts:

const DESTINATION_EID = EID.SEPOLIA; // Sei -> Sepolia
// const DESTINATION_EID = EID.BSC_TESTNET;  // Sei -> BSC Testnet

To change the amount:

const AMOUNT = ethers.parseEther("10.0"); // 10 tokens

To send to a different recipient, set DESTINATION_ADDRESS in your .env.

Manual send (via cast)

# 1. Get a fee quote
cast call $OFT_ADDRESS \
  "quoteSend((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),bool)" \
  "(40161,$(cast --to-bytes32 $RECIPIENT),1000000000000000000,990000000000000000,0x,0x,0x)" \
  false \
  --rpc-url https://evm-rpc-testnet.sei-apis.com

# 2. Send (include the native fee as msg.value)
cast send $OFT_ADDRESS \
  "send((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),(uint256,uint256),address)" \
  "(40161,$(cast --to-bytes32 $RECIPIENT),1000000000000000000,990000000000000000,0x,0x,0x)" \
  "($NATIVE_FEE,0)" \
  $YOUR_ADDRESS \
  --value $NATIVE_FEE \
  --private-key $PRIVATE_KEY \
  --rpc-url https://evm-rpc-testnet.sei-apis.com

Tracking transfers

All cross-chain messages can be tracked on:

Paste your transaction hash to see the message status.


6. LayerZero V2 Endpoint Reference

Endpoint Addresses

Network EndpointV2 Address
All EVM mainnets 0x1a44076050125825900e736c501f859c50fE728c
All EVM testnets 0x6EDCE65403992e310A62460808c4b910D972f10f

Endpoint IDs (EIDs)

Chain Mainnet EID Testnet EID
Sei 30280 40280
Ethereum 30101 40161 (Sepolia)
BSC 30102 40102

Full list: https://docs.layerzero.network/v2/deployments/mainnet


7. Switching to Mainnet

Step 1: Update .env with mainnet RPCs

Both projects have default mainnet RPCs already configured in hardhat.config.ts. Just make sure your wallet is funded on mainnet.

Step 2: Deploy using mainnet network names

# OFT Token
npm run deploy:sei
npm run deploy:ethereum
npm run deploy:bsc

# OFT Adapter
npm run deploy:sei
npm run deploy:ethereum
npm run deploy:bsc

Step 3: Update .env with mainnet addresses

Replace the testnet addresses with the new mainnet ones.

Step 4: Set peers on mainnet

The set-peers scripts auto-detect the network and use the correct EIDs. You'll need to add mainnet peer entries to the script or run them manually with cast.

For mainnet peers, use these EIDs:

  • Sei: 30280
  • Ethereum: 30101
  • BSC: 30102

Step 5: Verify contracts

Verify your contracts on block explorers for transparency:

npx hardhat verify --network sei <CONTRACT_ADDRESS> <CONSTRUCTOR_ARGS>
npx hardhat verify --network ethereum <CONTRACT_ADDRESS> <CONSTRUCTOR_ARGS>

8. Troubleshooting

"Peer not set" / Message rejected

Peers must be set bidirectionally. If you deployed on Sei and Sepolia, you need to run set-peers on both chains. Double-check that the addresses in your .env are correct.

"Insufficient fee" / Transaction reverts

The send function requires native tokens (SEI/ETH/BNB) to pay the LayerZero messaging fee. The scripts call quoteSend first to get the exact fee. Make sure your wallet has enough native tokens.

"Allowance" error (OFT Adapter only)

The adapter needs approval to spend your ERC-20 tokens. The send-tokens.ts script handles this automatically, but if you're calling manually:

cast send $TOKEN_ADDRESS \
  "approve(address,uint256)" \
  $ADAPTER_ADDRESS \
  1000000000000000000 \
  --private-key $PRIVATE_KEY \
  --rpc-url https://evm-rpc-testnet.sei-apis.com

Transfer stuck / not arriving

  1. Check LayerZero Scan with your tx hash
  2. LayerZero messages typically take 1-3 minutes on testnets
  3. If the message shows as "delivered" but tokens aren't visible, the destination contract may have reverted — check the destination chain explorer

Compile errors with imports

If your IDE shows import errors for @layerzerolabs/* or @openzeppelin/*, make sure you ran npm install first. The contracts are resolved from node_modules/.


Project Structure

layerzero-oft/
├── README.md                          ← You are here
├── oft-token/                         ← New omnichain token
│   ├── contracts/
│   │   └── SeiOFT.sol                 ← OFT token (burn/mint on all chains)
│   ├── deploy/
│   │   └── 001_deploy_oft.ts          ← Deploy script
│   ├── scripts/
│   │   ├── set-peers.ts               ← Wire chains together
│   │   └── send-tokens.ts             ← Cross-chain transfer
│   ├── hardhat.config.ts
│   ├── package.json
│   └── .env.example
└── oft-adapter/                       ← Wrap existing Sei token
    ├── contracts/
    │   ├── SeiOFTAdapter.sol           ← Adapter on Sei (lock/unlock)
    │   └── SeiOFTRemote.sol            ← OFT on spokes (mint/burn)
    ├── deploy/
    │   └── 001_deploy_adapter.ts       ← Smart deploy (adapter on Sei, remote on spokes)
    ├── scripts/
    │   ├── set-peers.ts                ← Wire adapter <-> remotes
    │   └── send-tokens.ts             ← Bridge from Sei (auto-approves)
    ├── hardhat.config.ts
    ├── package.json
    └── .env.example