Cross-Chain Bridges: Minting Money From Nothing

11 min read

July 11, 2026

Site Updates

💬 Comments Available

Drop your thoughts in the comments below! Found a bug or have feedback? Let me know.

🚧 Recent Migration

Migrated from Ghost to Astro. Spot any formatting issues? Report them!

Cross-Chain Bridges: Minting Money From Nothing

Table of contents

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:


Thanks for reading, and happy hunting!

— Ruben

Other Issues

Reentrancy: The Bug That Forked Ethereum
Reentrancy: The Bug That Forked Ethereum

Previous Issue

Comments

Enjoyed the article?

Stay Updated & Support

Get the latest offensive security insights, hacking techniques, and cybersecurity content delivered straight to your inbox.

Follow me on social media