The Parity Wallet Hacks: When Library Contracts Go Wrong
Two separate Parity multisig wallet vulnerabilities in 2017 together destroyed or permanently froze over $300 million. The first hack in July was a theft. The second in November wasn’t even intentional—a developer called a function out of curiosity and accidentally made $280 million permanently inaccessible. Both stemmed from the same architectural pattern handled incorrectly.
The Architecture
Parity’s multisig wallet used a delegatecall proxy design. Instead of deploying full wallet logic in every wallet contract, they separated the logic into a single shared WalletLibrary contract. Individual wallets held users’ funds and forwarded all calls to the library:
contract Wallet {
fallback() external payable {
address library = 0x...; // WalletLibrary
library.delegatecall(msg.data);
}
}
When delegatecall runs, it executes the library’s code but in the wallet’s storage context. The wallet’s ETH, its owners list, its required-signature threshold—all of it lives in the wallet’s storage, operated on by the library’s code. This made deployment cheap and upgrades possible, but it also meant every function in the library became callable through any wallet’s fallback.
The First Hack: July 19, 2017
The Flaw
WalletLibrary contained an initWallet function that set the wallet’s owner list and required signature count:
function initWallet(address[] _owners, uint _required, uint _daylimit) {
owners = _owners;
required = _required;
// more setup...
}
There was no check preventing initWallet from being called after the wallet was already initialized. Because the wallet’s fallback forwarded everything to the library, anyone could call initWallet on any deployed wallet and overwrite its owner list with their own address.
The Attack
Two transactions per wallet:
- Call
initWallet([attackerAddress], 1, maxUint256)through the wallet’s fallback. This made the attacker the sole owner with unlimited daily limits. - Call
execute(attackerAddress, walletBalance, "")to drain all funds to the attacker’s address.
The attacker drained 153,037 ETH (~$30 million at the time) from three projects: Swarm City, Edgeless Casino, and Aeternity.
A white hat group noticed the vulnerability in progress and raced to protect remaining vulnerable wallets using the same technique—taking control of wallets to secure funds before the attacker could reach them. They recovered approximately 377,000 ETH and returned it to rightful owners.
The Second Incident: November 6, 2017
A Worse Design Decision
When Parity patched the July hack, they added initialization protection to wallets. But the WalletLibrary contract itself was deployed as a regular contract with no special protection. It had the same initWallet function, now callable directly on the library address rather than through a wallet proxy.
A developer going by “devops199” discovered this. They called initWallet on the library contract directly, which worked—the library had no initialization guard on itself, only on wallets using it. They became the “owner” of the library.
Then they tried to undo it. They called kill:
function kill(address _to) onlyOwner external {
selfdestruct(_to);
}
The library contract was destroyed.
The Permanent Freeze
selfdestruct removes a contract’s bytecode from the blockchain. After it runs, the address holds no code—calls to it do nothing and return empty.
Every wallet that depended on the WalletLibrary for its logic now pointed to an empty address. The wallets still held their ETH. The ETH was still on-chain. But the only way to move it was to call functions on the library, and the library no longer existed.
Approximately 587 wallets holding ~513,774 ETH were frozen. At November 2017 prices, this was around $280 million. That ETH remains frozen today.
Devops199 filed a GitHub issue titled “I accidentally killed it.”
Why delegatecall Makes This Hard
delegatecall executes external code in the calling contract’s context. This is what makes proxy patterns work—the proxy holds the state, the logic contract holds the code, and delegatecall bridges them. But it creates a surface area problem: every public function in the logic contract is callable through the proxy’s fallback.
In the Parity case, that meant initWallet and kill—both administrative functions that should never have been externally reachable—were accessible through any wallet’s fallback handler. The wallet’s fallback had no selector filtering, so it forwarded initWallet calls just as readily as execute calls.
The Fixes That Should Have Been Applied
Initialization guards: The simplest fix is a boolean that gets set on first initialization:
bool private initialized;
function initWallet(address[] memory _owners, uint _required) external {
require(!initialized, "Already initialized");
initialized = true;
// setup...
}
OpenZeppelin’s Initializable contract is the canonical implementation of this pattern and has been audited extensively.
Explicit function forwarding: Instead of a catch-all fallback that forwards everything, the wallet should explicitly proxy only the functions that make sense to proxy. initWallet and kill should never appear in that list.
Non-destructible libraries: A library contract used by hundreds of dependent contracts should not contain selfdestruct. The gas savings of being able to destroy the contract are nowhere near worth the risk.
Immutable library addresses: If the library address is set at deploy time and never changeable, the failure mode where a bad library address gets substituted is eliminated.
The $280 Million That’s Still There
The frozen ETH is a recurring subject in Ethereum governance discussions. Several recovery proposals have been floated over the years, all of which would require a hard fork—changing the Ethereum protocol to allow recovery of funds from contracts that can no longer execute code. None have been implemented.
The community’s position has generally been that immutability is a core property of Ethereum and that making exceptions, even sympathetic ones, sets a problematic precedent. The funds remain frozen.
This is the most consequential outcome of the second Parity incident: it demonstrated that in the context of immutable blockchains, an accidental action can produce permanent, unrecoverable loss. There is no “undo.” There is no support ticket. There is no administrator who can fix it.