Securityadvancedarticle30 min read

Nomad Bridge Hack: $190M Copy-Paste Exploit | VCC

On August 1, 2022, the nomad bridge hack unfolded in a way never seen before. An attacker discovered that the Nomad Replica contract across multiple chains had never initialized its trusted root — it defaulted to bytes32(0). This meant anyone could submit an empty Merkle proof, and the bridge

Vietcardano teamTeam

Đội ngũ biên tập VietCardanoCommunity — các chuyên gia blockchain và Cardano, cung cấp nội dung giáo dục và nghiên cứu chuyên sâu cho cộng đồng.

Published September 14, 2026

On August 1, 2022, the nomad bridge hack unfolded in a way never seen before. An attacker discovered that the Nomad Replica contract across multiple chains had never initialized its trusted root — it defaulted to bytes32(0). This meant anyone could submit an empty Merkle proof, and the bridge would accept it because zero hash equals zero trusted root. Within minutes, 41 different attackers copied the exploit transaction, changed the recipient address, and drained $190 million across Ethereum, Moonbeam, Avalanche, and other chains. This is the story of the nomad 190 million exploit, the first major "crowdsourced hack" in DeFi history, and what it revealed about the dangers of uninitialized state in cross-chain bridges.


TL;DR

  • What happened: Attacker discovered Nomad Replica trusted root was never initialized (zero hash), submitted empty Merkle proofs to drain $190M across 5 chains; 41 copycats replicated the exploit in minutes
  • When: August 1, 2022 (exploit); August 2-3 (whitehat recovery, Replica upgrade)
  • Root cause: Replica contract initialize() never called — trusted root defaulted to bytes32(0); empty Merkle proof produces zero hash, which matches zero trusted root
  • Attribution: Unknown initial attacker (sophisticated, deep Ethereum internals knowledge); 41 copycat attackers; no nation-state link
  • Impact: Largest multi-chain bridge hack at the time; ~$190M drained across Ethereum, Moonbeam, Avalanche, Milkomeda, Evmos; ~$35M returned by whitehats
  • Fix: Replica upgraded with proper initialize() call setting trusted root to actual Merkle root; process() now rejects zero-hash proofs

The Architecture: A Multi-Chain Message Bridge

Nomad is an optimistic cross-chain bridge connecting Ethereum (Home chain) to multiple destination chains (Moonbeam, Avalanche, Milkomeda, Evmos, etc.). Unlike Wormhole's guardian model or Ronin's validator set, Nomad uses a single Replica contract on each destination chain that trusts a single "Home" contract on Ethereum.

Diagram - Nomad Bridge trusted root initialization flow

The trust model: When users lock tokens on Ethereum Home, a message is enqueued. Off-chain relayers watch for events, construct Merkle proofs, and submit them to the destination chain Replica. The Replica verifies the Merkle proof against its trustedRoot. If valid, it releases the bridged tokens.

The critical assumption: trustedRoot in Replica is correctly initialized to the Merkle root of the Home contract's message queue. The process()

Diagram - Nomad Bridge Architecture: Home (Ethereum) -> Relayers -> Replica (multi-chain); trustedRoot = 0x0 = vulnerability function only verifies that the proof's root matches `trustedRoot`.

The Vulnerability: Zero-Hash Trusted Root

On each destination chain, the Replica contract has an initialize() function that sets the trustedRoot. This function was never called during deployment.

`solidity // Simplified Replica.sol (conceptual) contract Replica { bytes32 public trustedRoot;

function initialize(bytes32 _trustedRoot) external { trustedRoot = _trustedRoot; // NEVER CALLED! }

function process(bytes calldata message, bytes calldata proof) external { bytes32 root = MerkleProof.verify(proof, message); require(root == trustedRoot, "Invalid proof"); // BUG: trustedRoot = 0x0! // Release funds... } } `

The bug: trustedRoot defaulted to bytes32(0) (zero hash). An empty Merkle proof also produces a zero hash. Zero hash == zero trustedRoot → verification PASSES.

The attacker exploited this by:

  1. Calling process() with an empty proof (no Merkle branches)
  2. The proof verification computes root = 0
  3. root == trustedRoot → 0 == 0 → TRUE
  4. Funds released without any actual message being enqueued on Ethereum Home

The Attack: A Copy-Paste Free-for-All

August 1, 2022, ~21:00 UTC — Attacker discovers the zero trustedRoot bug. Submits first process() call with empty proof, drains funds from Moonbeam Replica.

21:15 UTC — Exploit transaction visible in public mempool. The calldata is trivially copyable: just change the recipient address in the message.

21:30 UTC — Copycats swarm. Within minutes, 41 unique attackers submit their own versions of the exploit, each changing the recipient to their own address. Funds drain simultaneously from Moonbeam, Avalanche, Milkomeda, Evmos Replicas.

22:00 UTC — Nomad team detects anomalous outflows. Pauses Home contract (stops new messages). Deploys Replica upgrade with proper initialize() call.

August 2-3 — Whitehat hackers return ~$35M to Nomad multisig. $10M bounty paid. Remaining ~$150M at large.


Diagram - Nomad Copy-Paste Exploit Flow

The Numbers: $190M Stolen, Crowdsourced Drain

MetricValue
Total Stolen~$190 million
Chains AffectedEthereum Home, Moonbeam, Avalanche, Milkomeda, Evmos
Unique Attackers41
Time Window~60 minutes
Whitehat Returns~$35M
Bounty Paid$10M
Still at Large~$150M

This was a "copy-paste exploit" — the exploit calldata was identical for all attackers except the recipient address. No sophisticated tooling needed; anyone who saw the first tx in the mempool could copy it, change the recipient, and replay.


Diagram - Attack Timeline & Fund Flow

Root Cause Deep Dive: The Uninitialized Trusted Root

The Proxy Pattern Pitfall

Nomad used a proxy pattern: Home (Ethereum) → Replica (destination chains). The Replica is an upgradeable contract. During deployment:

  1. Replica implementation deployed
  2. Proxy deployed pointing to implementation
  3. initialize() MUST be called on proxy to set trustedRoot

Step 3 was missed. The Replica proxy was deployed but initialize() never called. In Solidity, uninitialized state variables default to zero. trustedRoot = bytes32(0).

The Missing Check

`solidity // Should have been in process(): require(trustedRoot != bytes32(0), "Trusted root not initialized"); `

This single check would have prevented the entire exploit. But it was missing — the code assumed initialize() would always be called.

Exploit Construction

  1. Attacker builds a message: recipient = attacker_address, amount = max_token_balance
  2. Creates empty Merkle proof: proof = [] (no branches)
  3. Computes root: MerkleProof.verify([], message) → returns bytes32(0)
  4. Calls Replica.process(message, [])
  5. Verification: root (0) == trustedRoot (0) → PASS
  6. Funds released to attacker

No signatures, no cryptography broken — just a default value bug.


The Copy-Paste Free-for-All

The exploit was trivially copyable. The malicious calldata was visible in the public mempool. Anyone could:

  1. Copy the transaction calldata
  2. Decode the message, change recipient to their own address
  3. Re-encode and submit

41 unique attackers participated — from sophisticated MEV bots to script kiddies with basic Ethereum knowledge. This was the first major "crowdsourced hack" in DeFi.

Why it spread so fast:

  • Public mempool visibility (no private mempool / Flashbots)
  • No commit-reveal or encryption on calldata
  • Identical calldata for all chains — just change recipient
  • No rate limiting, no anomaly detection, no pause guardian

Whitehat Recovery & Nomad Response

August 2-3, 2022 — Ethical hackers (whitehats) began returning funds to Nomad multisig:

  • ~$35M returned by whitehats who drained to protect
  • $10M bounty paid to whitehats
  • Replica upgrade deployed with proper initialize() call setting trusted root
  • Home contract paused to stop further processing

Final tally: ~$190M stolen, ~$35M returned, ~$150M still at large. Nomad bridge paused indefinitely.


Lessons: What Would Have Prevented This

1. Always initialize trusted state. The initialize() function MUST be called on deployment. Add constructor-time assertions or factory patterns that enforce initialization.

2. Reject zero as trusted root. Explicitly check require(trustedRoot != bytes32(0), "Trusted root not initialized"); in process(). Zero is never a valid Merkle root.

3. Anomaly detection on-chain. A circuit breaker that pauses process() when >X% of TVL is withdrawn in Y blocks. Nomad had no pause guardian.

4. Private mempool for sensitive operations. Use Flashbots or commit-reveal for bridge processing transactions to prevent mempool copycats.

5. Proxy pattern safety. Enforce initialize() on all implementation contracts. Use factory patterns that atomically deploy + initialize.

6. Rate limits on bridge processing. No bridge should allow unlimited process() calls without rate limiting or governance approval.


Diagram - Defense Checklist 5 Lessons

Frequently Asked Questions

How much was stolen in the Nomad bridge hack? ~$190 million across Ethereum, Moonbeam, Avalanche, Milkomeda, and Evmos. ~$35M returned by whitehats; ~$150M still at large.

Was this a cryptographic break (Merkle proofs broken)? No. The Merkle proof verification logic was correct. The bug was that the trusted root was never initialized — it defaulted to zero. An empty proof produces zero hash, which matched the zero trusted root.

Who hacked Nomad? Unknown initial attacker (sophisticated, deep Ethereum internals knowledge). 41 copycat attackers replicated the exploit. No nation-state attribution.

Did users lose money? Yes — users who had funds bridged via Nomad on affected chains lost funds. ~$35M was returned by whitehats; ~$150M remains unrecovered. Nomad bridge is paused.

Why could 41 attackers copy the exploit so easily? The exploit calldata was visible in the public mempool. The exploit required only changing the recipient address in the message. No cryptographic knowledge needed — just change the address and replay.

What is a "copy-paste exploit"? An exploit where the malicious transaction calldata is trivially reproducible by anyone who sees it. The only modification needed is changing the beneficiary address.

Has Nomad fixed the bridge? Yes. The Replica contract was upgraded with proper initialize() call setting the trusted root to the actual Merkle root. The process() function now rejects zero-hash proofs. The bridge remains paused.

What bridges are vulnerable to similar attacks? Any bridge using uninitialized state variables, proxy patterns without enforced initialization, or Merkle proof verification without zero-root checks. The pattern "default value passes verification" is a known vulnerability class.


Technical Appendix: Correct Replica Initialization Flow

For developers, here's the correct initialization flow that Nomad missed:

`

  1. Deploy Replica implementation contract
  2. Deploy proxy pointing to implementation
  3. CALL proxy.initialize(actual_merkle_root)
  4. Sets trustedRoot = actual_merkle_root (non-zero)
  5. VERIFY: trustedRoot != bytes32(0)
  6. Only then: Replica is ready to process messages

`

The missing CALL initialize() at step 3 cost $190M.


This article is part of the DeFi Hacks series. For more deep-dives on bridge exploits, see our analyses of the Wormhole hack, Ronin Bridge hack, and BNB Chain bridge hack.

Tags:

nomad bridge hacknomad 190 millioncopy paste exploitdark forest defizero hash trusted rootcrowdsourced hack