Skip to main content
Sigvex

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:

  1. The owner variable is address(0) — no one is the legitimate owner
  2. An attacker monitoring the mempool can front-run the deployment transaction and call initialize() first
  3. The attacker becomes the owner and can call upgradeTo() to point the proxy at a malicious implementation
  4. 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 recommends using _disableInitializers() in the implementation constructor in modern upgradeable-proxy releases.

How to Resolve

// Before: Vulnerable — implementation can be initialized by anyone
import "@audited/proxy/utils/Initializable.sol";
import "@audited/access/OwnableUpgradeable.sol";
import "@audited/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 "@audited/proxy/utils/Initializable.sol";
import "@audited/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 "@audited/proxy/utils/Initializable.sol";
import "@audited/access/OwnableUpgradeable.sol";
import "@audited/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:

  1. UUPS pattern detection: Identifies contracts inheriting from UUPSUpgradeable or implementing upgradeTo(address) and _authorizeUpgrade(address) functions.
  2. Constructor analysis: Checks whether the constructor contains a call to _disableInitializers() (via its 4-byte selector) or an equivalent initialization lock mechanism.
  3. Initialize function analysis: Analyzes the initialize() function to determine whether it is protected by initializer modifier (preventing re-initialization) and whether any additional caller restrictions apply.
  4. Access control on upgrade: Verifies that _authorizeUpgrade() contains adequate caller validation (e.g., onlyOwner or 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 initializer modifier) may be missed.
  • Two-step initialization schemes where initialize() sets a pendingOwner that must be separately accepted may be considered protected even if the pending state is exploitable.
  • 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

References