All posts

How Three Bridges Accepted Forged Proofs

Nomad, BNB Chain's Token Hub, and Hyperbridge each checked a Merkle proof before releasing funds, and each accepted a forged one. What a verifier has to be anchored to so that no forgery can pass.

A cross-chain bridge releases money on one chain because something happened on another. It can't see that other chain directly, so somewhere in the middle sits a verifier whose whole job is to decide whether the claim is true. The careful bridges make that verifier check a Merkle proof, a small piece of data that should only exist if the claimed transaction really is in the source chain's history.

Nomad, BNB Chain's Token Hub, and Hyperbridge all did that, and all three accepted proofs that were forged. Between them they minted or released about $750M that the source chain never authorized. The proof was checked every time. What went wrong was what the check was actually comparing against, and in each case it turned out to be something other than the source chain. Willow's proofs are built so that the check bottoms out in the source chain's own commitment, recomputed in full inside the proof, and none of these three forgeries gets past that. This post walks through the three verifiers, where each one's check stopped short, and how Willow anchors a proof so a forgery can't pass.

It's the mirror image of the KelpDAO exploit in April, where the verifier didn't check a proof at all and signed whatever an RPC node told it. Here the proof was in the loop, and the check was wrong.

Nomad

Nomad was a cross-chain messaging bridge. A message sent on one chain was committed into a Merkle tree, and a contract on the destination chain (the Replica) would release funds once the message had been proven against a root it trusted. The Replica kept a record of which roots were confirmed, and when a message was submitted for processing it looked up the root that message had been proven under and checked that the root was on the confirmed list.

In June 2022 the Replica was upgraded, and the upgrade initialized the contract with the zero value, 0x00, marked as a confirmed root. That was a routine thing to do. The problem was that a message nobody had ever proven also maps to zero, because that's the default value of an empty storage slot. So for any message at all, the lookup returned zero, zero was on the confirmed list, and the message counted as proven.

On August 1 someone noticed. The first exploit transaction moved 100 WBTC, and within minutes hundreds of people were copying its calldata on Etherscan, swapping in their own address, and submitting it. About $190M left the bridge in two and a half hours across roughly 960 transactions and 1,175 withdrawals. A little over $34M came back under a whitehat bounty.

Nothing about the proof was checked at all. The verifier's question was "is this root on my list?", and an empty slot answered yes.

BNB Chain

The BSC Token Hub bridged BNB from the old Beacon Chain to BNB Smart Chain. To withdraw, a relayer submitted a proof that a transfer was included in the Beacon Chain's state at some block height, and a precompile on BSC verified it as an IAVL range proof against that block's known application hash. That's the right shape. The root is the source chain's own commitment, and the proof has to hash up to it.

The bug was in the hashing. When the verifier recomputed the root from the proof, it used each path node's left child but never used the right one. Since the proof is supplied by the caller, the attacker took a real proof from a real block (block 110,217,401, from August 2020), attached a forged leaf carrying a mint instruction into the right-child field of a path node, and submitted it. The left side still hashed to the real root, so the verifier accepted the whole thing, forged leaf included.

Two transactions on October 6, 2022 minted 1M BNB each, about $570M. Validators halted the chain about 90 minutes after the second one. By then roughly $127M had been bridged out to Ethereum and other chains, and the remaining $430M or so sat frozen in the attacker's BSC address. The Moran hard fork on October 12 patched the precompile.

Here the verifier did check against the source chain's commitment. It just didn't check all of the proof against it, and the part it skipped is where the forgery lived.

Hyperbridge

Hyperbridge is a cross-chain messaging protocol built on Polkadot. Messages are committed into a Merkle Mountain Range, and its Ethereum contracts store a root for each state commitment they've received. To deliver a message, you submit an MMR proof, and the contract recomputes the root from the proof and checks it against the stored one.

The verifier's root computation was missing a bounds check. It never required the claimed leaf index to be smaller than the number of leaves. If you set the index to 1 on a tree with one leaf, the leaf fell out of range, the function found no leaves to hash, and it took a branch that used the first element of the proof directly as the root, without hashing the message into it at all. So the attacker submitted a proof whose first element was the stored root, and the function returned the stored root, and the check passed.

On April 13, 2026, the attacker used forged messages to take over the admin of several bridged token contracts on Ethereum, then minted a billion bridged DOT and sold it before the gateway was paused. The realized loss was about 108 ETH, around $237K. Polytope Labs shipped a patch within 72 hours and commissioned an outside audit, which found 14 issues across the verification stack.

The verifier had the right root. For one input, it never used it.

What the three have in common

All three verifiers were checking Merkle proofs, so the lesson isn't "use proofs." The question is what a verifier's check bottoms out in.

A verifier accepts a claim about the source chain when some condition is satisfied. If that condition can be satisfied without the claim actually being true on the source chain, the verifier can be forged, and somebody will eventually work out how. Nomad's condition bottomed out in a storage slot that anyone could hit with an empty message. BNB's bottomed out in the source chain's real root, but only half of the proof was made to hash up to it, so the other half could say anything. Hyperbridge's bottomed out in the real root too, except for one index where the verifier returned the proof's own first element instead of computing anything.

Turned around, a verifier is sound when the only way to satisfy it is to have the real source chain produce the thing being claimed. The root has to be the chain's own commitment, not a slot the operator initialized. The whole proof has to be forced through that commitment, every field, with no branch that returns early. That sounds obvious written down, and all three of these teams would have agreed with it. The failures were in the distance between the sentence and the code.

What each verifier's check bottomed out in: Nomad's in a storage slot where the empty value read as a confirmed root; BNB Token Hub's in half of the proof, since only left children were hashed; Hyperbridge's in the proof's own first element, returned as the root for an out-of-range index; Willow's in the chain's own commitment, with every receipt hashed, tries rebuilt, headers chained, and checked against Ethereum's record

How Willow anchors a proof

Willow's proofs are built so that the check bottoms out in the source chain's own commitment, and so that everything between the claim and that commitment is recomputed inside the proof.

For an Ethereum window, the proof takes the raw receipts of every block, hashes and RLP-decodes each one, rebuilds the receipts trie, and shows that the root it gets matches the root in the block header. It then chains the headers together and produces the hash of the last one. The Ethereum contract that verifies the proof checks that hash against Ethereum's own record of its history, through the beacon chain, so a proof over blocks Ethereum never produced fails. For a rollup like Base, the proof computes the output root from the blocks it covers and the contract checks it against the root the rollup finalized on Ethereum. In neither case is there a trusted-root slot that someone sets, and there's no relayer-supplied field the circuit skips over.

The proof is a zero-knowledge proof of a fixed circuit, which means the statement it can prove is pinned down in advance and doesn't change with the input. The verifier contract on Ethereum checks that one statement. The contracts are open source, and the adversarial test suite that tries to build forgeries against the circuit ships alongside it.

The circuit and the contract are still code, and code can be wrong. The difference is that there's one statement and one path through it, so it can be audited, fuzzed, and formally verified against its specification once, rather than kept correct across every code path a relayer can reach.

Where Willow is today

Willow is the first cryptographically verifiable blockchain indexer. We index a chain's blocks and hand back a proof that the data is correct and complete, which a contract on Ethereum can verify before it acts. Proofs of Ethereum and Base activity have both verified on mainnet, anchored to each chain's own commitment, and the same construction extends to any chain that commits its state to Ethereum.

If you run a bridge, or anything else that releases money on a claim about another chain, and you want to know what your verifier's check bottoms out in, we'd like to hear from you at [email protected].

Sources: rekt.news on Nomad · Nomad transaction counts (PostQuantum) · Immunefi on the BSC Token Hub · rekt.news on BNB · The Block on the Moran hard fork · Verichains on Hyperbridge · DarkNavy on Hyperbridge · Hyperbridge post-mortem

Put proven data under your app.

Partners

Request a call