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 againstownerwill fail or, worse, allow anyone to passaddress(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 "@openzeppelin/contracts-upgradeable/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
- Proxy pattern identification: Checks for EIP-1967 storage slots, upgrade-related selectors (
upgradeTo,proxiableUUID), andinitialize()functions. - Constructor analysis: Scans the constructor bytecode for
SSTOREinstructions. - Slot filtering: Excludes EIP-1967 proxy slots and
_disableInitializers()patterns (max-value writes to low-numbered initializer slots). - Write categorization: Classifies writes as owner/admin (critical), balance/supply (high), or configuration (medium) based on slot naming heuristics and stored values.
- 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.
Related Detectors
- UUPS Initialization — detects uninitialized UUPS proxies
- Storage Collision — detects proxy storage collisions
- Initializer Reentrancy — detects reentrancy in initializers