All vulnerabilities
CRITICALWeb3exploited in the wild

WEB3-NOMAD-2022

Web3 · Ethereum · Nomad

Summary

On August 1, 2022, the Nomad token bridge was drained of about $190 million. Nomad messages require two steps, prove (record the message hash under a proven Merkle root) then process (execute), and process() gated execution on acceptableRoot(messages[hash]) being valid. During a routine upgrade, initialize() set confirmAt[_committedRoot] = 1 with _committedRoot equal to bytes32(0) (the empty-tree root), so confirmAt[0x00] became non-zero. For any never-proven message, messages[hash] returns the Solidity default bytes32(0), and acceptableRoot(0x00) then read confirmAt[0x00] = 1 and passed the timestamp check, so every unproven message was treated as valid. Attackers skipped prove() entirely and called process() directly with crafted calldata to release funds, submitting no Merkle proof at all. After the first demonstration, hundreds of opportunistic users copy-pasted the transaction with their own addresses, turning it into a chaotic crowdsourced free-for-all; only about $22 million was recovered shortly after.

How to avoid it in your code

  • Never let bytes32(0) or a none/default sentinel be a confirmable root; explicitly reject it in acceptableRoot().
  • Guard initializers: require _committedRoot is non-zero before writing confirmAt.
  • Keep unproven and valid-root domains disjoint; do not overload bytes32(0) as both mapping default and sentinel.
  • Reject empty or absent Merkle proofs in process(); require a positive proof record, not merely non-revert.
  • Re-audit and re-run full invariant tests after every upgrade or initializer; treat deployed-vs-audited drift as a release blocker.

References

Related vulnerabilities

All Web3 →