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 tobytes32(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.
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()
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:
- Calling
process()with an empty proof (no Merkle branches) - The proof verification computes root = 0
root == trustedRoot→0 == 0→ TRUE- 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.
The Numbers: $190M Stolen, Crowdsourced Drain
| Metric | Value |
|---|---|
| Total Stolen | ~$190 million |
| Chains Affected | Ethereum Home, Moonbeam, Avalanche, Milkomeda, Evmos |
| Unique Attackers | 41 |
| 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.
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:
- Replica implementation deployed
- Proxy deployed pointing to implementation
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
- Attacker builds a message:
recipient = attacker_address, amount = max_token_balance - Creates empty Merkle proof:
proof = [](no branches) - Computes root:
MerkleProof.verify([], message)→ returnsbytes32(0) - Calls
Replica.process(message, []) - Verification:
root (0) == trustedRoot (0)→ PASS - 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:
- Copy the transaction calldata
- Decode the message, change
recipientto their own address - 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.
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:
`
- Deploy Replica implementation contract
- Deploy proxy pointing to implementation
- CALL proxy.initialize(actual_merkle_root)
- Sets trustedRoot = actual_merkle_root (non-zero)
- VERIFY: trustedRoot != bytes32(0)
- 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.
