Skip to main content
Sigvex

Constructor State

Detects improper state initialization in constructors of upgradeable contracts where state is invisible to proxies.

Constructor State

Overview

The constructor state detector identifies upgradeable contracts that initialize critical state (owner, balances, configuration) inside the constructor instead of an initialize() function. In proxy patterns, the implementation contract’s constructor runs during deployment and writes to the implementation’s own storage. When a proxy delegatecalls into the implementation, it uses the proxy’s storage – where the constructor-set values do not exist.

This is the same class of vulnerability that contributed to the Wormhole bridge hack ($325M). Any contract using a proxy pattern where the owner is set in the constructor will have address(0) as the owner when accessed through the proxy.

Why This Is an Issue

Developers accustomed to non-upgradeable contracts naturally place initialization logic in the constructor. When migrating to a proxy architecture, forgetting to move this logic to an initialize() function means:

  • Owner is address(0): Access control checks against owner will fail or, worse, allow anyone to pass address(0) checks.
  • Token supply is zero: Initial minting done in the constructor leaves the proxy with zero total supply.
  • Configuration is default: Fee rates, oracles, and other parameters revert to Solidity’s default zero values.

How to Resolve

// Before: Vulnerable -- owner set in constructor
contract TokenV1 {
    address public owner;
    uint256 public totalSupply;

    constructor(uint256 initialSupply) {
        owner = msg.sender;               // Only in implementation storage
        totalSupply = initialSupply;       // Never visible via proxy
    }
}

// After: Fixed -- state set in initializer
import "@audited/proxy/utils/Initializable.sol";

contract TokenV1 is Initializable {
    address public owner;
    uint256 public totalSupply;

    constructor() {
        _disableInitializers();  // Prevent implementation from being initialized
    }

    function initialize(uint256 initialSupply) external initializer {
        owner = msg.sender;            // Written to proxy storage
        totalSupply = initialSupply;   // Visible via proxy
    }
}

Examples

Vulnerable

contract VaultImpl {
    address public admin;
    uint256 public feeRate;

    constructor() {
        admin = msg.sender;   // Lost when used behind proxy
        feeRate = 300;        // 3% fee -- proxy sees 0%
    }

    function setFee(uint256 newFee) external {
        require(msg.sender == admin);  // Always fails via proxy
        feeRate = newFee;
    }
}

Fixed

contract VaultImpl is Initializable {
    address public admin;
    uint256 public feeRate;

    constructor() { _disableInitializers(); }

    function initialize(uint256 _feeRate) external initializer {
        admin = msg.sender;
        feeRate = _feeRate;
    }

    function setFee(uint256 newFee) external {
        require(msg.sender == admin);
        feeRate = newFee;
    }
}

Sample Sigvex Output

[CRITICAL] constructor-state
  Owner/admin set in constructor of upgradeable contract
  Location: constructor @ block 0, instruction 1
  Confidence: 0.80

  The constructor writes to storage slot(s) that appear to set ownership:
  [0]. In proxy patterns, this state is set in the implementation
  contract's storage, NOT the proxy's storage.

Detection Methodology

  1. Proxy pattern identification: Checks for EIP-1967 storage slots, upgrade-related selectors (upgradeTo, proxiableUUID), and initialize() functions.
  2. Constructor analysis: Scans the constructor bytecode for SSTORE instructions.
  3. Slot filtering: Excludes EIP-1967 proxy slots and _disableInitializers() patterns (max-value writes to low-numbered initializer slots).
  4. Write categorization: Classifies writes as owner/admin (critical), balance/supply (high), or configuration (medium) based on slot naming heuristics and stored values.
  5. Context modifiers: Reduces confidence for audited libraries and known implementation contracts.

Limitations

False positives: Non-upgradeable contracts with initialize() functions (used for factory patterns rather than proxy upgrades) may be flagged. Contracts that intentionally set implementation-level state in the constructor (e.g., for _disableInitializers()) are filtered by the detector. False negatives: Constructors that delegate initialization to internal functions are not fully traced.

References