Nomad Bridge Exploit
A routine upgrade set the trusted root to bytes32(0), making every message hash valid. Hundreds of attackers simply copied the first exploit transaction to drain $190M in hours.
Background
Nomad was a cross-chain messaging bridge that allowed assets to move between Ethereum, Avalanche, Moonbeam, and other chains. Messages from one chain were validated using a Merkle root — a cryptographic commitment to a set of valid messages.
A routine upgrade to the contract introduced a catastrophic initialization bug.
The Vulnerability
During the upgrade, the "committed root" — the trusted Merkle root against which messages were validated — was accidentally set to bytes32(0). The message processing code checked if a message was proven against an acceptable root.
The problem: messages[anyUnknownHash] returns 0 (the default mapping value in Solidity). And bytes32(0) was now a trusted root. So every message was valid.
function process(bytes memory _message) public returns (bool) {
bytes32 _messageHash = _message.keccak();
// messages[unknown hash] returns 0 by default in Solidity
// acceptableRoot(0) returns true because 0x000... was set as trusted!
require(
acceptableRoot(messages[_messageHash]),
"!proven"
);
// Attacker just needed to craft a message claiming to bridge tokens
// Any message hash would pass the require above
_process(_message);
}What Happened
- One attacker noticed the vulnerability and crafted a transaction claiming to bridge $100M USDC to themselves
- The transaction succeeded because the zero-root made any message valid
- Other observers on Etherscan and the mempool noticed the transaction
- They simply copied it, changing only their own recipient address — no hacking skill required
- Within hours, hundreds of "copycats" drained the remaining $90M
- It became the most "democratic" exploit in DeFi history — literally anyone could participate
Impact
$190M drained in hours. The bridge was completely emptied. Unlike Euler Finance, the Nomad exploit funds were largely not returned. The protocol shut down and has not relaunched.
Lessons Learned
The zero value (0, false, address(0), bytes32(0)) is the default in Solidity. Using defaults as "trusted" or "valid" states is a critical anti-pattern.
- Verify initialization values: After every upgrade, verify that critical security parameters are set correctly — not just set
- Never use zero as a trusted value: Default Solidity values (0, false, 0x0) should never be treated as valid or trusted
- Add post-upgrade invariant checks: Write automated tests that assert critical security invariants after each upgrade
- Timelocks on upgrades: A timelock would have given the community time to spot the misconfiguration before funds were at risk
- Monitor your contracts: Real-time monitoring would have caught the first exploit transaction and allowed emergency pausing