Uninitialized UUPS Proxy
Detects UUPS upgradeable proxy contracts where the implementation contract is deployed without calling the initializer, leaving the proxy's ownership and upgrade authority unset.
Uninitialized UUPS Proxy
Overview
Remediation Guide: How to Fix Uninitialized UUPS Proxy
The UUPS initialization detector identifies UUPS (Universal Upgradeable Proxy Standard, EIP-1822) proxy contracts where the implementation contract is deployed without being properly initialized. In the UUPS pattern, the upgrade authority and contract ownership are set by an initialize() function — not a constructor — because constructors run in the context of the implementation address, not the proxy address. If initialize() is never called (or can be called by anyone after deployment), an attacker can call it first, claim ownership, and immediately upgrade the implementation to a malicious contract.
Sigvex detects this pattern by analyzing the presence of _disableInitializers() or equivalent protection in the implementation contract’s constructor, and by checking whether the initialize function has adequate access control to prevent front-running. The detector flags implementations where the constructor does not disable future initialization calls and the initialize function is callable by any address.
Why This Is an Issue
UUPS proxies delegate storage to the proxy contract but execution to the implementation. Because constructors run in the implementation’s storage context, any state set in the constructor is invisible to the proxy. All initialization — including setting the owner and the address authorized to upgrade — must happen through initialize(), which runs in the proxy’s context.
If an implementation contract is deployed and initialize() has not been called:
- The
ownervariable isaddress(0)— no one is the legitimate owner - An attacker monitoring the mempool can front-run the deployment transaction and call
initialize()first - The attacker becomes the owner and can call
upgradeTo()to point the proxy at a malicious implementation - All proxy state and funds are now under attacker control
Historical incidents include the Wormhole exploit class and multiple DeFi protocol post-mortems where initialization was not front-run-protected. OpenZeppelin’s security advisories recommend using _disableInitializers() in the implementation constructor since OZ contracts v4.6.
How to Resolve
// Before: Vulnerable — implementation can be initialized by anyone
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract VulnerableImplementation is Initializable, OwnableUpgradeable, UUPSUpgradeable {
// VULNERABLE: no constructor that disables initializers
// Anyone can call initialize() on the bare implementation
function initialize(address initialOwner) public initializer {
__Ownable_init(initialOwner);
__UUPSUpgradeable_init();
}
function _authorizeUpgrade(address) internal override onlyOwner {}
}
// After: Fixed — constructor disables initializers on the implementation
contract SecureImplementation is Initializable, OwnableUpgradeable, UUPSUpgradeable {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers(); // Prevents anyone from calling initialize() on implementation
}
function initialize(address initialOwner) public initializer {
__Ownable_init(initialOwner);
__UUPSUpgradeable_init();
}
function _authorizeUpgrade(address) internal override onlyOwner {}
}
Examples
Vulnerable Code
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract VulnerableVaultImplementation is Initializable, UUPSUpgradeable {
address public owner;
mapping(address => uint256) public balances;
// NO constructor with _disableInitializers()
// Attacker can call initialize() on the raw implementation contract
function initialize(address _owner) public initializer {
owner = _owner; // Attacker calls this first with their own address
}
function _authorizeUpgrade(address newImplementation) internal override {
require(msg.sender == owner, "Not owner");
// Attacker who called initialize() is now "owner" and can upgrade
}
function deposit() external payable {
balances[msg.sender] += msg.value;
}
}
Fixed Code
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract SecureVaultImplementation is Initializable, OwnableUpgradeable, UUPSUpgradeable {
mapping(address => uint256) public balances;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
// This call sets initializedVersion = type(uint8).max on the implementation
// Any future call to initialize() or reinitialize() will revert
}
function initialize(address initialOwner) public initializer {
__Ownable_init(initialOwner);
__UUPSUpgradeable_init();
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {
// Additional checks can be added here (e.g., timelock)
}
function deposit() external payable {
balances[msg.sender] += msg.value;
}
}
Sample Sigvex Output
{
"detector_id": "uups-initialization",
"severity": "critical",
"confidence": 0.91,
"description": "UUPS implementation contract VulnerableVaultImplementation does not call _disableInitializers() in its constructor. The initialize() function can be called by any address on the bare implementation, allowing an attacker to claim ownership and upgrade to a malicious implementation.",
"location": { "function": "constructor", "offset": 0 }
}
Detection Methodology
Sigvex identifies UUPS initialization vulnerabilities through the following steps:
- UUPS pattern detection: Identifies contracts inheriting from
UUPSUpgradeableor implementingupgradeTo(address)and_authorizeUpgrade(address)functions. - Constructor analysis: Checks whether the constructor contains a call to
_disableInitializers()(via its 4-byte selector) or an equivalent initialization lock mechanism. - Initialize function analysis: Analyzes the
initialize()function to determine whether it is protected byinitializermodifier (preventing re-initialization) and whether any additional caller restrictions apply. - Access control on upgrade: Verifies that
_authorizeUpgrade()contains adequate caller validation (e.g.,onlyOwneror equivalent), since a takeover via uninitialized implementation leads directly to upgrade capability.
Limitations
False positives:
- Contracts that use a custom initialization lock mechanism not based on OpenZeppelin’s
_disableInitializers()may be flagged if the pattern is not recognized. - Implementation contracts intended to be used only as library code (not behind a proxy) may be flagged if they expose an
initialize()function for documentation purposes.
False negatives:
- Custom proxy implementations that use non-standard initialization patterns (not
initializermodifier) may be missed. - Two-step initialization schemes where
initialize()sets apendingOwnerthat must be separately accepted may be considered protected even if the pending state is exploitable.
Related Detectors
- Access Control — the UUPS exploit succeeds by capturing the owner role
- Storage Collision — UUPS proxies are also susceptible to storage layout conflicts during upgrades
- Delegatecall — UUPS uses delegatecall; understanding delegatecall risks is essential for proxy security