Nomad Bridge (2022)
A $190M loss in what may be the most chaotic exploit in DeFi history. The bug was a single line of code, but the exploitation was different from any prior attack: once the first attacker demonstrated the technique, hundreds of opportunistic users copy-pasted the transaction and drained funds in a public free-for-all that lasted hours.
Timeline
- August 1, 2022, ~21:30 UTC: First attacker exploited the bug, draining ~$2M.
- Within minutes: Other users noticed the technique. The attack was trivially copyable — change the recipient address, resubmit.
- Over the next several hours: Approximately 300 addresses participated. Total drained: $190M.
- August 2, 2022: Nomad acknowledged the incident. Offered a 10% bounty for return of funds. Many participants — but not all — returned their share.
Root Cause
Nomad's bridge used a Replica contract on each destination chain that processed cross-chain messages. The Replica verified that a message was part of a Merkle tree whose root had been previously committed by the bridge's "updater" role.
A code update set the initial committedRoot to bytes32(0). This is a sentinel value indicating "no commitment yet." But the check that validated incoming messages did:
require(acceptableRoot(messages[messageHash]), "!proven");
Where acceptableRoot(0) evaluated to true — because 0 was considered "the default trusted root, equivalent to a fresh contract."
When messages[messageHash] returns 0 (for any never-seen message), the function returns true. Every message was automatically valid.
Exploit Path
- First attacker constructed an arbitrary "valid" message — claiming a withdrawal of any amount, to any address.
- Submitted to
process(). The function checkedmessages[messageHash]for the message; it was0; the function returned. The withdrawal executed. - Posted to Twitter and Discord.
- Others reverse-engineered the transaction. The change required: replace the recipient address with one's own. No technical sophistication needed.
- Hundreds of addresses ran modified copies of the transaction. The bridge drained.
This is the only major exploit in DeFi history that was "open-source" while ongoing — the technique was public, and the bridge had no kill switch fast enough to stop the rush.
What an Audit Should Have Caught
The bug was a one-line code change. The change was made to fix something else, and its impact on the validation logic was not caught.
Findings that should have appeared:
-
acceptableRoot(0) == trueis dangerous. Sentinel values that mean "default trust" should be explicitly excluded from validation paths. The check should have been:bytes32 root = messages[messageHash]; require(root != bytes32(0) && acceptableRoot(root), "!proven"); -
Initialization values must be reviewed. The
committedRoot = bytes32(0)initialization was the trigger. Any state-machine constant that has a sentinel meaning needs explicit handling. -
Configuration changes to a deployed bridge are critical changes. Nomad had been operating; this was a configuration update, not a launch. Audits often cover launches and miss subsequent changes. Continuous audit / change-audit programs catch this.
-
No kill switch. Once the exploit started, there was no way to pause the bridge in time. Bridges holding nine-figure value need an emergency pause function with appropriate authorization (multisig, monitoring service, etc.).
Lessons
-
Sentinel values are footguns.
bytes32(0),address(0),uint256.maxand similar are conceptually distinct from "any valid value." The code must handle them explicitly, not assume they'll be filtered upstream. -
Configuration changes need their own audit. A protocol that's been audited may make a subsequent code change (even one line) that introduces a critical bug. Some firms now offer "change audits" specifically for this.
-
Public exploits get worse. Pre-Nomad, the typical exploit was conducted by a single attacker who tried to be subtle. Nomad showed that an exploit can be free-for-all. Modern bridges should have monitoring + pause that can stop a draining bridge within a few blocks.
-
Refund / recovery is governance-dependent. Nomad recovered some funds by appealing to participants and offering bounties. Many participants returned funds (sometimes after legal pressure). The recovery is partial, and depends on the attackers being identifiable / accountable; many were not.
-
"Whitehat" status is socially negotiated. Some Nomad participants claimed to be "rescuing" funds and intended to return them. Some did. Some kept the funds. There is no on-chain distinction between "rescue" and "theft." Audits should not assume that whitehats will appear to mitigate a bug; the protocol must be self-sufficient.
-
One-line bugs cause maximum damage. The complexity of the codebase does not bound the damage. A simple-looking change in a critical path can be the entire vulnerability.
Nomad's bug was the kind of mistake that any developer could make and any code review could miss. The systemic lesson is not "do better reviews" — that's always advisable but never sufficient — but "design systems that fail safely when individual changes are wrong." Bridges with sentinel-value protection, kill switches, and rate limits are robust to bugs of this class.