Cross-Chain Bridges: Minting Money From Nothing
11 min read
July 11, 2026

Table of contents
👋 Introduction
Hey everyone!
The last two issues covered flash loans and reentrancy, attacks that drain one protocol at a time. Bridges are different. A bridge is the single largest pot of pooled collateral in crypto, and breaking its trust model does not drain a protocol. It mints money from nothing.
Cross-chain bridges have lost more value than any other category in Web3. Chainalysis counted roughly 2 billion USD stolen across bridge hacks in 2022 alone, about 69 percent of all crypto stolen that year. The reason is structural. A bridge locks real assets on one chain and mints wrapped copies on another, so the contract holding the collateral is a honeypot that grows with every user. Break the check that says “a deposit really happened,” and you walk away with the whole pool.
Here is what makes bridges uniquely fragile. Every other attack we have covered needs a bug in application logic. A bridge can be flawless in its business logic and still fall, because the weak point is the layer that decides who gets to say a deposit occurred. That layer is where every major bridge hack lives.
This week: the lock-and-mint model that makes bridges a target, validator keys stolen at Ronin, a forged guardian signature at Wormhole, the zero-value root that turned Nomad into a free-for-all, and the Merkle proof forgery that let one attacker mint a million tokens on BNB Chain.
Let’s get into it 👇
🌉 The Lock-and-Mint Model
An asset on Ethereum cannot move to another chain, because the two chains cannot read each other’s state. A bridge fakes the movement. You lock the real token in a contract on the source chain, an off-chain party watches for that lock event, and a contract on the destination chain mints a one-to-one wrapped copy. Burn the wrapped copy later and the original unlocks.
// Source chain: lock and emit an event off-chain watchers pick up
function deposit(uint256 amount, address recipient) external {
token.transferFrom(msg.sender, address(this), amount);
emit Deposit(msg.sender, recipient, amount, nonce++);
}
// Destination chain: mint, but ONLY if the attestation is trusted
function withdraw(bytes calldata message, bytes calldata proof) external {
require(verifyAttestation(message, proof), "invalid"); // the whole game
(address to, uint256 amount) = decode(message);
wrappedToken.mint(to, amount);
}
Everything hinges on verifyAttestation. Who is allowed to tell the destination chain that a lock happened on the source? The answer defines the trust model, and it ranges from a fixed multisig of external validators, to light clients that cryptographically verify source-chain block headers, to optimistic schemes that assume validity and allow challenges. The L2Beat bridge risk framework classifies bridges precisely along this axis, and it maps almost perfectly onto which ones got drained.
Think of a bridge as an oracle bolted to an access-control gate, wrapped around a vault. The vault logic barely matters. The oracle and the gate are the attack surface, and the rest of this issue is five ways attackers broke them.
🔑 Stealing the Keys: Ronin
The Ronin bridge required five of nine validator signatures to approve a withdrawal. An attacker who controls five keys does not need a single line of buggy Solidity. The contract does exactly what it was built to do, for the wrong person.
That is what happened in March 2022. The attacker obtained five validator keys: four belonging to Sky Mavis and one belonging to the Axie DAO. The DAO key was reachable because months earlier, during a period of heavy load, Sky Mavis had been allowlisted to sign on the DAO’s behalf. When the load subsided, nobody revoked that permission. The Halborn breakdown shows the attacker signed two withdrawals and moved around 625 million USD out of the vault.
The theft went unnoticed for six days. No monitoring watched the bridge balance, so the drain was discovered only when a user reported being unable to withdraw. A five-of-nine threshold sounds decentralized until you learn one organization effectively controlled five signers. Count the keys that actually sit in separate hands, not the number printed in the threshold.
✍️ Forging the Signature: Wormhole
Now the bug lives in the code. Wormhole let attackers produce a valid guardian signature without any guardian ever signing anything, and it came down to trusting an account the contract never verified.
Wormhole’s Solana contract checked guardian signatures using a Secp256k1 verification step that read from a “sysvar” account, a special account the runtime uses to expose instruction data. The verification routine used a deprecated function that never confirmed the supplied account was the genuine sysvar. So the attacker passed a fake sysvar account stuffed with a forged verification instruction. The check happily reported success. The CertiK analysis walks through the swapped account step by step.
// The shape of the bug: signature is "valid", signer is never checked
address signer = ecrecover(msgHash, v, r, s);
// require(isGuardian[signer], "not a guardian"); <-- the missing line
mint(to, amount); // attacker self-signs any message they want
With the guardian check bypassed, the attacker posted a message authorizing 120,000 wrapped ETH to be minted on Solana, backed by nothing locked on Ethereum. This is the exact failure mode from Issue 6 on JWT attacks: a signature can be cryptographically valid and still be worthless if you never confirm who signed it. Verifying a signature and verifying the signer are two different checks. Skip the second and the first is theater.
🎚️ Colliding the Selector: Poly Network
Not every access-control bug is a missing check. Sometimes the check exists and you route around it by making the contract call a function it was never meant to expose.
Poly Network’s cross-chain manager executed incoming messages by calling a target contract and method encoded in the message, and it never restricted which method. In Ethereum a function is dispatched by the first four bytes of the hash of its signature, its selector. The attacker brute-forced a harmless-looking method name whose selector collided with putCurEpochConPubKeyBytes, the function that sets the bridge’s keeper public keys. Because the manager itself owned the keeper store, the crafted message made the bridge overwrite its own validator set.
// The manager forwards message-supplied calldata without restricting the target method.
// A brute-forced method name collides with selector 0x41973cd9, so the manager
// ends up calling putCurEpochConPubKeyBytes(attackerKey) on itself.
(bool ok,) = toContract.call(abi.encodePacked(sig, args)); // sig chosen by attacker
With their own key installed as the sole keeper, the attacker signed off on their own withdrawals across every connected chain and moved around 610 million USD. The Kudelski analysis reconstructs the selector collision. The bug was not a broken signature check. It was an execution path powerful enough to rewrite who the valid signers were. Audit what a bridge’s privileged functions can call, not just who is allowed to call them.
🌳 The Zero Root: Nomad
Some bugs need a crafted signature. This one needed you to submit a transaction with your own address in it, and hundreds of strangers figured that out within hours.
Nomad proves that a message was approved by checking it against a set of trusted roots. A June 2022 upgrade initialized that trusted root to 0x00. Unset entries in the messages mapping also return 0x00. So every message that had never been seen before hashed to a slot returning 0x00, and the contract treated 0x00 as a proven root. Any fabricated withdrawal passed.
// After the upgrade, confirmAt[0x00] was a valid timestamp.
// A never-seen message maps to 0x00, so acceptableRoot() returns true.
require(acceptableRoot(messages[_leaf]), "!proven"); // messages[_leaf] == 0x00
What followed was the first mass-looting event in bridge history. Once the first exploit transaction appeared in the mempool, others copied it, swapped in their own destination address, and rebroadcast. No skill required. The Immunefi teardown documents how a single initialization value turned nearly 190 million USD into a public piñata. Initialization is not boilerplate. When a default value flows into a security check, the default becomes the exploit.
🍃 Faking the Proof: BNB Token Hub
Here is the one that should make you stop. The attacker did not steal a key or catch a misconfigured upgrade. They handed the bridge a mathematically forged proof that a deposit existed, and the proof checked out against a real block from two years earlier.
The BNB Chain Token Hub verified cross-chain messages using IAVL Merkle proofs, a tree structure where a message’s inclusion is proven by hashing a path up to a known root. The implementation had a flaw: it computed the root from the left child of a node and ignored a crafted right child. So the attacker injected a payload leaf that authorized minting, attached it as an unchecked right child, and the computed root still matched a genuine historical block root. The Immunefi analysis reconstructs the forged proof in detail.
Real proof path: leaf -> hash -> hash -> known_root (matches a real block)
Forged proof: [real left subtree] + [attacker leaf as right child]
verifier hashes only the left path -> same known_root
attacker's minting leaf rides along, unverified
The attacker deposited 100 BNB to register as a relayer, then submitted the forged proof to mint roughly one million BNB. Most got frozen when validators halted the chain, but the mechanism is what matters. A proof system is only as strong as the code that walks the proof. If the verifier skips a branch, the branch is where you hide the payload. You can replay these against mainnet state with a forge test --fork-url harness and watch the forged root validate.
📡 Community Radar
Verus-Ethereum bridge hack, May 2026 (Halborn)
A fresh reminder that the classics never die. Both sides of the Verus-Ethereum bridge validated cryptographic structure and notary signatures correctly. Neither side checked that the amount locked on the source equaled the amount released on the destination. The value-comparison step was simply missing from the validation function. An attacker locked around one cent and released 11.58 million USD. Four years after Nomad, a bridge shipped with no check that the money going out matched the money coming in.
🎯 Key Takeaways
The one thing to carry into your next Web3 engagement: on a bridge, the business logic is a distraction. Find the attestation layer, the code that decides a source-chain event really happened, and attack that. Every hack in this issue lived there. The vault logic was fine. The gate was not.
Trace the trust model to actual key custody, not the printed threshold. Ronin was five-of-nine on paper and effectively one organization in practice. When you assess a bridge, count independent signers, check whether temporary allowlists were ever revoked, and confirm someone monitors the bridge balance in real time. A six-day-old theft is a monitoring failure as much as a key-management one.
In the verification code itself, two patterns recur. First, valid-signature-wrong-signer: the contract confirms a signature is cryptographically sound but never confirms the signer is authorized, exactly the JWT confusion pattern one layer down. Second, default-value-as-proof: an uninitialized root, a zeroed mapping entry, or a skipped proof branch becomes a passing check. Grep every verification function for ecrecover results that never hit an allowlist, for initializer values that feed security checks, and for proof walks that touch only one child of a node.
For hands-on work, reach for Slither to flag unchecked calls and uninitialized state in bridge contracts, then Foundry to fork mainnet and replay the actual exploit transactions. If you see a multisig bridge, audit the signer set. If you see a light-client or proof-based bridge, audit the proof verifier line by line. If you see an upgradeable proxy, audit every initializer. The trust model tells you where to point the tools.
Practice:
- Ethernaut (OpenZeppelin) - the Delegation, Preservation, and proxy levels mirror the initialization and privileged-call bugs behind Nomad and Poly Network
- Damn Vulnerable DeFi (The Red Guild) - the Climber challenge covers timelock and uninitialized-execution takeover, the closest analogue to bridge init bugs
- Slither (crytic) - static analysis for unchecked external calls, missing access control, and uninitialized state
- Foundry (foundry-rs) - fork mainnet and replay historical bridge exploits as PoCs
- L2Beat bridge risk framework - taxonomy for classifying any bridge’s trust model and centralization risk
- Chainlink CCIP EVM best practices - how a modern bridge design defends the attestation and receiver layers
- Chainalysis cross-chain bridge hacks report - why bridges concentrate risk and attract attackers
- ethereum.org: introduction to bridges - trusted versus trustless models, a clean primer on the trust spectrum
Thanks for reading, and happy hunting!
— Ruben
Other Issues
Previous Issue
💬 Comments Available
Drop your thoughts in the comments below! Found a bug or have feedback? Let me know.