Initializer Vulnerabilities and _disableInitializers
Quick answer. Anyone can call the implementation's initializer and take whatever powers it grants; in 2017 that let someone destroy Parity's shared wallet library, freezing every wallet using it. Lock it by calling _disableInitializers() in the implementation's constructor.
Video transcript
Proxies cannot use constructors, so upgradeable contracts use an initializer. Until it runs, the first caller takes whatever powers it grants.
In November 2017, someone initialized Parity's shared wallet library, made themselves owner, then killed it, freezing every wallet using it. OpenZeppelin wrote that estimated losses may total over 500,000 ETH.
The fix is one constructor that calls disable initializers, locking the implementation while the proxy initializes normally. Since Dencun, attackers cannot self-destruct a live implementation on Ethereum, but they can still take what its initializer grants.
Deploy a proxy, then initialize it in a second transaction, and anyone can front-run that call. In July 2025, Dedaub described an attack that hit tens of contracts this way. Pass the initialize call as the proxy constructor's data.
Guardian's audits still find these. Poolshark's initialize could run again, and Ginza Gaming's lock was removed, likely for testing. M0 used initializer on an already initialized proxy, and Bracket skipped a parent initializer, leaving its vault ownerless.
Before you deploy, lock every implementation and use the right modifiers. Call each parent initializer once, put init data in the proxy constructor, and upgrade and reinitialize in one call. Then verify events, implementation slots and plugin checks.
A Guardian audit attacks upgrade and privileged flows, and Guardian also verifies deployments. Read the full article, or talk to Guardian at guardian.security.
Proxies cannot use constructors, so upgradeable contracts set themselves up in an initializer. Until it runs, the first caller takes the powers it grants.
OpenZeppelin's warning is plain: "An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation contract."
Here are the four ways initializers go wrong, with incidents and findings from Guardian's public audits.
Constructors Versus Initializers
A proxy delegatecalls its implementation. The implementation's constructor writes to the implementation's own storage, never the proxy's.
So setup moves to an initialize function with the initializer modifier, which lets it run once. Parent contracts expose __Name_init functions that only run inside it.
Leave the modifier off and initialize can run again. In Poolshark LMP-1, a Critical, anyone could call it after setup.
For how proxies work, see upgradeable smart contracts explained.
Uninitialized Implementations
The proxy gets initialized. The implementation behind it often does not, so anyone can call initialize on it directly.
Three cases
Parity, November 2017. Someone called initWallet on the shared wallet library, made themselves owner, then killed it.
OpenZeppelin wrote that estimated losses "may total over 500,000 ETH".
OpenZeppelin, September 2021. An advisory warned that UUPSUpgradeable 4.1.0 to 4.3.1 "may be vulnerable to an attack affecting uninitialized implementation contracts".
Wormhole, 2022. Whitehat satya0x found the core bridge implementation on Ethereum uninitialized. Wormhole paid a $10 million bounty, per Immunefi.
An attacker could have set their own guardians, then used submitContractUpgrade to delegatecall a self-destructing contract. The team fixed it the day it was reported.
Two changes have narrowed this path since:
- OpenZeppelin 4.3.2 stopped UUPS upgrade functions from running outside the active proxy.
- EIP-6780, live with Ethereum's Dencun upgrade on March 13, 2024:
SELFDESTRUCTdeletes code only in the creating transaction.
Lock it anyway
An attacker who owns an implementation can still confuse users and tools. One constructor closes it:
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
_disableInitializers sets the implementation's version to type(uint64).max. The proxy's storage is untouched, so initializing through the proxy still works.
Audits still find it missing. Footium L-01 and Nashpoint I-10 left implementations open, both since resolved.
In Ginza Gaming M-01, the fix review found the _disableInitializers constructor removed, "likely for testing purposes".
Front-Running Initialization
Deploy a proxy in one transaction and initialize it in the next, and anyone can call initialize in between.
In July 2025, Dedaub described CPIMP, an attack that had front-run proxy initializations "to insert malicious implementations". It hit "tens of contracts" on several chains.
The trap: "the (legitimate owner's) initialization transaction goes through, without reverting." Dedaub says SEAL 911 coordinated rescues that largely mitigated it.
Guardian flags the same gap in deploy scripts:
- Citrea I-01: a USDC.e deploy script from Circle's repository deployed and initialized the proxy in separate transactions.
- Ethos I-28: non-atomic deployment would let anyone initialize new contracts with their own values.
The fix is one transaction: pass the encoded initialize call as the proxy constructor's data. Since 5.6.0, OpenZeppelin's ERC1967Proxy reverts without it.
For the script side, see secure Foundry deployment scripts.
Reinitializer Versioning
initializer only runs on a fresh contract. An upgrade that adds state needs reinitializer(n), which runs once, and only if no version of n or higher ran before.
In v5 the version is a uint64; v4 capped it at 255. Versions can skip numbers, and OpenZeppelin leaves running them in order "up to the developer or operator".
- M0 M-01: new setup used
initializeron a proxy already at version 4; Guardian recommendedreinitializer. - Orderly L-33: the same pattern, so added
initializelogic could never run. - Valantis I-04: upgrading without calling
initializeV3in the same transaction would leave new values at zero.
Upgrade and reinitialize in one call with upgradeToAndCall. A reinitializer has no access control of its own, so restrict it to the admin as a backstop.
Initializers in Inherited Contracts
Constructors run for every parent automatically. Initializers do not, so initialize must call each parent's __Name_init once.
- Bracket H-03: no
__Ownable_initcall, so the vault had no owner and could not be upgraded. - Synthetix M-03: skipping
__ERC20Permit_initleft an incorrect permit domain separator. - Aria L-01: two skipped parent initializers were empty then, but could break if OpenZeppelin adds logic.
Calling two parent __Name_init functions that share an ancestor can initialize it twice. OpenZeppelin's Upgrades Plugins flag missing and repeated parent calls.
For storage gaps and UUPS authorization, see five upgradeable contract vulnerabilities.
Deploy Script Checklist
- Every implementation constructor calls
_disableInitializers(). - Every
initializecarriesinitializer; later setup usesreinitializer(n). - Every parent initializer is called exactly once.
- Every proxy gets its init data in the constructor, never a second transaction.
- Upgrades that add state call their reinitializer through
upgradeToAndCall. - Each implementation emitted
Initialized(type(uint64).max)at deployment. - Each proxy's EIP-1967 slot points at the reviewed implementation.
- The Upgrades Plugins pass; reinitializers are validated only when annotated.
How Guardian Helps
A Guardian smart contract audit reviews upgrade paths in design, then attacks privileged and upgrade flows in the code.
Guardian also verifies deployments. For USDT0, it matched proxies, implementations and admins byte-exact against the audited source; see the case study.
Frequently Asked Questions
What does _disableInitializers do?
It sets the contract's initialized version to the maximum uint64 value, so no initializer or reinitializer can run on it again. Called in an implementation's constructor, it locks the implementation without touching the proxy's storage.
Can an attacker front-run initialize?
Yes, if the proxy is deployed and initialized in separate transactions. Anyone can call initialize in between and take control. Pass the initialize call as the proxy constructor's data so both happen in one transaction.
When should I use reinitializer instead of initializer?
Use reinitializer(n) for setup added by an upgrade. The initializer modifier only works on a fresh contract, so on a live proxy it reverts. Each version runs once, and only if no higher version ran before.
Does EIP-6780 make uninitialized implementations safe?
No. It removes one outcome: since Dencun, SELFDESTRUCT deletes code only in the creating transaction, so an attacker cannot destroy a live implementation that way on Ethereum. They can still take whatever powers its initializer grants.
Sources
- OpenZeppelin Contracts (GitHub): Initializable.sol, v5.7.0
- OpenZeppelin Docs: Writing Upgradeable Contracts
- OpenZeppelin Docs: Using with Upgrades, Contracts 5.x
- OpenZeppelin Contracts (GitHub): CHANGELOG, 4.3.2 (September 14, 2021) to 5.7.0 (July 29, 2026)
- OpenZeppelin (GitHub advisory): UUPSUpgradeable vulnerability in OpenZeppelin Contracts (September 14, 2021)
- OpenZeppelin Contracts (GitHub): ERC1967Proxy.sol, v5.7.0
- OpenZeppelin: Parity Wallet Hack Reloaded (November 7, 2017)
- Immunefi: Wormhole Uninitialized Proxy Bugfix Review (May 20, 2022)
- Dedaub: The CPIMP Attack (July 15, 2025)
- Ethereum Improvement Proposals: EIP-6780, SELFDESTRUCT only in same transaction
- Ethereum Improvement Proposals: EIP-7569, Hardfork Meta: Dencun
- ethereum.org: Cancun-Deneb (Dencun) FAQ, activated March 13, 2024
- Guardian: Poolshark Limit (August 21, 2023)
- Guardian: Footium Protocol Review (November 24, 2025)
- Guardian: Nashpoint Contract Updates (March 1, 2026)
- Guardian: Ginza Gaming Protocol Review (December 9, 2024)
- Guardian: Citrea Stablecoin Bridge (September 29, 2025)
- Guardian: Ethos Network Vouching and Slashing (July 20, 2026)
- Guardian: M0 Liquidity Delivery (January 30, 2026)
- Guardian: Orderly Cross-Chain Yield Vault (February 24, 2025)
- Guardian: Valantis Overseer (January 21, 2026)
- Guardian: Bracket Contract Updates (September 24, 2025)
- Guardian: Synthetix SNX Vaults (May 2, 2025)
- Guardian: Aria Contract Updates (October 8, 2025)
Close the initialization gap before you deploy
Guardian reviews your initializers, upgrade path and deploy scripts, then verifies what actually reached the chain.