Building a Cross-Chain Token Bridge with LayerZero V2

We built a cross-chain ERC20 token bridge using LayerZero V2. It moves BRG tokens across Ethereum, Arbitrum, Base, and Optimism — 12 directional pathways, all trustless, with multi-DVN verification. This post walks through the actual code. The full source is at github.com/gnuser/brg-bridge.

Architecture

/images/brg-bridge-architecture.svg

Lock-and-Mint Model

The bridge uses LayerZero’s OFT (Omnichain Fungible Token) standard:

  • Ethereum (home chain): BridgeOFTAdapter locks ERC20 tokens when bridging out, unlocks when bridging back.
  • L2 chains: BridgeOFT mints synthetic tokens on receive, burns on send. No pre-minted supply.

Total supply is always conserved: locked on Ethereum = minted across L2s.

LayerZero V2 Part 6: Developer Gotchas

This is Part 6 of a series on LayerZero V2. Part 1 covers the basics.

/images/part6-developer-gotchas.svg

These are the mistakes that cost us time building the BRG Bridge. Learn from our pain.

1. Wrong Option Type on Testnets

What happens: You use Type 3 options (the recommended format), but the testnet pathway doesn’t have Type 3 ULN config set. Transaction reverts with LZ_ULN_InvalidWorkerOptions.

LayerZero V2 Part 5: Composed Messages

This is Part 5 of a series on LayerZero V2. Part 1 covers the basics.

/images/part5-composed-messages.svg

Sometimes you want to bridge tokens AND do something with them in one operation. Bridge USDC to Arbitrum and immediately swap it for ETH. Bridge tokens and stake them. LayerZero V2 calls this composed messages.

The Problem with V1

In V1, composed operations were all-or-nothing. If you bridged + swapped + staked, and the staking failed, the ENTIRE transaction reverted — including the bridge. Your tokens got stuck in limbo.

LayerZero V2 Part 4: OFT Token Bridging

This is Part 4 of a series on LayerZero V2. Part 1 covers the basics.

/images/part4-oft-bridging.svg

OFT (Omnichain Fungible Token) is the most common application built on LayerZero. It moves ERC20 tokens between chains.

The Lock-and-Mint Model

Ethereum (Home Chain)              L2 Chains (Arb, Base, Opt)
┌──────────────────┐              ┌──────────────────┐
│ Your ERC20 Token │              │ OFT Contract     │
│ (real token)     │              │ (synthetic token) │
└────────┬─────────┘              └────────┬─────────┘
         │                                 │
┌────────┴─────────┐                       │
│ OFTAdapter       │ ◄── LayerZero ──────► │
│ LOCKS tokens     │     message           │ MINTS tokens
│ when bridging out│                       │ when receiving
│ UNLOCKS tokens   │                       │ BURNS tokens
│ when bridging in │                       │ when sending
└──────────────────┘                       │

Bridge OUT (Ethereum → Arbitrum):

LayerZero V2 Part 3: The DVN Security Model

This is Part 3 of a series on LayerZero V2. Part 1 covers the basics, Part 2 covers the message lifecycle.

/images/part3-dvn-security.svg

The DVN security model is the most important part of LayerZero V2. It determines who you trust and how much.

How DVN Configuration Works

Every app sets its own DVN configuration per pathway (source chain → destination chain):

My App's Security Config:
  Required DVNs:  [LayerZero Labs, Google Cloud]    ← BOTH must verify
  Optional DVNs:  [Polyhedra ZK, Nethermind]        ← At least 1 must verify
  Threshold:      1

A message is deliverable when:

LayerZero V2 Part 2: How a Message Travels

This is Part 2 of a series on LayerZero V2. Part 1 covers the basics.

/images/part2-message-lifecycle.svg

Let’s trace a message from Chain A to Chain B, step by step.

Phase 1: Sending (Chain A)

Step 1:  Your app calls send() on the OFT contract
Step 2:  OFT burns/locks tokens and builds the message
Step 3:  OFT calls _lzSend() → EndpointV2.send()
Step 4:  Endpoint routes to SendUln302 (the MessageLib)
Step 5:  SendUln302 encodes the message into a Packet
Step 6:  SendUln302 collects fees (DVN + Executor + treasury)
Step 7:  SendUln302 emits an event with the packet data

At this point, the message exists on Chain A as an event log. Nothing has happened on Chain B yet.