Parity Multisig (2017)
Two incidents, six months apart, against the same library contract pattern. The first (July 2017) drained ~$30M; the second (November 2017) frozen ~$280M permanently. Both stemmed from unprotected initialization functions in a shared library contract.
Timeline
First incident — July 19, 2017
- Attacker reinitialized the library contract used by Parity multisig wallets.
- Drained 153,037 ETH (~$30M at the time) from three wallets before being stopped by white-hat counter-exploitation.
Second incident — November 6, 2017
- A different user accidentally invoked
initWalleton the library contract, becoming its owner. - They then called
kill(theselfdestructfunction on the library), permanently freezing all wallets that used the library. - Total frozen: 513,774 ETH (~$280M at the time, ~$1.5B at later peaks).
Root Cause
Parity multisig wallets used a library-contract pattern: a small per-wallet proxy delegated calls to a shared library contract that held the actual logic. The library was deployed once and shared across thousands of wallets.
The library exposed:
initWallet(...)— the initialization function.kill()— a self-destruct function (intended for use through the per-wallet proxies).
Neither was protected against direct calls to the library itself.
First incident specifics
initWallet had no check preventing re-initialization. An attacker could call it on a deployed wallet and become its owner.
Second incident specifics
initWallet on the library contract (not on a wallet) was callable by anyone. The user devops199 called it, became the library owner, then called kill(), which executed selfdestruct on the library. Once the library was destroyed, every wallet's delegatecall returned to empty code; all funds were stuck.
Exploit Path
First incident
// Attacker, calling the wallet (not the library), but the wallet
// delegate-called to the library:
wallet.initWallet([attacker], 1, 0);
// Wallet's m_owners updated to [attacker].
wallet.execute(attacker, balance, ...);
// Funds transferred.
Second incident
// User called initWallet on the library directly:
library.initWallet([devops199], 1, 0);
// Library's m_owner now devops199.
library.kill(devops199);
// library is selfdestruct'd; code at address removed.
// Every wallet that delegate-called the library now fails.
What an Audit Should Have Caught
Two distinct findings, both surface-readable:
1. initWallet callable multiple times
function initWallet(...) {
// ❌ no check that this hasn't been called
m_owners = ...;
}
Fix: state-variable check require(m_owners.length == 0) or a dedicated initialized flag.
2. Library contract is independently functional
The library could be called as an EOA target, not just delegate-called. Any code at a deployable address must consider what happens when someone calls it directly with arbitrary arguments. The mitigation:
constructor() {
// disable initializers on the implementation itself
initialized = true;
}
This is now the standard pattern in OpenZeppelin's upgradeable contracts (_disableInitializers() in the constructor of the implementation, ensuring the impl can't be initialized as if it were a proxy instance).
3. selfdestruct reachable
The library exposed kill() reachable by its owner. Combined with the ability to become its owner, a single user could destroy the library and thousands of wallets.
The audit question: is selfdestruct necessary? If not, remove it. If yes, gate it behind multi-step governance.
Lessons
-
Initializers must be one-shot. Any function that sets ownership, admin, or other critical state must be callable exactly once, with an explicit guard. OpenZeppelin's
Initializablepattern and_disableInitializers()are standard. -
Implementation contracts must not be independently functional. When using proxy/library patterns, the impl must be inert if called directly. Disable initializers in its constructor; assume someone will call it.
-
selfdestructis a load-bearing footgun. Post-Dencun,selfdestructno longer fully removes code (EIP-6780), so the Parity scenario can't happen identically today. But analogous patterns (storage clearing, ownership renounce + new admin) remain. -
Shared library contracts amplify per-incident impact. A bug in the library affects every wallet using it. The Parity incidents were single-codebase incidents that became thousand-victim incidents because of the shared library. This is the same argument for being conservative about shared infrastructure (cf. the OpenSea registry, the Permit2 contract).
-
"It only matters when called from a wallet" is wrong. Reasoning by intended use is dangerous. The auditor must consider: what happens when this code runs in every context, including ones the developer didn't intend?
-
Operational response matters. The first incident triggered a white-hat counter-exploitation that recovered some funds. The second was unrecoverable because
selfdestructis final (under the rules then). Audits should consider not just "is there a bug" but "if there's a bug, what's the recovery path."
The Parity multisig incidents are foundational case studies for proxy / upgradeability audits (§4.12). Every modern proxy implementation that disables initializers on the impl is doing so because of this lesson.