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.
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
Entries in the SAFE database that describe this failure. The first ones name this incident directly.
Every figure on this page comes from the post-mortems above, not from us. Losses are US dollars at the time of the incident.
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.