
OmniToken — A Cross-Chain Token with LayerZero V2 OFT
I built OmniToken (OMNI), a cross-chain fungible token deployed on Ethereum Sepolia, Optimism Sepolia, Arbitrum Sepolia, and Base Sepolia using the LayerZero V2 OFT (Omnichain Fungible Token) standard — plus a token sale contract and a modern Web3 frontend to use it.
Why LayerZero V2 OFT
Traditionally, if you wanted your token on multiple chains you deployed separate contracts and trusted a bridge to move liquidity between them. The OFT standard flips that: one canonical contract per chain, and the LayerZero V2 endpoint handles secure cross-chain messaging, so tokens move between chains natively.
The token contract itself is minimal — it inherits OFT from LayerZero and OpenZeppelin's Ownable:
contract OmniToken is OFT {
constructor(string memory _name, string memory _symbol, address _lzEndpoint, address _delegate)
OFT(_name, _symbol, _lzEndpoint, _delegate)
Ownable(_delegate)
{
// Initial supply is minted only on the home chain;
// other chains receive tokens through bridging.
if (block.chainid == 11155111) {
_mint(_delegate, 1_000_000 * 10 ** decimals());
}
}
}
The initial supply is minted only on the home chain (Ethereum Sepolia). Every other chain starts at zero — tokens arrive there only through actual cross-chain transfers, which keeps the supply honest across the whole network.
Deployment Across Four Chains
Each testnet has its own chain ID and LayerZero endpoint ID:
| Network | Chain ID | LayerZero EID |
|---|---|---|
| Ethereum Sepolia | 11155111 | 40161 |
| Optimism Sepolia | 11155420 | 40232 |
| Arbitrum Sepolia | 421614 | 40231 |
| Base Sepolia | 84532 | 40245 |
All four use the shared LayerZero V2 testnet endpoint: 0x6EDCE65403992e310A62460808c4b910D972f10f.
Deployment is a two-step process with Foundry scripts. First, deploy the contract on each chain with verification:
forge script script/Deploy.s.sol:DeployOmniToken \
--rpc-url eth_sepolia --broadcast --verify
Then run the configuration script on every chain to set up trusted peers:
forge script script/ConfigureOFT.s.sol:ConfigureOFT \
--rpc-url eth_sepolia --broadcast
Until peers are set on both sides, cross-chain sends revert with Peer not set — that's the LayerZero V2 security model in action: no chain trusts another until explicitly configured.
The Token Sale
Beyond the token itself, I built omniTokenSale — a fixed-rate sale contract where users buy OMNI in exchange for native ETH:
- 1 ETH = 1000 OMNI (
0.001 ETHper token) - ReentrancyGuard on every state-changing path
- A
receive()fallback so sending ETH to the contract directly triggers a purchase - Owner-only withdrawal for accumulated ETH, plus emergency token recovery
function buyTokens() public payable nonReentrant {
require(msg.value > 0, "omniTokenSale: Must send ETH to buy tokens");
uint256 tokenAmount = msg.value * TOKENS_PER_ETH;
uint256 contractTokenBalance = omniToken.balanceOf(address(this));
require(contractTokenBalance >= tokenAmount, "omniTokenSale: Not enough tokens in contract");
omniToken.transfer(msg.sender, tokenAmount);
emit TokensPurchased(msg.sender, msg.value, tokenAmount);
}
Sending Tokens Cross-Chain
From a frontend or script, the flow is: build a SendParam, quote the fee, then send. The quote step is essential — the destination gas fee is paid in native tokens on the source chain, so the value sent with the transaction must match what quoteSend returns:
const [nativeFee] = await token.quoteSend(sendParam, false);
const tx = await token.send(
sendParam,
{ nativeFee, lzTokenFee: 0 },
wallet.address,
{ value: nativeFee }
);
Messages typically arrive in 1–5 minutes and can be tracked on LayerZero Scan.
Frontend
The frontend is a Vite + React + TypeScript app with shadcn/ui, wagmi, and RainbowKit. It ships four pages:
- Bridge — transfer OMNI between the four networks
- Balance — view per-chain balances for a connected wallet
- Buy — the token sale interface
- Docs — deployment addresses and usage notes
All protocol addresses are config-driven, so pointing the app at a fresh deployment is a config change, not a code change.
Reflections
The OFT standard hides a lot of complexity — I wrote a normal ERC20 and got cross-chain transfers for free. The real engineering was in the deployment plumbing: getting peers configured consistently across four chains, and building the quote-then-send flow correctly so users pay the right destination fee. LayerZero's "no chain trusts another until configured" model is a good reminder of how cross-chain security actually works.
Live app: omni-token.vercel.app