OpenZeppelin UUPSUpgradeable Uninitialized-Implementation selfdestruct Bricking Near Miss (2021-09-09)
- Name: OpenZeppelin UUPSUpgradeable uninitialized-implementation
selfdestructbricking near miss - Date: 2021-09-09
- Protocol: OpenZeppelin UUPSUpgradeable ecosystem
- Chain: Ethereum, Polygon, xDAI / Gnosis, BNB Chain, Avalanche
- Category: unprotected initializer / implementation takeover / implementation-side upgrade auth /
delegatecall-selfdestructbricking / ecosystem near miss - Loss: no confirmed public exploit from the core issue before mitigation, but OpenZeppelin said it privately initialized 150+ vulnerable implementations across five networks; iosiro separately described more than
$50Mof at-risk value across four privately reported projects, including about$44Min KeeperDAO vaults and about$6.95Min Rivermen NFTs - Summary: OpenZeppelin’s UUPSUpgradeable pattern put upgrade logic inside the implementation contract rather than on the proxy. In vulnerable releases, if the implementation contract itself was left uninitialized, an attacker could initialize that implementation directly, become its upgrade authority, and call
upgradeToAndCall()on the implementation instead of the proxy. That let attacker-chosen code execute throughDELEGATECALLin the implementation context and ultimately destroy the implementation withSELFDESTRUCT. Every proxy pointing at that implementation would then delegate into an address with no code. Because UUPS kept upgrade logic on the implementation, those proxies could not simply use a separate proxy-admin path to recover. OpenZeppelin responded before a known public drain by privately initializing more than 150 implementations and shipping Contractsv4.3.2with anonlyProxyguard. - Trigger / entry point:
- UUPS proxies delegated both business logic and upgrade logic to the implementation contract.
- Many teams correctly initialized the proxy state but left the implementation contract itself uninitialized, assuming that implementation-side state was operationally irrelevant.
- In OpenZeppelin Contracts
v4.1.0throughv4.3.1, the implementation still exposedupgradeTo()andupgradeToAndCall()as callable logic on that implementation contract. - The upgrade path internally used
DELEGATECALL, which made direct implementation takeover more dangerous than an ordinary stray owner slot.
- Root cause:
- The security model understated how much authority still lived in the implementation contract itself.
- UUPS let authorization for upgrades read implementation-side state, so an uninitialized implementation could appoint its own upgrade admin even though proxy users only interacted with proxy state.
- Once that implementation-side authority was captured, the attacker could reach a dangerous
DELEGATECALLprimitive from the implementation directly. - The design difference from Transparent proxies mattered: bricking the implementation also removed the upgrade path, because that path lived in the implementation that had just been destroyed.
- Exploit path:
- The attacker identifies a UUPS proxy whose implementation contract is uninitialized.
- They call the implementation’s initializer directly and set themselves as owner / upgrade authority on the implementation.
- They call
upgradeToAndCall()on the implementation itself, not on the proxy. - A practical proof of concept first used the upgrade path’s
DELEGATECALLcapability to patch the rollback-test storage slot, then invoked a second upgrade call whose delegated payload executedSELFDESTRUCTagainst the implementation. - The implementation code disappears, while proxies continue pointing at that same now-empty address.
- User calls routed through those proxies can no longer execute business logic or upgrade logic, so funds or control surfaces behind the proxies become stuck.
- What the attacker needed to be true:
- The system had to use the UUPS pattern rather than a Transparent proxy with separate proxy-side admin logic.
- The implementation contract had to remain uninitialized and therefore able to assign upgrade authority.
- The implementation had to expose OpenZeppelin’s vulnerable upgrade path with reachable
upgradeTo()/upgradeToAndCall()behavior. - The proxies depending on that implementation had to lack any independent admin surface that could repoint them after implementation destruction.
- Technical anatomy worth remembering:
- This was not a
proxy storage overwritten directlyincident. The first seized object was the implementation contract’s own auth state. - The core lesson is uncomfortable: implementation-side state is not harmless if the implementation contains live upgrade logic or other privileged execution paths.
- UUPS’s rollback-safety mechanism improved upgrade robustness against accidental lockups, but in these versions it also preserved a powerful
DELEGATECALLgadget once implementation auth was captured. - The incident is a modern proxy-era cousin of Parity’s shared-library freeze: a supposedly inert shared code object still retained mutable authority and could brick every dependent instance at once.
- OpenZeppelin’s whitehat mitigation also preserved an operational lesson: when forced to initialize abandoned implementations, writing gibberish or sacrificial values and then destroying the helper admin surface is safer than trying to mirror live proxy state.
- This was not a
- Representative transactions and contracts:
- OpenZeppelin mitigation EOA:
0x37E8d216c3f6c79eC695FBD0cB9842e62fB84370 - OpenZeppelin batching contract used for mass initialization:
0x310fAC62C976d8F6FDFA34332a56EA1a05493b5b - Hotfix release: OpenZeppelin Contracts
v4.3.2 - Advisory scope: OpenZeppelin Contracts
v4.1.0throughv4.3.1 - Concrete at-risk case documented by iosiro: KeeperDAO
HidingVaultNFTimplementation0xce03849d56d7bf63c5fea2152488ee6d69684cbf - Concrete at-risk case documented by iosiro: KeeperDAO
HidingVaultNFTProxy0xe2ad581fc01434ee426bb3f471c4cb0317ee672e - Example user withdrawal tx iosiro showed would have reverted if the implementation had already been bricked:
0x813da76dbe0f5dc42356c4b8a777461fdfe2619d4222b94e87ece20934d85546
- OpenZeppelin mitigation EOA:
- Response / mitigation:
- OpenZeppelin published a public advisory on 2021-09-09 instructing all UUPS users to initialize implementation contracts directly.
- Before broad disclosure of the full bug mechanics, OpenZeppelin privately initialized more than 150 vulnerable implementations across Ethereum mainnet, Polygon, xDAI / Gnosis, BNB Chain, and Avalanche.
- OpenZeppelin relayed the mainnet mitigation privately through Taichi to reduce generalized frontrunning risk.
- The
v4.3.2hotfix added anonlyProxymodifier so upgrade functions could no longer be called directly on the implementation. - OpenZeppelin also updated docs and codegen so implementation contracts would be proactively initialized or locked on deployment.
Three implementation-and-upgrade-authority promotions this incident collapsed
-
A UUPS implementation object existed and was still directly reachable as an initialization surface
- Every affected deployment already had a real implementation contract onchain, and many teams had left that implementation uninitialized because the proxy itself appeared fully configured.
- That is only the weakest fact:
a shared code object with setup verbs still existed and could still be called directly.
-
That implementation-side state still counted as exact upgrade-auth truth for this fleet’s real upgrade rail
- The harder question was whether implementation-side ownership was truly irrelevant once proxy-side ownership had been set.
- The incident broke here: in vulnerable UUPS releases, direct implementation initialization could still make
_authorizeUpgrade()succeed on the implementation’s ownupgradeTo()/upgradeToAndCall()path, so supposedly inert implementation state was silently promoted into canonical upgrade authority.
-
That accepted implementation-side authority still deserved fleet-wide code and liveness authority after auth capture
- Even once implementation auth had been captured, healthy design still could have denied that authority a direct line to fleet-wide bricking.
- Instead, the accepted implementation-side upgrade rail retained a
DELEGATECALL-reachable path that could patch rollback state, execute attacker-chosen code, and ultimatelySELFDESTRUCTthe implementation every dependent proxy still trusted.
Pattern tags
- unprotected-initializer-reinitialization
- shared-code-singleton-retained-authority-bricking
- privileged-upgrade-abuse
- Implementation-side upgrade rail remained directly reachable on a supposedly inert shared code object
- Shared dependency blast radius collapse / one unsealed implementation could brick every dependent proxy instance
Why the exploit worked
- UUPS put the upgrade primitive on the implementation itself, so
implementation state is irrelevantwas never actually true. Once_authorizeUpgrade()read attacker-owned implementation state, the attacker had seized the only live upgrade rail that mattered. - The vulnerable releases did not yet force upgrade entrypoints to run only through an active proxy. That meant a direct call to the implementation could still reach
upgradeToAndCall()and its rollback-testdelegatecallmachinery in standalone context. - The implementation was not merely ownable in theory; it retained a fleet-wide destructive capability. A captured initializer did not just create a stray admin slot on an unused code blob — it created a path to remove the implementation runtime that every dependent proxy still trusted.
- The recovery asymmetry versus Transparent proxies mattered. Because UUPS keeps upgrade logic on the implementation, destroying that implementation also destroyed the mechanism needed to repoint the proxies, turning an auth mistake into a likely permanent freeze.
- The postmortem’s fix makes the healthy design target explicit: seal or disable implementation-side initialization, and make upgrade verbs context-bound with
onlyProxy-style guards so direct calls to the implementation are boring and harmless.
Detection and audit pivots
- For every proxy pattern in use, ask where the upgrade authority really lives at runtime: proxy admin, beacon admin, or the implementation itself. If the implementation contains the live upgrade verb, treat the implementation address as privileged infrastructure, not passive code.
- Enumerate direct-call behavior on implementations and singleton logic contracts.
initialize,upgradeTo,upgradeToAndCall, emergency upgrade helpers, and any path that can reachdelegatecallor code-destruction semantics should be tested in standalone context, not only through the proxy. - Review deployment evidence, not just source shape. A safe-looking UUPS implementation is still vulnerable if the implementation contract onchain never ran
_disableInitializers()or an equivalent sealing step. - Trace the blast radius of one implementation compromise. If multiple proxies point at the same implementation, ask whether one seized implementation can brick or redefine all of them at once and whether any surviving relink or emergency-exit path exists afterward.
- Treat rollback checks, upgrade safety guards, and other upgrade robustness machinery as exploit primitives after auth capture. If an attacker can satisfy
_authorizeUpgrade(), ask what code-execution surface the upgrade path gives them before assuming the failure ends at ownership takeover.
Failure-prevention checklist
- Seal every implementation and singleton logic contract at deployment. Constructors or post-deploy setup should call
_disableInitializers()or an equivalent one-way lock soimplementation uninitializedcan never become latent upgrade authority. - Bind upgrade verbs to proxy context, not merely to caller role.
onlyProxy,notDelegated, or equivalent context guards should make direct calls toupgradeTo()andupgradeToAndCall()on the implementation revert even if some implementation-side auth state is later captured. - Inventory implementation addresses as privileged fleet infrastructure. Monitoring should answer
which proxies point here, is initialization disabled here, and what direct-call verbs still work here?, not onlywhich proxy admins exist? - Treat upgrade-path
delegatecallas a post-auth high-severity primitive. If an authorized upgrade route can execute arbitrary delegated code, review for code-destruction, storage-slot patching, ordelegatecallchaining rather than assuming ownership takeover is the end of the story. - Avoid recovery designs that depend exclusively on the code object that can itself be bricked. If one implementation compromise would also destroy the only repointing path, add a separate emergency migration, beacon, admin, or exit rail, or accept that the fleet is effectively one-shot.
- Model shared implementations as shared blast-radius objects. If many proxies depend on one implementation, sealing, rotating, or auditing that implementation deserves the same operational care as a multisig or bridge verifier, because one mistake can freeze every dependent instance at once.
- Make deployment verification check onchain state, not only source imports. CI, scripts, and audits should confirm the implementation was actually initialized or locked onchain, that upgrade functions are context-bound, and that no abandoned implementation instance remains live after redeploys.
- Keep implementation initialization values sacrificial and non-operational. If emergency sealing requires direct initialization, use gibberish roles or burner admins that cannot later be mistaken for a real governance path, exactly because implementation-side state should never become a meaningful operator surface.
- Rehearse fleet-wide incident response for shared implementation bugs. Teams should know how to enumerate dependent proxies, privately mitigate exposed implementations, and communicate recovery assumptions before a public proof of concept turns one library bug into ecosystem-wide copycat exploitation.
Audit prompts
- Where does upgrade authority actually live for this deployment right now, and what exact onchain object decides
_authorizeUpgrade()in the code path that matters? - Can the implementation or singleton logic contract still be initialized, reinitialized, or otherwise given meaningful authority through a direct call that bypasses proxy context?
- If I call
upgradeTo()orupgradeToAndCall()directly on the implementation, what exactly happens today, and does any guard prove the call came through a live proxy? - Once implementation-side auth is captured, what execution surface becomes reachable: arbitrary
delegatecall, rollback-slot writes, beacon swaps, code destruction, or only a bounded implementation pointer change? - If this implementation disappears or becomes corrupted, what exact surviving rail can repoint or rescue the dependent proxies, and has that rail been tested against a real bricked-implementation state?
- How many proxies, vaults, or downstream wrappers share this implementation, and what is the maximum freeze or theft blast radius if one implementation object is seized?
- Does deployment tooling prove
_disableInitializers()or an equivalent seal actually ran on every implementation address, including historical deployments, clones, and abandoned upgrade attempts? - Are any upgrade-safety helpers, rollback checks, or temporary compatibility shims giving an attacker a stronger primitive after auth capture than reviewers intuitively treat them as having?
- If a whitehat or operator had to seal this implementation today, what initialization values would they write, and could those values accidentally create a new live governance or ownership story later?
Lessons that should already be standard
- Shared code is not passive just because users only touch proxies. If the implementation still contains the live upgrade verb, the implementation address is part of the real privilege surface.
- Uninitialized implementation state is not a harmless housekeeping defect when authorization reads that state. In UUPS it can become fleet-wide upgrade authority, not just a stray owner slot.
Authorized upgrade pathandsafe recovery pathare different properties. If the same code object provides both, destroying that object can convert a takeover into a permanent freeze.- Upgrade robustness machinery can become exploit machinery after auth capture. Rollback checks, compatibility shims, and delegated setup hooks deserve the same adversarial review as explicit plugin systems.
- Fleet topology matters. One implementation bug with many dependent proxies is operationally closer to a shared bridge verifier or wallet library compromise than to an isolated app bug.
- Deployment evidence matters as much as source correctness. A patched library does not save instances whose implementation contracts were already deployed unsealed.
- Emergency mitigation should leave implementation-side state boring, sacrificial, and dead-ended. The healthy goal is not
set the implementation owner correctly; it ismake implementation-side ownership cease to matter at all.
Why this incident matters
- It is one of the cleanest ecosystem-scale reminders that
implementation contractis not synonymous withpassive code blob. - It shows how an uninitialized implementation can become more than an ownership-takeover bug: in UUPS it can become a one-shot fleet-bricking primitive.
- The case is especially useful because the danger was caught before a canonical public drain, preserving a high-signal near miss rather than only post-loss folklore.
- It also sharply separates UUPS from Transparent proxies: the same
destroy the implementationoutcome is much worse when the implementation also owns the only upgrade rail.
Internal linkages
- Core recurring pattern: unprotected-initializer-reinitialization.
- More specific fleet-bricking lens: shared-code-singleton-retained-authority-bricking.
- Closely related shared-code bricking incident: parity-multisig-walletlibrary-uninitialized-selfdestruct-freeze-2017-11-06.
- Adjacent control-surface family: privileged-upgrade-abuse.
Control surface
-
The leverage sat in implementation-deployment hygiene, direct reachability of implementation initializers, implementation-side ownership of the UUPS upgrade rail, and the
DELEGATECALL-based upgrade machinery that could be turned into code destruction. -
Teams did not mainly fail because proxy business logic was wrong. They failed because the supposedly secondary implementation object still defined who could exercise the only live upgrade capability.
-
Sources:
- https://forum.openzeppelin.com/t/security-advisory-initialize-uups-implementation-contracts/15301
- https://forum.openzeppelin.com/t/uupsupgradeable-vulnerability-post-mortem/15680
- https://github.com/OpenZeppelin/openzeppelin-contracts/security/advisories/GHSA-5vp3-v4hc-gx76
- https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/security/advisories/GHSA-q4h9-46xg-m3x9
- https://iosiro.com/blog/openzeppelin-uups-proxy-vulnerability-disclosure
- https://raw.githubusercontent.com/yehjxraymond/exploding-kitten/main/README.md
-
Last reviewed: 2026-08-02 UTC