OpenZeppelin UUPS
Summary
ExpandThe OpenZeppelin UUPS (Universal Upgradeable Proxy Standard) implementation provides a well-structured upgradeability pattern that moves upgrade logic into the implementation contract rather than the proxy, reducing proxy contract complexity and gas costs. However, this audit identified two significant vulnerabilities that undermine the security guarantees of the upgrade mechanism itself. The codebase demonstrates strong adherence to Solidity best practices and leverages established patterns such as EIP-1967 storage slots, but the findings reveal fundamental risks in the upgrade authorization and initialization lifecycle that could result in permanent loss of contract functionality or unauthorized state manipulation.
The most critical risk area concerns the upgrade authorization pathway on the implementation contract directly. Finding C-1 demonstrates that the `upgradeTo` function is callable on the implementation contract itself, not solely through the proxy. An attacker can invoke `upgradeTo` on the unproxied implementation with a malicious contract containing `selfdestruct`, permanently destroying the implementation bytecode and bricking all proxies that delegate to it. This attack vector was notably exploited in the September 2021 incident affecting UUPS-based contracts. Finding H-1 compounds this risk by identifying that implementation contracts deployed without calling `_disableInitializers` in their constructors remain open to unauthorized initialization, allowing an attacker to claim ownership of the implementation contract and subsequently invoke privileged upgrade functions as a precondition to the C-1 attack.
Findings
2 issues identified
UUPS upgradeTo callable directly on implementation — enables selfdestruct bricking
contracts/proxy/utils/UUPSUpgradeable.sol:27
View Details
Description
The upgradeTo and upgradeToAndCall functions lack an onlyProxy guard, allowing them to be called directly on the implementation contract. An attacker can initialize the implementation (H-1), become owner, then call upgradeTo with a selfdestruct contract. During the rollback test in _upgradeToAndCallSecure, delegatecall executes selfdestruct, permanently destroying the implementation and bricking all proxies.
Impact
Permanent destruction of the implementation contract. All proxies delegating to it become non-functional and all funds are permanently locked. This is irreversible. CVE-2021-41264.
Recommendation
Add an onlyProxy modifier using an immutable self-reference: store address(this) at construction, then require address(this) != __self in the modifier. Apply to both upgradeTo and upgradeToAndCall.
Fix
The immutable __self variable stores the implementation's own address at deploy time. The onlyProxy modifier checks that address(this) != __self (proving execution is via delegatecall) and that the proxy's implementation slot points back to this contract. This prevents direct calls to upgradeTo on the implementation, blocking the selfdestruct attack chain.
contracts/proxy/utils/UUPSUpgradeable.sol
Initializable lacks _disableInitializers — implementation contracts can be initialized by anyone
contracts/proxy/utils/Initializable.sol:8
View Details
Description
The Initializable contract has no _disableInitializers() function. Implementation contracts deployed behind proxies have their own storage where _initialized is false. Anyone can call initialize() directly on the implementation contract to become its owner, which is a prerequisite for the C-1 selfdestruct attack.
Impact
An attacker can take ownership of any unprotected implementation contract by calling its initialize function directly. This enables the critical selfdestruct attack in C-1 and may have additional protocol-specific consequences. Real-world precedent: Audius governance takeover (Sept 2022).
Recommendation
Add a _disableInitializers() internal function that sets _initialized to true. Document that all implementation contracts must call _disableInitializers() in their constructor.
Fix
Adding _disableInitializers() allows implementation contracts to call it in their constructor (e.g., constructor() { _disableInitializers(); }), which sets _initialized = true on the implementation's own storage. This prevents anyone from calling initialize() directly on the implementation. Proxy instances are unaffected because they have separate storage where _initialized starts as false.
contracts/proxy/utils/Initializable.sol
Conclusion
ExpandThe OpenZeppelin UUPS implementation reflects a high standard of code quality in its proxy delegation mechanics, storage layout management, and adherence to EIP-1967. The upgrade pattern itself is architecturally sound and gas-efficient compared to transparent proxy alternatives. Nevertheless, the two findings identified in this audit represent a compounding attack chain of serious concern: an uninitialized implementation contract (H-1) provides the access vector through which the critical implementation destruction vulnerability (C-1) can be executed, meaning the combination of these two issues presents a realistic and severe threat to any deployed proxy system relying on this code.
Remediation of C-1 should be treated as the highest priority and addressed immediately by adding an onlyProxy modifier or equivalent check to the upgradeTo and upgradeToAndCall functions, ensuring they revert when called directly on the implementation rather than through a delegatecall from the proxy. Finding H-1 should be remediated concurrently by introducing a _disableInitializers call within the constructor of all implementation contracts, which sets the initialization state to its maximum value and prevents any post-deployment initialization on the implementation itself. Both fixes should be deployed together, as resolving H-1 alone does not fully mitigate the C-1 attack surface if other authorization bypasses exist, and resolving C-1 alone leaves implementation contracts exposed to unauthorized state changes through reinitialization.
Legal Disclaimer: This report covers the code submitted for analysis. It does not account for infrastructure, deployment configuration, third-party dependencies, or changes made after the audit date. Automated analysis may produce false positives or miss context-dependent vulnerabilities. audited.xyz provides this report “as is” without warranty of any kind.