Toutes les vulnérabilités
CRITICALWeb3exploited in the wildcurated

WEB3-PARITY-2017

Web3 · Ethereum · Parity Multisig Wallet

Résumé

The Parity multisig wallet, a popular way to hold Ethereum securely with multiple signers, suffered two disasters from the same design flaw in 2017. In July, an attacker exploited it to steal about $30 million. Then in November, a curious user poking at the code accidentally triggered the flaw in reverse and permanently froze about $150 million belonging to hundreds of wallets, locking it away forever with no way to recover it. Together they are the textbook lesson in smart-contract initialization, the danger of shared library code, and why an irreversible system can punish a single missing access check twice over.

How it happened

Parity's design was clever and, it turned out, fragile. Each user's wallet was a thin smart contract holding no logic of its own. To save gas, it used delegatecall to forward any call it did not recognise to a single shared WalletLibrary contract, which then executed in the calling wallet's storage context. The flaw was that the library's initWallet function, which sets the wallet's owners, was public and had no guard to stop it being called more than once.

That one omission caused both disasters. On 19 July 2017, an attacker called initWallet on several already-deployed wallets, overwrote their owner list with just their own address, set the required signatures to one, and called execute() to drain them, making off with 153,037 ETH, about $30 million, from token-sale wallets including aeternity, Swarm City, and Edgeless. (A community "White Hat Group" used the very same exploit defensively to rescue another ~377,000 ETH from still-vulnerable wallets and later returned it.) The hurried fix protected the individual wallet contracts but left the shared WalletLibrary itself sitting un-initialized, a gap a GitHub user had flagged weeks earlier that went unaddressed. So on 6 November 2017, a user called devops199, exploring the code, called the unprotected initWallet directly on the shared library, became its owner, and then called the library's kill() function, which ran selfdestruct and deleted the shared code. Every wallet that depended on it through delegatecall was instantly bricked: 513,774 ETH, more than $150 million across 587 wallets (including around $90 million of Polkadot's own ICO funds), frozen permanently. The library was the load-bearing wall, and someone had deleted it.

Why Parity still matters

Parity is the initialization-and-shared-code lesson, taught twice. An uninitialized, publicly callable initializer let anyone claim ownership (the July theft), and treating a delegatecall library as outside the trust boundary let someone destroy it (the November freeze). On an irreversible ledger, neither could be undone, and the frozen funds are gone to this day; a community vote on a recovery proposal (EIP-999) failed in April 2018, with roughly 55% against, on immutability grounds. The defences are now standard practice in smart contract development: protect every initializer with an "already initialized" guard such as OpenZeppelin's Initializable; treat any library reached via delegatecall as inside your trust boundary and deploy it initialized and locked; never use delegatecall as a catch-all fallback; gate selfdestruct and ownership changes behind explicit access control, or remove selfdestruct entirely (Ethereum has since deprecated it, partly because of this); and initialize a singleton implementation the moment it is deployed so no one else can claim it. The multisig was meant to add safety, and a library bug erased it.

Comment le corriger

  • There is no recovery for the frozen funds; the November freeze is permanent, which is itself the lesson of an unfixable bug on an immutable ledger.
  • For the theft, the only responses were to fork or rebuild; protect every initializer and lock deployed library and implementation contracts immediately so the pattern cannot recur.
  • Audit all delegatecall and proxy relationships for uninitialized implementations and publicly callable initializers before deploying.

Comment l’éviter

  • Protect every initializer with an initialized guard or OpenZeppelin Initializable; never leave init() publicly re-callable.
  • Treat any library reached via delegatecall as part of your trust boundary; deploy it initialized and locked.
  • Do not use delegatecall as a catch-all fallback; explicitly allowlist which library functions are externally reachable.
  • Gate selfdestruct and ownership-changing paths behind explicit access control, and prefer removing selfdestruct entirely.
  • After deploying a singleton implementation, call its initializer immediately so no one else can claim it.

Références

Vulnérabilités liées

Tout Web3 →