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). |
- Prerequisites
- Option A — New OFT Token (oft-token)
- Option B — OFT Adapter for Existing Token (oft-adapter)
- How setPeer Works
- Sending Tokens Cross-Chain
- LayerZero V2 Endpoint Reference
- Switching to Mainnet
- Troubleshooting
- Node.js >= 18
- A funded wallet on Sei testnet + destination testnet (Sepolia or BSC testnet)
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.
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.
cd oft-token
npm installcp .env.example .envEdit .env and fill in:
PRIVATE_KEY=your_private_key_here
TOKEN_NAME="My Token"
TOKEN_SYMBOL="MTK"npm run compilenpm run deploy:sei-testnetSave the deployed address — you'll need it for the .env file.
Note: The same
SeiOFTcontract 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_mintline inSeiOFT.solbefore deploying on spoke chains.
npm run deploy:sepolia
npm run deploy:bsc-testnetUpdate your .env with all three addresses:
OFT_SEI_ADDRESS=0x...
OFT_SEPOLIA_ADDRESS=0x...
OFT_BSC_TESTNET_ADDRESS=0x...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-testnetnpm run send:sei-testnetThis 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.
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.
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.
cd oft-adapter
npm installcp .env.example .envEdit .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.
npm run compilenpm run deploy:sei-testnetThe deploy script detects the network — on Sei it deploys SeiOFTAdapter, on other chains it deploys SeiOFTRemote.
npm run deploy:sepolia
npm run deploy:bsc-testnetUpdate your .env:
ADAPTER_SEI_ADDRESS=0x...
REMOTE_SEPOLIA_ADDRESS=0x...
REMOTE_BSC_TESTNET_ADDRESS=0x...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-testnetThe send script handles ERC-20 approval automatically:
npm run send:sei-testnetThis will:
- Check allowance and approve the adapter to spend your tokens (if needed)
- Get a fee quote from LayerZero
- Lock tokens in the adapter and send the cross-chain message
- Mint tokens on the destination chain
Track your transfer on LayerZero Scan.
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.
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.comBoth 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 TestnetTo change the amount:
const AMOUNT = ethers.parseEther("10.0"); // 10 tokensTo send to a different recipient, set DESTINATION_ADDRESS in your .env.
# 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.comAll cross-chain messages can be tracked on:
- Testnet: https://testnet.layerzeroscan.com/
- Mainnet: https://layerzeroscan.com/
Paste your transaction hash to see the message status.
| Network | EndpointV2 Address |
|---|---|
| All EVM mainnets | 0x1a44076050125825900e736c501f859c50fE728c |
| All EVM testnets | 0x6EDCE65403992e310A62460808c4b910D972f10f |
| Chain | Mainnet EID | Testnet EID |
|---|---|---|
| Sei | 30280 | 40280 |
| Ethereum | 30101 | 40161 (Sepolia) |
| BSC | 30102 | 40102 |
Full list: https://docs.layerzero.network/v2/deployments/mainnet
Both projects have default mainnet RPCs already configured in hardhat.config.ts. Just make sure your wallet is funded on mainnet.
# 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:bscReplace the testnet addresses with the new mainnet ones.
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
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>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.
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.
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- Check LayerZero Scan with your tx hash
- LayerZero messages typically take 1-3 minutes on testnets
- If the message shows as "delivered" but tokens aren't visible, the destination contract may have reverted — check the destination chain explorer
If your IDE shows import errors for @layerzerolabs/* or @openzeppelin/*, make sure you ran npm install first. The contracts are resolved from node_modules/.
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