Writing
OmniToken — Cross-Chain Token
OmniToken — LayerZero V2 OFT Cross-Chain Token

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:

NetworkChain IDLayerZero EID
Ethereum Sepolia1115511140161
Optimism Sepolia1115542040232
Arbitrum Sepolia42161440231
Base Sepolia8453240245

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 ETH per 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