An upgradeable (proxy-based) contract sets state in a constructor. Constructor code runs in the implementation's own context, never in the proxy's storage, so the proxy is left uninitialised — owner is zero, guards are off — and an attacker can seize it.
// implementation constructor sets owner = msg.sender (deployer). // through the proxy, owner is still 0 (constructor never ran there). // attacker calls initialize()/unprotected setter -> becomes owner.
// use an initializer, not a constructor, and lock the implementation
constructor() { _disableInitializers(); }
function initialize(address o) external initializer { __Ownable_init(o); }
Largest recorded losses in the same failure class — related, not the same bug:
The SaferICO scanner runs 201 detectors over your Solidity source, SAFE-0129 among them. Paste an address or the source itself — a small per-scan fee, shown before you sign, or unlimited on any plan.