Skip to content
#09 largest Input validation 2022

The Nomad Bridge hack — $190M lost

Loss$190M
Date1 Aug 2022
ChainEthereum ↔ Moonbeam
Failure classInput validation
In assetsdrained by ~300 different addresses
Targetthe first crowdsourced robbery
1

What happened

A routine upgrade initialized the Replica contract with a committed root of 0x00…00. Because initialize() marks the committed root as confirmed, confirmAt[0x00] = 1 — the zero hash became a permanently trusted root. And in Solidity, an unset mapping entry is the zero hash. So messages[anyUnknownHash] returned 0x00, which acceptableRoot happily approved. Every unproven message was "proven". Then it went viral: people copied the first attacker's calldata, pasted in their own address, and sent it. Hundreds of them.

2

How the attack ran

  1. An upgrade sets the root to zeroinitialize() then marks it confirmed
  2. Send a message that was never provenAn unwritten mapping entry returns 0x00
  3. acceptableRoot(0x00) == trueSo every unproven message passes the !proven check
  4. Copy-paste looting~300 addresses reused the calldata — $190M
3

The code

Replica.sol — three functions, one default value
function initialize(..., bytes32 _committedRoot, ...) public initializer {
    ...
    confirmAt[_committedRoot] = 1;   // upgrade passed 0x00 ⇒ confirmAt[0x00] = 1
}

function acceptableRoot(bytes32 _root) public view returns (bool) {
    uint256 _time = confirmAt[_root];
    if (_time == 0) return false;
    return block.timestamp >= _time;   // 0x00 → confirmAt = 1 → true, forever
}

function process(bytes memory _message) public returns (bool _success) {
    bytes32 _messageHash = keccak256(_message);
    // an un-proven message: messages[_messageHash] == bytes32(0)
    require(acceptableRoot(messages[_messageHash]), "!proven");   // ← always true
    ...
}
→ result: any message, from anyone, executed as if the bridge had signed it
4

What would have caught it

What an audit looks for: the zero value is a real input. Ask of every mapping: "what does an entry that was never written mean here?" And treat upgrades as new deployments — the vulnerable value here was introduced by an initialization argument, not by any change in the code.
6

Sources

Every figure on this page comes from the post-mortems above, not from us. Losses are US dollars at the time of the incident.

Check your own contract for this

Input validation is one of the 203 classes the SaferICO scanner checks for. It will not review your signing process — but it will read your Solidity.

Run the scanner See how it is attacked Read the docs