Nomad Bridge Invalid-Proof Acceptance / Trusted-Root Failure (2022-08-01)
- Name: Nomad Bridge invalid-proof acceptance / trusted-root failure
- Date: 2022-08-01
- Protocol: Nomad
- Chain: Ethereum + Nomad-connected EVM chains
- Category: bridge verification failure / invalid-proof acceptance / trusted-root contamination / forged-message processing
- Loss: about 190M drained, with later reporting that roughly $36M was returned
- Summary: A June 2022 Replica upgrade changed Nomad’s message-verification semantics and left
confirmAt[bytes32(0)] = 1on affected Replica contracts. That was fatal because unproven messages also defaulted tomessages[_messageHash] == bytes32(0). Theprocess()path now checkedacceptableRoot(messages[_messageHash]), so forged messages inherited the zero root and passed as already acceptable. The sharper failure was not merelyinvalid proof accepted; it was that bootstrap trusted-root inventory and runtime message-proof inventory both allowedbytes32(0)to carry live verifier meaning after the upgrade. Once the first attacker demonstrated the calldata shape, copycats only had to replay the sameprocess()call and replace the recipient address. The bridge became a permissionless loot box. - Trigger / entry point:
- Nomad’s
Replica.process()path requiredacceptableRoot(messages[_messageHash])instead of the older explicitmessages[_messageHash] == MessageStatus.Provencheck. - Replica initialization pre-approved
_committedRootby settingconfirmAt[_committedRoot] = 1. - On replicas initialized with
_committedRoot == bytes32(0), the zero root became permanently acceptable. - Unproven messages read back as
bytes32(0)from themessagesmapping, so forged messages were treated as if they had already been proven under an acceptable root.
- Nomad’s
- Root cause:
- The bridge’s authentication boundary collapsed because the verifier accepted the default zero value as a trusted root.
- This was introduced during a June 21, 2022 smart-contract upgrade that changed how Nomad tracked proof status from a status flag to message-hash-to-root mapping semantics.
- The protocol effectively conflated two meanings of zero:
no proof recordedandinitial committed root. - More precisely,
process()should have had to prove two separate facts —this message has an explicit proof recordandthe referenced root is currently acceptable— but the upgraded logic only enforced the second predicate. - Once those values collided,
process()no longer distinguished an actually proven cross-chain message from attacker-supplied calldata.
- Exploit path:
- A Replica upgrade introduced the new
acceptableRoot(messages[_messageHash])validation flow. - A Replica initializer pre-approved
_committedRootby settingconfirmAt[_committedRoot] = 1; on affected deployments that root wasbytes32(0). - For any forged message that had never been proven,
messages[_messageHash]returned the defaultbytes32(0)value. acceptableRoot(bytes32(0))returnedtruebecauseconfirmAt[bytes32(0)]had been set to1, and every live block timestamp is greater than or equal to1.- Attackers could therefore call
process()directly without first calling a legitimateprove()flow and still satisfy the bridge’s proof gate. - The first exploiter crafted a fraudulent bridge message that released tokens from Nomad custody to an attacker-controlled recipient.
- Once the calldata format circulated publicly, copycats replayed prior exploit transactions, patched in their own recipient addresses, and drained remaining bridge liquidity in smaller chunks rather than engineering bespoke full-bridge drains.
- A Replica upgrade introduced the new
- What the attacker needed to be true:
- The affected Replica contract had to have been initialized or upgraded such that
confirmAt[bytes32(0)]was nonzero. - The bridge had to rely on the new root-acceptance check rather than the older explicit
MessageStatus.Provenequality check. - Bridge custody still had to hold withdrawable assets on the destination side.
- The attacker needed only the ability to submit calldata to
process(); they did not need the Updater key, a valid attestation, or a successful merkle proof.
- The affected Replica contract had to have been initialized or upgraded such that
- Technical anatomy worth remembering:
- This was a verifier failure, not a liquidity-model failure. The bridge released assets because its authenticity check stopped authenticating anything meaningful.
- It is also the cleanest bridge case in the corpus for
proof-state sentinel separationfailure rather than classicproof-to-message bindingfailure. Nomad did not preserve a real trusted root and then detach the wrong leaf from it; it letabsence of message proofinherit the same runtime value asacceptable root already known. - The bridge’s optimistic-security machinery did not save it because the forged messages did not require a fraudulent Updater signature. Nomad’s own FAQ notes the Watchers were designed to detect compromised Updater behavior, not smart-contract bugs in
process(). - The exploit was unusually democratized: after the first public success, hundreds of addresses joined in by replaying working calldata and changing recipient bytes.
- That democratization also exposed the bridge’s blast-radius design. Once
process()stopped authenticating messages, live bridge custody could be harvested over and over until operators unenrolled the Replicas; authenticity failure and effectively unbounded release authority had become the same event. - The most important implementation lesson is that
default mapping valueis itself protocol state. In verifier code, an uninitialized zero is never neutral if zero can also be interpreted as a valid proof artifact.
Three bridge-truth promotions this incident collapsed
- A bridge-shaped proof-state object existed at all
- The Replica had some root inventory, some message-proof storage, and some bootstrap state left behind by initialization.
- At this stage the strongest true fact was only
the verifier has raw state for roots and messages, notthis exact message was proven.
- That raw state was promoted into canonical truth that this exact message had been proven under an acceptable root
- This was the decisive failure. Untouched
messages[_messageHash]entries returnedbytes32(0), and the upgradedprocess()path treated that value as a root candidate foracceptableRoot(...)instead of first requiring an explicit proved-message state. - Because initialization had already made
confirmAt[bytes32(0)] = 1on affected Replicas, the bridge collapsedno proof recordedandroot already acceptableinto the same verifier meaning.
- This was the decisive failure. Untouched
- That accepted bridge truth still deserved immediate custody-release authority
- Once
process()returned success, the route still let the forged message drive ordinary token-release logic against live bridge custody. - The public exploit template then showed there was almost no blast-radius separation between
verifier storage lied onceandanyone can keep harvesting assets until operators shut the lane down.
- Once
- Representative transactions and contracts:
- Vulnerable Ethereum Replica contract highlighted in public writeups:
0xB92336759618F55bd0F8313bd843604592E27bd8 - Fatal Replica initialization transaction cited by public postmortems:
0x53fd92771d2084a9bf39a6477015ef53b7f116c79d98a21be723d06d79024cad - Ethereum production upgrade transaction cited by Nomad:
0x7bccd64f4c4d5f6f545c2edf904857e6ddb460532fc0ac7eb5ac175cd21e56b1 - Early successful exploit transaction cited by rekt.news:
0xb1fe26cc8892f58eb468f5208baaf38bac422b5752cca0b9c8a871855d63ae28 - Example copycat exploit transaction highlighted by Mandiant:
0xa5fe9d044e4f3e5aa5bc4c0709333cd2190cba0f4e7f16bcf73f49f83e4a5460
- Vulnerable Ethereum Replica contract highlighted in public writeups:
- Response / mitigation:
- Nomad unenrolled the Replica contracts from the bridge and governance system so no further bridge messages could be processed.
- The front-end was disabled and users were told not to bridge until a fix and restart were complete.
- Nomad later offered a return program that let whitehats keep 10% if 90% was returned; Mandiant reports about $36M was ultimately recovered.
- Key transaction hashes:
- Fatal Replica initialization transaction:
0x53fd92771d2084a9bf39a6477015ef53b7f116c79d98a21be723d06d79024cad - Ethereum production upgrade transaction:
0x7bccd64f4c4d5f6f545c2edf904857e6ddb460532fc0ac7eb5ac175cd21e56b1 - Early successful exploit transaction:
0xb1fe26cc8892f58eb468f5208baaf38bac422b5752cca0b9c8a871855d63ae28 - Example copycat exploit transaction:
0xa5fe9d044e4f3e5aa5bc4c0709333cd2190cba0f4e7f16bcf73f49f83e4a5460
- Fatal Replica initialization transaction:
Pattern tags
- bridge-verification-failure
- proof-state-sentinel-collapse
- Replica upgrade logic let
bytes32(0)collapse two different verifier claims —no proof recordedandacceptable committed root already known— into one executable value - The bridge’s critical predicate became only
root is acceptable now, while silently dropping the separate predicatethis exact message has an explicit proof record - Watcher and optimistic-updater machinery were orthogonal to the live failure, because forged messages no longer needed a fraudulent Updater signature once
process()itself accepted untouched proof storage as bridge truth - One false verifier result still had uncapped line-of-sight to live bridge-router custody, and the exploit’s public calldata template turned that verifier bug into mass copycat withdrawals rather than one bespoke operator-grade drain
Why the exploit worked
- Nomad’s own Aug. 2022 RCA makes the decisive break very explicit: after the June 21 Replica upgrade,
process()no longer requiredmessages[_messageHash] == MessageStatus.Proven; it required onlyacceptableRoot(messages[_messageHash]). That semantic shift mattered because untouchedmessages[_messageHash]entries defaulted tobytes32(0). - The initializer then pre-approved
_committedRootby writingconfirmAt[_committedRoot] = 1. On Replicas deployed against empty Home trees,_committedRootwas itselfbytes32(0), so the same zero value now also meantalready acceptable root. - From that point on, forged messages inherited a trusted-looking root without ever passing a real
prove()flow. The bridge therefore stopped asking one fused authenticity question and instead letdefault mapping valueimpersonatealready proven under a root whose optimistic timer elapsed. - The exploit was unusually democratized because the hard part was already over once the first forged
process()call became public. As Mandiant and Coinbase both noted, later attackers often did not synthesize new bridge messages from first principles; they replayed working calldata and replaced recipient addresses. That is a useful bounded-release lesson, not just colorful incident lore: when verifier failure and live custody release sit too close together, the route can degrade fromsingle exploittoopen harvestalmost instantly. - Nomad’s watcher model also preserved the narrower systems lesson. Watchers were built to respond to fraudulent Updater signatures, but the bridge did not need a bad signature here; the contract had already accepted unproven messages as authentic. That means the real security object was always
process()’s proof-state semantics, not the surrounding optimistic-governance shell.
Detection and audit pivots
-
Treat default values in verifier storage as live protocol state, not neutral blanks. If
0,bytes32(0), or another sentinel can ever also meantrusted root,proven, oralready executable, the bridge has already collapsed two security states into one. -
Audit verifier upgrades by preserving the exact conjunction of predicates that used to define authenticity. Nomad’s dangerous change was not merely
new code path; it was thatmessage explicitly provendisappeared as a separately enforced condition and was replaced by a weaker root-acceptance check. -
Review initializer and migration code as custody code. The critical exploit precondition was set during Replica initialization, not during some exotic runtime edge case:
confirmAt[_committedRoot] = 1became catastrophic only because_committedRootcould be the same zero sentinel returned by untouched proof mappings. -
Ask what human or automated backstop still matters if the verifier lies without any signer compromise. If watchers, fraud proofs, or operator alerts only trigger on bad signatures while the contract can counterfeit authenticity locally, the route’s advertised safety shell is pointed at the wrong failure mode.
-
Model blast radius assuming the first successful forged call becomes public. Nomad shows that a bridge with reusable calldata, deep custody, and no effective post-verification throttles can turn a verifier bug into crowd-sourced drainage within minutes.
-
Comparable exploit pattern:
Failure-prevention checklist
- Keep
unproven message,known root, andprocessable messageas three explicitly different states. A bridge should never be able to answeryestois this executable?using only a default mapping read plus a bootstrap root that happened to share the same raw value. - Treat initializer and migration writes as verifier logic. If a new Replica or route pre-approves a starting root, prove that the chosen root cannot alias any untouched proof-state value and add assertions that reject zero / empty / legacy sentinels unless they are deliberately modeled as non-executable.
- Preserve the full authenticity conjunction across upgrades. If the old system required both
message explicitly provenandreferenced root is acceptable, the replacement path should not silently collapse those into one weaker predicate just because the storage model changed. - Make proof-state semantics machine-auditable at the storage layer. Reviewers should be able to enumerate every raw value that can inhabit
messages[...],confirmAt[...], and related verifier slots and say exactly which ones meannever proven,proven but waiting,currently executable, oralready processed. - Rate-limit post-verification release paths as if verifier bugs are inevitable. One forged or copy-pastable
process()calldata template should not have line-of-sight to the route’s entire live custody balance before operators can pause. - Rehearse the
verifier lies without signer compromisefailure mode operationally. Watchers, pausers, and monitoring should not assume that bad bridge truth always arrives with a bad updater signature, bad validator set, or obvious offchain compromise. - Regression-test zero / empty / untouched proof paths directly: unknown message hash, zero committed root, zero proof root, post-upgrade process call without prior
prove(), and replay attempts against any route that bootstraps from an empty tree. - Keep one route-local liability view that compares executable bridge messages, live custody inventory, and per-asset release caps so a proof-state bug cannot instantly become uncapped treasury loss.
Audit prompts
- What exact conjunction of facts is supposed to authorize
process()today:message was proven,proof referenced an acceptable root,message not yet processed, andhandler scope matches? Which of those facts are stored separately, and which ones are currently inferred from the same raw value? - Can any untouched storage read such as
0,bytes32(0), or a legacy sentinel be interpreted asacceptable root,already proven, orprocessable nowafter initialization, migration, or downgrade-compatibility code runs? - If the bridge changes proof bookkeeping from status flags to root pointers or another derived representation, what concrete test proves the old authenticity predicate survived the refactor unchanged?
- Do initializer parameters, bootstrap roots, or backfilled history writes ever pre-approve values that can also arise from absent proof records or empty message trees?
- What monitoring would fire if the verifier started accepting arbitrary calldata without any signer compromise? Would operators see unusual
process()success rates, unexpected message hashes, or route-level liability drift quickly enough to matter? - If the first successful forged bridge call became public on social media, what concrete caps, delays, or per-asset throttles would stop the route from turning into mass copycat withdrawals?
- Which bridge defenses only watch compromised signer / updater behavior, and which ones independently watch for
contract itself is authorizing nonsensefailures? - Can reviewers enumerate every raw verifier-state value that means
trueorfalseforacceptableRoot(...)and show that none of them are reachable through untouched mappings or ambiguous legacy constants?
Lessons that should already be standard
- Default values are not neutral in verifier code. If
bytes32(0)or0can ever also meantrusted,proven, orready, the bridge already has an authenticity bug waiting for the right code path. - Upgrades do not merely change implementation; they change truth semantics. Any refactor that rewrites
what proves a message is realdeserves custody-grade review, not ordinary feature-shipping review. - Bootstrap convenience is part of the threat model. Pre-approving an initial root to avoid replaying history is fine only if the bootstrap object cannot be confused with
nothing has been proven yet. - Signature-based monitoring is not enough for bridges. A route can catastrophically fail even when no updater, guardian, or validator key is compromised, because the contract may already be lying locally about proof state.
- Once forged bridge calldata is reusable, bounded release matters as much as correctness. Nomad shows how quickly a verifier bug can become a public harvesting game when one success template can be copied by everyone.
- The healthy bridge question is never just
is this root acceptable?; it isdid this exact message earn the right to spend against that acceptable root, and is the spend still bounded if we are wrong?
Why this incident matters
- It is one of the cleanest examples of a bridge breaking at the verification boundary itself rather than at the token contract, relayer, or economic layer.
- It shows how an upgrade meant to improve liveness semantics can silently destroy authenticity semantics.
- It is also one of the clearest bridge cases where
absence itself became authority: untouched message-proof storage inherited executable meaning because bootstrap root state and runtime proof state were not kept disjoint. - It also demonstrates that once a bridge verifier says
any message is valid, the attack surface stops being elite-hacker-only and turns into mass replay looting. - It is also a sharp bounded-release case study: one verifier bug plus a publicly reusable calldata template let attackers iterate through live bridge custody for hours, showing how little separation remained between
false bridge truth acceptedandvery large value immediately exits.
Internal linkages
- Core recurring pattern: bridge-verification-failure.
- Sharper verifier-state pattern: proof-state-sentinel-collapse — use this when the real reusable lesson is that
no proof recorded yet, bootstrap trusted-root state, and executable proof state were allowed to alias into the same runtime value. - Closest positive-design companion: bridge-proof-state-sentinel-separation-and-default-value-discipline — useful when the sharper question is how
messages[_messageHash], trusted-root storage, and initialization state should have been represented sobytes32(0)could never mean bothno proof recordedandacceptable proof artifact. - Broader sentinel-semantics companion: sentinel-lane-separation-and-default-value-deauthorization — useful when the sharper cross-family question is how default values, bootstrap placeholders, and absent-state sentinels should be kept from inheriting live authority anywhere in the system.
- Positive-design companion: bridge-truth-security-object — useful when the sharper question is what one fused authenticity object
Replica.process()should have required before any destination-side release path treated a message as proven. - Positive-design companion: bridge-bounded-release-design — the second bridge lesson is that one forged-message bug plus copy-pastable calldata should not have been able to walk so much live custody out of the system before operators could react.
- Structural contrast: proof-to-message-binding-failure — those bugs keep the trusted root or header real but detach the executed leaf from it, whereas Nomad corrupted root-acceptance state itself by letting
bytes32(0)mean bothno proof recordedandacceptable committed root. - Operational contrast: verifier-input-independence-failure — KelpDAO and related cases ask whether honest-looking verifiers observed the same lie, while Nomad asks a harsher upstream question: what if the bridge contract itself already treats untouched proof state as authentic before any observation diversity even matters?
Control surface
-
The leverage sat in Replica upgrade logic, initializer state, root-validity tracking, and the exact semantics of
process()authentication. -
Nomad did not fail because a signer was compromised or because liquidity was thin. It failed because the contract that was supposed to distinguish valid cross-chain messages from arbitrary calldata stopped making that distinction.
-
Sources:
- bridge-proof-state-sentinel-separation-and-default-value-discipline
- https://medium.com/nomad-xyz-blog/nomad-bridge-hack-root-cause-analysis-875ad2e5aacd
- https://cloud.google.com/blog/topics/threat-intelligence/dissecting-nomad-bridge-hack
- https://www.coinbase.com/blog/nomad-bridge-incident-analysis
- https://rekt.news/nomad-rekt
-
Last reviewed: 2026-08-01 UTC